85628fbf5043357186791e6cba88a2de8185fe99
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $me $conf $money_char $date_format );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
7 use List::Util qw(min max);
8 use Date::Format;
9 use Text::Template 1.20;
10 use File::Temp 0.14;
11 use String::ShellQuote;
12 use HTML::Entities;
13 use Locale::Country;
14 use Storable qw( freeze thaw );
15 use FS::UID qw( datasrc );
16 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
17 use FS::Record qw( qsearch qsearchs dbh );
18 use FS::cust_main_Mixin;
19 use FS::cust_main;
20 use FS::cust_statement;
21 use FS::cust_bill_pkg;
22 use FS::cust_bill_pkg_display;
23 use FS::cust_bill_pkg_detail;
24 use FS::cust_credit;
25 use FS::cust_pay;
26 use FS::cust_pkg;
27 use FS::cust_credit_bill;
28 use FS::pay_batch;
29 use FS::cust_pay_batch;
30 use FS::cust_bill_event;
31 use FS::cust_event;
32 use FS::part_pkg;
33 use FS::cust_bill_pay;
34 use FS::cust_bill_pay_batch;
35 use FS::part_bill_event;
36 use FS::payby;
37
38 @ISA = qw( FS::cust_main_Mixin FS::Record );
39
40 $DEBUG = 0;
41 $me = '[FS::cust_bill]';
42
43 #ask FS::UID to run this stuff for us later
44 FS::UID->install_callback( sub { 
45   $conf = new FS::Conf;
46   $money_char = $conf->config('money_char') || '$';  
47   $date_format = $conf->config('date_format') || '%x';  
48 } );
49
50 =head1 NAME
51
52 FS::cust_bill - Object methods for cust_bill records
53
54 =head1 SYNOPSIS
55
56   use FS::cust_bill;
57
58   $record = new FS::cust_bill \%hash;
59   $record = new FS::cust_bill { 'column' => 'value' };
60
61   $error = $record->insert;
62
63   $error = $new_record->replace($old_record);
64
65   $error = $record->delete;
66
67   $error = $record->check;
68
69   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
70
71   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
72
73   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
74
75   @cust_pay_objects = $cust_bill->cust_pay;
76
77   $tax_amount = $record->tax;
78
79   @lines = $cust_bill->print_text;
80   @lines = $cust_bill->print_text $time;
81
82 =head1 DESCRIPTION
83
84 An FS::cust_bill object represents an invoice; a declaration that a customer
85 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
86 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
87 following fields are currently supported:
88
89 Regular fields
90
91 =over 4
92
93 =item invnum - primary key (assigned automatically for new invoices)
94
95 =item custnum - customer (see L<FS::cust_main>)
96
97 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
98 L<Time::Local> and L<Date::Parse> for conversion functions.
99
100 =item charged - amount of this invoice
101
102 =item invoice_terms - optional terms override for this specific invoice
103
104 =back
105
106 Customer info at invoice generation time
107
108 =over 4
109
110 =item previous_balance
111
112 =item billing_balance
113
114 =back
115
116 Deprecated
117
118 =over 4
119
120 =item printed - deprecated
121
122 =back
123
124 Specific use cases
125
126 =over 4
127
128 =item closed - books closed flag, empty or `Y'
129
130 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
131
132 =item agent_invid - legacy invoice number
133
134 =back
135
136 =head1 METHODS
137
138 =over 4
139
140 =item new HASHREF
141
142 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
143 Invoices are normally created by calling the bill method of a customer object
144 (see L<FS::cust_main>).
145
146 =cut
147
148 sub table { 'cust_bill'; }
149
150 sub cust_linked { $_[0]->cust_main_custnum; } 
151 sub cust_unlinked_msg {
152   my $self = shift;
153   "WARNING: can't find cust_main.custnum ". $self->custnum.
154   ' (cust_bill.invnum '. $self->invnum. ')';
155 }
156
157 =item insert
158
159 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
160 returns the error, otherwise returns false.
161
162 =item delete
163
164 This method now works but you probably shouldn't use it.  Instead, apply a
165 credit against the invoice.
166
167 Using this method to delete invoices outright is really, really bad.  There
168 would be no record you ever posted this invoice, and there are no check to
169 make sure charged = 0 or that there are no associated cust_bill_pkg records.
170
171 Really, don't use it.
172
173 =cut
174
175 sub delete {
176   my $self = shift;
177   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
178
179   local $SIG{HUP} = 'IGNORE';
180   local $SIG{INT} = 'IGNORE';
181   local $SIG{QUIT} = 'IGNORE';
182   local $SIG{TERM} = 'IGNORE';
183   local $SIG{TSTP} = 'IGNORE';
184   local $SIG{PIPE} = 'IGNORE';
185
186   my $oldAutoCommit = $FS::UID::AutoCommit;
187   local $FS::UID::AutoCommit = 0;
188   my $dbh = dbh;
189
190   foreach my $table (qw(
191     cust_bill_event
192     cust_event
193     cust_credit_bill
194     cust_bill_pay
195     cust_bill_pay
196     cust_credit_bill
197     cust_pay_batch
198     cust_bill_pay_batch
199     cust_bill_pkg
200   )) {
201
202     foreach my $linked ( $self->$table() ) {
203       my $error = $linked->delete;
204       if ( $error ) {
205         $dbh->rollback if $oldAutoCommit;
206         return $error;
207       }
208     }
209
210   }
211
212   my $error = $self->SUPER::delete(@_);
213   if ( $error ) {
214     $dbh->rollback if $oldAutoCommit;
215     return $error;
216   }
217
218   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
219
220   '';
221
222 }
223
224 =item replace OLD_RECORD
225
226 Replaces the OLD_RECORD with this one in the database.  If there is an error,
227 returns the error, otherwise returns false.
228
229 Only printed may be changed.  printed is normally updated by calling the
230 collect method of a customer object (see L<FS::cust_main>).
231
232 =cut
233
234 #replace can be inherited from Record.pm
235
236 # replace_check is now the preferred way to #implement replace data checks
237 # (so $object->replace() works without an argument)
238
239 sub replace_check {
240   my( $new, $old ) = ( shift, shift );
241   return "Can't change custnum!" unless $old->custnum == $new->custnum;
242   #return "Can't change _date!" unless $old->_date eq $new->_date;
243   return "Can't change _date!" unless $old->_date == $new->_date;
244   return "Can't change charged!" unless $old->charged == $new->charged
245                                      || $old->charged == 0;
246
247   '';
248 }
249
250 =item check
251
252 Checks all fields to make sure this is a valid invoice.  If there is an error,
253 returns the error, otherwise returns false.  Called by the insert and replace
254 methods.
255
256 =cut
257
258 sub check {
259   my $self = shift;
260
261   my $error =
262     $self->ut_numbern('invnum')
263     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
264     || $self->ut_numbern('_date')
265     || $self->ut_money('charged')
266     || $self->ut_numbern('printed')
267     || $self->ut_enum('closed', [ '', 'Y' ])
268     || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
269     || $self->ut_numbern('agent_invid') #varchar?
270   ;
271   return $error if $error;
272
273   $self->_date(time) unless $self->_date;
274
275   $self->printed(0) if $self->printed eq '';
276
277   $self->SUPER::check;
278 }
279
280 =item display_invnum
281
282 Returns the displayed invoice number for this invoice: agent_invid if
283 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
284
285 =cut
286
287 sub display_invnum {
288   my $self = shift;
289   if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
290     return $self->agent_invid;
291   } else {
292     return $self->invnum;
293   }
294 }
295
296 =item previous
297
298 Returns a list consisting of the total previous balance for this customer, 
299 followed by the previous outstanding invoices (as FS::cust_bill objects also).
300
301 =cut
302
303 sub previous {
304   my $self = shift;
305   my $total = 0;
306   my @cust_bill = sort { $a->_date <=> $b->_date }
307     grep { $_->owed != 0 && $_->_date < $self->_date }
308       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
309   ;
310   foreach ( @cust_bill ) { $total += $_->owed; }
311   $total, @cust_bill;
312 }
313
314 =item cust_bill_pkg
315
316 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
317
318 =cut
319
320 sub cust_bill_pkg {
321   my $self = shift;
322   qsearch(
323     { 'table'    => 'cust_bill_pkg',
324       'hashref'  => { 'invnum' => $self->invnum },
325       'order_by' => 'ORDER BY billpkgnum',
326     }
327   );
328 }
329
330 =item cust_bill_pkg_pkgnum PKGNUM
331
332 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
333 specified pkgnum.
334
335 =cut
336
337 sub cust_bill_pkg_pkgnum {
338   my( $self, $pkgnum ) = @_;
339   qsearch(
340     { 'table'    => 'cust_bill_pkg',
341       'hashref'  => { 'invnum' => $self->invnum,
342                       'pkgnum' => $pkgnum,
343                     },
344       'order_by' => 'ORDER BY billpkgnum',
345     }
346   );
347 }
348
349 =item cust_pkg
350
351 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
352 this invoice.
353
354 =cut
355
356 sub cust_pkg {
357   my $self = shift;
358   my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
359   my %saw = ();
360   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
361 }
362
363 =item open_cust_bill_pkg
364
365 Returns the open line items for this invoice.
366
367 Note that cust_bill_pkg with both setup and recur fees are returned as two
368 separate line items, each with only one fee.
369
370 =cut
371
372 # modeled after cust_main::open_cust_bill
373 sub open_cust_bill_pkg {
374   my $self = shift;
375
376   # grep { $_->owed > 0 } $self->cust_bill_pkg
377
378   my %other = ( 'recur' => 'setup',
379                 'setup' => 'recur', );
380   my @open = ();
381   foreach my $field ( qw( recur setup )) {
382     push @open, map  { $_->set( $other{$field}, 0 ); $_; }
383                 grep { $_->owed($field) > 0 }
384                 $self->cust_bill_pkg;
385   }
386
387   @open;
388 }
389
390 =item cust_bill_event
391
392 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
393
394 =cut
395
396 sub cust_bill_event {
397   my $self = shift;
398   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
399 }
400
401 =item num_cust_bill_event
402
403 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
404
405 =cut
406
407 sub num_cust_bill_event {
408   my $self = shift;
409   my $sql =
410     "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
411   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
412   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
413   $sth->fetchrow_arrayref->[0];
414 }
415
416 =item cust_event
417
418 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
419
420 =cut
421
422 #false laziness w/cust_pkg.pm
423 sub cust_event {
424   my $self = shift;
425   qsearch({
426     'table'     => 'cust_event',
427     'addl_from' => 'JOIN part_event USING ( eventpart )',
428     'hashref'   => { 'tablenum' => $self->invnum },
429     'extra_sql' => " AND eventtable = 'cust_bill' ",
430   });
431 }
432
433 =item num_cust_event
434
435 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
436
437 =cut
438
439 #false laziness w/cust_pkg.pm
440 sub num_cust_event {
441   my $self = shift;
442   my $sql =
443     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
444     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
445   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
446   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
447   $sth->fetchrow_arrayref->[0];
448 }
449
450 =item cust_main
451
452 Returns the customer (see L<FS::cust_main>) for this invoice.
453
454 =cut
455
456 sub cust_main {
457   my $self = shift;
458   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
459 }
460
461 =item cust_suspend_if_balance_over AMOUNT
462
463 Suspends the customer associated with this invoice if the total amount owed on
464 this invoice and all older invoices is greater than the specified amount.
465
466 Returns a list: an empty list on success or a list of errors.
467
468 =cut
469
470 sub cust_suspend_if_balance_over {
471   my( $self, $amount ) = ( shift, shift );
472   my $cust_main = $self->cust_main;
473   if ( $cust_main->total_owed_date($self->_date) < $amount ) {
474     return ();
475   } else {
476     $cust_main->suspend(@_);
477   }
478 }
479
480 =item cust_credit
481
482 Depreciated.  See the cust_credited method.
483
484  #Returns a list consisting of the total previous credited (see
485  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
486  #outstanding credits (FS::cust_credit objects).
487
488 =cut
489
490 sub cust_credit {
491   use Carp;
492   croak "FS::cust_bill->cust_credit depreciated; see ".
493         "FS::cust_bill->cust_credit_bill";
494   #my $self = shift;
495   #my $total = 0;
496   #my @cust_credit = sort { $a->_date <=> $b->_date }
497   #  grep { $_->credited != 0 && $_->_date < $self->_date }
498   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
499   #;
500   #foreach (@cust_credit) { $total += $_->credited; }
501   #$total, @cust_credit;
502 }
503
504 =item cust_pay
505
506 Depreciated.  See the cust_bill_pay method.
507
508 #Returns all payments (see L<FS::cust_pay>) for this invoice.
509
510 =cut
511
512 sub cust_pay {
513   use Carp;
514   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
515   #my $self = shift;
516   #sort { $a->_date <=> $b->_date }
517   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
518   #;
519 }
520
521 sub cust_pay_batch {
522   my $self = shift;
523   qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
524 }
525
526 sub cust_bill_pay_batch {
527   my $self = shift;
528   qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
529 }
530
531 =item cust_bill_pay
532
533 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
534
535 =cut
536
537 sub cust_bill_pay {
538   my $self = shift;
539   map { $_ } #return $self->num_cust_bill_pay unless wantarray;
540   sort { $a->_date <=> $b->_date }
541     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
542 }
543
544 =item cust_credited
545
546 =item cust_credit_bill
547
548 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
549
550 =cut
551
552 sub cust_credited {
553   my $self = shift;
554   map { $_ } #return $self->num_cust_credit_bill unless wantarray;
555   sort { $a->_date <=> $b->_date }
556     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
557   ;
558 }
559
560 sub cust_credit_bill {
561   shift->cust_credited(@_);
562 }
563
564 =item cust_bill_pay_pkgnum PKGNUM
565
566 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
567 with matching pkgnum.
568
569 =cut
570
571 sub cust_bill_pay_pkgnum {
572   my( $self, $pkgnum ) = @_;
573   map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
574   sort { $a->_date <=> $b->_date }
575     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
576                                 'pkgnum' => $pkgnum,
577                               }
578            );
579 }
580
581 =item cust_credited_pkgnum PKGNUM
582
583 =item cust_credit_bill_pkgnum PKGNUM
584
585 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
586 with matching pkgnum.
587
588 =cut
589
590 sub cust_credited_pkgnum {
591   my( $self, $pkgnum ) = @_;
592   map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
593   sort { $a->_date <=> $b->_date }
594     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
595                                    'pkgnum' => $pkgnum,
596                                  }
597            );
598 }
599
600 sub cust_credit_bill_pkgnum {
601   shift->cust_credited_pkgnum(@_);
602 }
603
604 =item tax
605
606 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
607
608 =cut
609
610 sub tax {
611   my $self = shift;
612   my $total = 0;
613   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
614                                              'pkgnum' => 0 } );
615   foreach (@taxlines) { $total += $_->setup; }
616   $total;
617 }
618
619 =item owed
620
621 Returns the amount owed (still outstanding) on this invoice, which is charged
622 minus all payment applications (see L<FS::cust_bill_pay>) and credit
623 applications (see L<FS::cust_credit_bill>).
624
625 =cut
626
627 sub owed {
628   my $self = shift;
629   my $balance = $self->charged;
630   $balance -= $_->amount foreach ( $self->cust_bill_pay );
631   $balance -= $_->amount foreach ( $self->cust_credited );
632   $balance = sprintf( "%.2f", $balance);
633   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
634   $balance;
635 }
636
637 sub owed_pkgnum {
638   my( $self, $pkgnum ) = @_;
639
640   #my $balance = $self->charged;
641   my $balance = 0;
642   $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
643
644   $balance -= $_->amount            for $self->cust_bill_pay_pkgnum($pkgnum);
645   $balance -= $_->amount            for $self->cust_credited_pkgnum($pkgnum);
646
647   $balance = sprintf( "%.2f", $balance);
648   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
649   $balance;
650 }
651
652 =item apply_payments_and_credits [ OPTION => VALUE ... ]
653
654 Applies unapplied payments and credits to this invoice.
655
656 A hash of optional arguments may be passed.  Currently "manual" is supported.
657 If true, a payment receipt is sent instead of a statement when
658 'payment_receipt_email' configuration option is set.
659
660 If there is an error, returns the error, otherwise returns false.
661
662 =cut
663
664 sub apply_payments_and_credits {
665   my( $self, %options ) = @_;
666
667   local $SIG{HUP} = 'IGNORE';
668   local $SIG{INT} = 'IGNORE';
669   local $SIG{QUIT} = 'IGNORE';
670   local $SIG{TERM} = 'IGNORE';
671   local $SIG{TSTP} = 'IGNORE';
672   local $SIG{PIPE} = 'IGNORE';
673
674   my $oldAutoCommit = $FS::UID::AutoCommit;
675   local $FS::UID::AutoCommit = 0;
676   my $dbh = dbh;
677
678   $self->select_for_update; #mutex
679
680   my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
681   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
682
683   if ( $conf->exists('pkg-balances') ) {
684     # limit @payments & @credits to those w/ a pkgnum grepped from $self
685     my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
686     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
687     @credits  = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
688   }
689
690   while ( $self->owed > 0 and ( @payments || @credits ) ) {
691
692     my $app = '';
693     if ( @payments && @credits ) {
694
695       #decide which goes first by weight of top (unapplied) line item
696
697       my @open_lineitems = $self->open_cust_bill_pkg;
698
699       my $max_pay_weight =
700         max( map  { $_->part_pkg->pay_weight || 0 }
701              grep { $_ }
702              map  { $_->cust_pkg }
703                   @open_lineitems
704            );
705       my $max_credit_weight =
706         max( map  { $_->part_pkg->credit_weight || 0 }
707              grep { $_ } 
708              map  { $_->cust_pkg }
709                   @open_lineitems
710            );
711
712       #if both are the same... payments first?  it has to be something
713       if ( $max_pay_weight >= $max_credit_weight ) {
714         $app = 'pay';
715       } else {
716         $app = 'credit';
717       }
718     
719     } elsif ( @payments ) {
720       $app = 'pay';
721     } elsif ( @credits ) {
722       $app = 'credit';
723     } else {
724       die "guru meditation #12 and 35";
725     }
726
727     my $unapp_amount;
728     if ( $app eq 'pay' ) {
729
730       my $payment = shift @payments;
731       $unapp_amount = $payment->unapplied;
732       $app = new FS::cust_bill_pay { 'paynum'  => $payment->paynum };
733       $app->pkgnum( $payment->pkgnum )
734         if $conf->exists('pkg-balances') && $payment->pkgnum;
735
736     } elsif ( $app eq 'credit' ) {
737
738       my $credit = shift @credits;
739       $unapp_amount = $credit->credited;
740       $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
741       $app->pkgnum( $credit->pkgnum )
742         if $conf->exists('pkg-balances') && $credit->pkgnum;
743
744     } else {
745       die "guru meditation #12 and 35";
746     }
747
748     my $owed;
749     if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
750       warn "owed_pkgnum ". $app->pkgnum;
751       $owed = $self->owed_pkgnum($app->pkgnum);
752     } else {
753       $owed = $self->owed;
754     }
755     next unless $owed > 0;
756
757     warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
758     $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
759
760     $app->invnum( $self->invnum );
761
762     my $error = $app->insert(%options);
763     if ( $error ) {
764       $dbh->rollback if $oldAutoCommit;
765       return "Error inserting ". $app->table. " record: $error";
766     }
767     die $error if $error;
768
769   }
770
771   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
772   ''; #no error
773
774 }
775
776 =item generate_email OPTION => VALUE ...
777
778 Options:
779
780 =over 4
781
782 =item from
783
784 sender address, required
785
786 =item tempate
787
788 alternate template name, optional
789
790 =item print_text
791
792 text attachment arrayref, optional
793
794 =item subject
795
796 email subject, optional
797
798 =item notice_name
799
800 notice name instead of "Invoice", optional
801
802 =back
803
804 Returns an argument list to be passed to L<FS::Misc::send_email>.
805
806 =cut
807
808 use MIME::Entity;
809
810 sub generate_email {
811
812   my $self = shift;
813   my %args = @_;
814
815   my $me = '[FS::cust_bill::generate_email]';
816
817   my %return = (
818     'from'      => $args{'from'},
819     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
820   );
821
822   my %opt = (
823     'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
824     'template'      => $args{'template'},
825     'notice_name'   => ( $args{'notice_name'} || 'Invoice' ),
826   );
827
828   my $cust_main = $self->cust_main;
829
830   if (ref($args{'to'}) eq 'ARRAY') {
831     $return{'to'} = $args{'to'};
832   } else {
833     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
834                            $cust_main->invoicing_list
835                     ];
836   }
837
838   if ( $conf->exists('invoice_html') ) {
839
840     warn "$me creating HTML/text multipart message"
841       if $DEBUG;
842
843     $return{'nobody'} = 1;
844
845     my $alternative = build MIME::Entity
846       'Type'        => 'multipart/alternative',
847       'Encoding'    => '7bit',
848       'Disposition' => 'inline'
849     ;
850
851     my $data;
852     if ( $conf->exists('invoice_email_pdf')
853          and scalar($conf->config('invoice_email_pdf_note')) ) {
854
855       warn "$me using 'invoice_email_pdf_note' in multipart message"
856         if $DEBUG;
857       $data = [ map { $_ . "\n" }
858                     $conf->config('invoice_email_pdf_note')
859               ];
860
861     } else {
862
863       warn "$me not using 'invoice_email_pdf_note' in multipart message"
864         if $DEBUG;
865       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
866         $data = $args{'print_text'};
867       } else {
868         $data = [ $self->print_text(\%opt) ];
869       }
870
871     }
872
873     $alternative->attach(
874       'Type'        => 'text/plain',
875       #'Encoding'    => 'quoted-printable',
876       'Encoding'    => '7bit',
877       'Data'        => $data,
878       'Disposition' => 'inline',
879     );
880
881     $args{'from'} =~ /\@([\w\.\-]+)/;
882     my $from = $1 || 'example.com';
883     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
884
885     my $logo;
886     my $agentnum = $cust_main->agentnum;
887     if ( defined($args{'template'}) && length($args{'template'})
888          && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
889        )
890     {
891       $logo = 'logo_'. $args{'template'}. '.png';
892     } else {
893       $logo = "logo.png";
894     }
895     my $image_data = $conf->config_binary( $logo, $agentnum);
896
897     my $image = build MIME::Entity
898       'Type'       => 'image/png',
899       'Encoding'   => 'base64',
900       'Data'       => $image_data,
901       'Filename'   => 'logo.png',
902       'Content-ID' => "<$content_id>",
903     ;
904
905     $alternative->attach(
906       'Type'        => 'text/html',
907       'Encoding'    => 'quoted-printable',
908       'Data'        => [ '<html>',
909                          '  <head>',
910                          '    <title>',
911                          '      '. encode_entities($return{'subject'}), 
912                          '    </title>',
913                          '  </head>',
914                          '  <body bgcolor="#e8e8e8">',
915                          $self->print_html({ 'cid'=>$content_id, %opt }),
916                          '  </body>',
917                          '</html>',
918                        ],
919       'Disposition' => 'inline',
920       #'Filename'    => 'invoice.pdf',
921     );
922
923     my @otherparts = ();
924     if ( $cust_main->email_csv_cdr ) {
925
926       push @otherparts, build MIME::Entity
927         'Type'        => 'text/csv',
928         'Encoding'    => '7bit',
929         'Data'        => [ map { "$_\n" }
930                              $self->call_details('prepend_billed_number' => 1)
931                          ],
932         'Disposition' => 'attachment',
933         'Filename'    => 'usage-'. $self->invnum. '.csv',
934       ;
935
936     }
937
938     if ( $conf->exists('invoice_email_pdf') ) {
939
940       #attaching pdf too:
941       # multipart/mixed
942       #   multipart/related
943       #     multipart/alternative
944       #       text/plain
945       #       text/html
946       #     image/png
947       #   application/pdf
948
949       my $related = build MIME::Entity 'Type'     => 'multipart/related',
950                                        'Encoding' => '7bit';
951
952       #false laziness w/Misc::send_email
953       $related->head->replace('Content-type',
954         $related->mime_type.
955         '; boundary="'. $related->head->multipart_boundary. '"'.
956         '; type=multipart/alternative'
957       );
958
959       $related->add_part($alternative);
960
961       $related->add_part($image);
962
963       my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
964
965       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
966
967     } else {
968
969       #no other attachment:
970       # multipart/related
971       #   multipart/alternative
972       #     text/plain
973       #     text/html
974       #   image/png
975
976       $return{'content-type'} = 'multipart/related';
977       $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
978       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
979       #$return{'disposition'} = 'inline';
980
981     }
982   
983   } else {
984
985     if ( $conf->exists('invoice_email_pdf') ) {
986       warn "$me creating PDF attachment"
987         if $DEBUG;
988
989       #mime parts arguments a la MIME::Entity->build().
990       $return{'mimeparts'} = [
991         { $self->mimebuild_pdf(\%opt) }
992       ];
993     }
994   
995     if ( $conf->exists('invoice_email_pdf')
996          and scalar($conf->config('invoice_email_pdf_note')) ) {
997
998       warn "$me using 'invoice_email_pdf_note'"
999         if $DEBUG;
1000       $return{'body'} = [ map { $_ . "\n" }
1001                               $conf->config('invoice_email_pdf_note')
1002                         ];
1003
1004     } else {
1005
1006       warn "$me not using 'invoice_email_pdf_note'"
1007         if $DEBUG;
1008       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1009         $return{'body'} = $args{'print_text'};
1010       } else {
1011         $return{'body'} = [ $self->print_text(\%opt) ];
1012       }
1013
1014     }
1015
1016   }
1017
1018   %return;
1019
1020 }
1021
1022 =item mimebuild_pdf
1023
1024 Returns a list suitable for passing to MIME::Entity->build(), representing
1025 this invoice as PDF attachment.
1026
1027 =cut
1028
1029 sub mimebuild_pdf {
1030   my $self = shift;
1031   (
1032     'Type'        => 'application/pdf',
1033     'Encoding'    => 'base64',
1034     'Data'        => [ $self->print_pdf(@_) ],
1035     'Disposition' => 'attachment',
1036     'Filename'    => 'invoice-'. $self->invnum. '.pdf',
1037   );
1038 }
1039
1040 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1041
1042 Sends this invoice to the destinations configured for this customer: sends
1043 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
1044
1045 Options can be passed as a hashref (recommended) or as a list of up to 
1046 four values for templatename, agentnum, invoice_from and amount.
1047
1048 I<template>, if specified, is the name of a suffix for alternate invoices.
1049
1050 I<agentnum>, if specified, means that this invoice will only be sent for customers
1051 of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
1052 single agent) or an arrayref of agentnums.
1053
1054 I<invoice_from>, if specified, overrides the default email invoice From: address.
1055
1056 I<amount>, if specified, only sends the invoice if the total amount owed on this
1057 invoice and all older invoices is greater than the specified amount.
1058
1059 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1060
1061 =cut
1062
1063 sub queueable_send {
1064   my %opt = @_;
1065
1066   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1067     or die "invalid invoice number: " . $opt{invnum};
1068
1069   my @args = ( $opt{template}, $opt{agentnum} );
1070   push @args, $opt{invoice_from}
1071     if exists($opt{invoice_from}) && $opt{invoice_from};
1072
1073   my $error = $self->send( @args );
1074   die $error if $error;
1075
1076 }
1077
1078 sub send {
1079   my $self = shift;
1080
1081   my( $template, $invoice_from, $notice_name );
1082   my $agentnums = '';
1083   my $balance_over = 0;
1084
1085   if ( ref($_[0]) ) {
1086     my $opt = shift;
1087     $template = $opt->{'template'} || '';
1088     if ( $agentnums = $opt->{'agentnum'} ) {
1089       $agentnums = [ $agentnums ] unless ref($agentnums);
1090     }
1091     $invoice_from = $opt->{'invoice_from'};
1092     $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1093     $notice_name = $opt->{'notice_name'};
1094   } else {
1095     $template = scalar(@_) ? shift : '';
1096     if ( scalar(@_) && $_[0]  ) {
1097       $agentnums = ref($_[0]) ? shift : [ shift ];
1098     }
1099     $invoice_from = shift if scalar(@_);
1100     $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1101   }
1102
1103   return 'N/A' unless ! $agentnums
1104                    or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1105
1106   return ''
1107     unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1108
1109   $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
1110                     $conf->config('invoice_from', $self->cust_main->agentnum );
1111
1112   my %opt = (
1113     'template'     => $template,
1114     'invoice_from' => $invoice_from,
1115     'notice_name'  => ( $notice_name || 'Invoice' ),
1116   );
1117
1118   my @invoicing_list = $self->cust_main->invoicing_list;
1119
1120   #$self->email_invoice(\%opt)
1121   $self->email(\%opt)
1122     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1123
1124   #$self->print_invoice(\%opt)
1125   $self->print(\%opt)
1126     if grep { $_ eq 'POST' } @invoicing_list; #postal
1127
1128   $self->fax_invoice(\%opt)
1129     if grep { $_ eq 'FAX' } @invoicing_list; #fax
1130
1131   '';
1132
1133 }
1134
1135 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ] 
1136
1137 Emails this invoice.
1138
1139 Options can be passed as a hashref (recommended) or as a list of up to 
1140 two values for templatename and invoice_from.
1141
1142 I<template>, if specified, is the name of a suffix for alternate invoices.
1143
1144 I<invoice_from>, if specified, overrides the default email invoice From: address.
1145
1146 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1147
1148 =cut
1149
1150 sub queueable_email {
1151   my %opt = @_;
1152
1153   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1154     or die "invalid invoice number: " . $opt{invnum};
1155
1156   my @args = ( $opt{template} );
1157   push @args, $opt{invoice_from}
1158     if exists($opt{invoice_from}) && $opt{invoice_from};
1159
1160   my $error = $self->email( @args );
1161   die $error if $error;
1162
1163 }
1164
1165 #sub email_invoice {
1166 sub email {
1167   my $self = shift;
1168
1169   my( $template, $invoice_from, $notice_name );
1170   if ( ref($_[0]) ) {
1171     my $opt = shift;
1172     $template = $opt->{'template'} || '';
1173     $invoice_from = $opt->{'invoice_from'};
1174     $notice_name = $opt->{'notice_name'} || 'Invoice';
1175   } else {
1176     $template = scalar(@_) ? shift : '';
1177     $invoice_from = shift if scalar(@_);
1178     $notice_name = 'Invoice';
1179   }
1180
1181   $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
1182                     $conf->config('invoice_from', $self->cust_main->agentnum );
1183
1184   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
1185                             $self->cust_main->invoicing_list;
1186
1187   #better to notify this person than silence
1188   @invoicing_list = ($invoice_from) unless @invoicing_list;
1189
1190   my $subject = $self->email_subject($template);
1191
1192   my $error = send_email(
1193     $self->generate_email(
1194       'from'        => $invoice_from,
1195       'to'          => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1196       'subject'     => $subject,
1197       'template'    => $template,
1198       'notice_name' => $notice_name,
1199     )
1200   );
1201   die "can't email invoice: $error\n" if $error;
1202   #die "$error\n" if $error;
1203
1204 }
1205
1206 sub email_subject {
1207   my $self = shift;
1208
1209   #my $template = scalar(@_) ? shift : '';
1210   #per-template?
1211
1212   my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1213                 || 'Invoice';
1214
1215   my $cust_main = $self->cust_main;
1216   my $name = $cust_main->name;
1217   my $name_short = $cust_main->name_short;
1218   my $invoice_number = $self->invnum;
1219   my $invoice_date = $self->_date_pretty;
1220
1221   eval qq("$subject");
1222 }
1223
1224 =item lpr_data HASHREF | [ TEMPLATE ]
1225
1226 Returns the postscript or plaintext for this invoice as an arrayref.
1227
1228 Options can be passed as a hashref (recommended) or as a single optional value
1229 for template.
1230
1231 I<template>, if specified, is the name of a suffix for alternate invoices.
1232
1233 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1234
1235 =cut
1236
1237 sub lpr_data {
1238   my $self = shift;
1239   my( $template, $notice_name );
1240   if ( ref($_[0]) ) {
1241     my $opt = shift;
1242     $template = $opt->{'template'} || '';
1243     $notice_name = $opt->{'notice_name'} || 'Invoice';
1244   } else {
1245     $template = scalar(@_) ? shift : '';
1246     $notice_name = 'Invoice';
1247   }
1248
1249   my %opt = (
1250     'template'    => $template,
1251     'notice_name' => $notice_name,
1252   );
1253
1254   my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1255   [ $self->$method( \%opt ) ];
1256 }
1257
1258 =item print HASHREF | [ TEMPLATE ]
1259
1260 Prints this invoice.
1261
1262 Options can be passed as a hashref (recommended) or as a single optional
1263 value for template.
1264
1265 I<template>, if specified, is the name of a suffix for alternate invoices.
1266
1267 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1268
1269 =cut
1270
1271 #sub print_invoice {
1272 sub print {
1273   my $self = shift;
1274   my( $template, $notice_name );
1275   if ( ref($_[0]) ) {
1276     my $opt = shift;
1277     $template = $opt->{'template'} || '';
1278     $notice_name = $opt->{'notice_name'} || 'Invoice';
1279   } else {
1280     $template = scalar(@_) ? shift : '';
1281     $notice_name = 'Invoice';
1282   }
1283
1284   my %opt = (
1285     'template'    => $template,
1286     'notice_name' => $notice_name,
1287   );
1288
1289   do_print $self->lpr_data(\%opt);
1290 }
1291
1292 =item fax_invoice HASHREF | [ TEMPLATE ] 
1293
1294 Faxes this invoice.
1295
1296 Options can be passed as a hashref (recommended) or as a single optional
1297 value for template.
1298
1299 I<template>, if specified, is the name of a suffix for alternate invoices.
1300
1301 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1302
1303 =cut
1304
1305 sub fax_invoice {
1306   my $self = shift;
1307   my( $template, $notice_name );
1308   if ( ref($_[0]) ) {
1309     my $opt = shift;
1310     $template = $opt->{'template'} || '';
1311     $notice_name = $opt->{'notice_name'} || 'Invoice';
1312   } else {
1313     $template = scalar(@_) ? shift : '';
1314     $notice_name = 'Invoice';
1315   }
1316
1317   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1318     unless $conf->exists('invoice_latex');
1319
1320   my $dialstring = $self->cust_main->getfield('fax');
1321   #Check $dialstring?
1322
1323   my %opt = (
1324     'template'    => $template,
1325     'notice_name' => $notice_name,
1326   );
1327
1328   my $error = send_fax( 'docdata'    => $self->lpr_data(\%opt),
1329                         'dialstring' => $dialstring,
1330                       );
1331   die $error if $error;
1332
1333 }
1334
1335 =item ftp_invoice [ TEMPLATENAME ] 
1336
1337 Sends this invoice data via FTP.
1338
1339 TEMPLATENAME is unused?
1340
1341 =cut
1342
1343 sub ftp_invoice {
1344   my $self = shift;
1345   my $template = scalar(@_) ? shift : '';
1346
1347   $self->send_csv(
1348     'protocol'   => 'ftp',
1349     'server'     => $conf->config('cust_bill-ftpserver'),
1350     'username'   => $conf->config('cust_bill-ftpusername'),
1351     'password'   => $conf->config('cust_bill-ftppassword'),
1352     'dir'        => $conf->config('cust_bill-ftpdir'),
1353     'format'     => $conf->config('cust_bill-ftpformat'),
1354   );
1355 }
1356
1357 =item spool_invoice [ TEMPLATENAME ] 
1358
1359 Spools this invoice data (see L<FS::spool_csv>)
1360
1361 TEMPLATENAME is unused?
1362
1363 =cut
1364
1365 sub spool_invoice {
1366   my $self = shift;
1367   my $template = scalar(@_) ? shift : '';
1368
1369   $self->spool_csv(
1370     'format'       => $conf->config('cust_bill-spoolformat'),
1371     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1372   );
1373 }
1374
1375 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1376
1377 Like B<send>, but only sends the invoice if it is the newest open invoice for
1378 this customer.
1379
1380 =cut
1381
1382 sub send_if_newest {
1383   my $self = shift;
1384
1385   return ''
1386     if scalar(
1387                grep { $_->owed > 0 } 
1388                     qsearch('cust_bill', {
1389                       'custnum' => $self->custnum,
1390                       #'_date'   => { op=>'>', value=>$self->_date },
1391                       'invnum'  => { op=>'>', value=>$self->invnum },
1392                     } )
1393              );
1394     
1395   $self->send(@_);
1396 }
1397
1398 =item send_csv OPTION => VALUE, ...
1399
1400 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1401
1402 Options are:
1403
1404 protocol - currently only "ftp"
1405 server
1406 username
1407 password
1408 dir
1409
1410 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1411 and YYMMDDHHMMSS is a timestamp.
1412
1413 See L</print_csv> for a description of the output format.
1414
1415 =cut
1416
1417 sub send_csv {
1418   my($self, %opt) = @_;
1419
1420   #create file(s)
1421
1422   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1423   mkdir $spooldir, 0700 unless -d $spooldir;
1424
1425   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1426   my $file = "$spooldir/$tracctnum.csv";
1427   
1428   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1429
1430   open(CSV, ">$file") or die "can't open $file: $!";
1431   print CSV $header;
1432
1433   print CSV $detail;
1434
1435   close CSV;
1436
1437   my $net;
1438   if ( $opt{protocol} eq 'ftp' ) {
1439     eval "use Net::FTP;";
1440     die $@ if $@;
1441     $net = Net::FTP->new($opt{server}) or die @$;
1442   } else {
1443     die "unknown protocol: $opt{protocol}";
1444   }
1445
1446   $net->login( $opt{username}, $opt{password} )
1447     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1448
1449   $net->binary or die "can't set binary mode";
1450
1451   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1452
1453   $net->put($file) or die "can't put $file: $!";
1454
1455   $net->quit;
1456
1457   unlink $file;
1458
1459 }
1460
1461 =item spool_csv
1462
1463 Spools CSV invoice data.
1464
1465 Options are:
1466
1467 =over 4
1468
1469 =item format - 'default' or 'billco'
1470
1471 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
1472
1473 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1474
1475 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
1476
1477 =back
1478
1479 =cut
1480
1481 sub spool_csv {
1482   my($self, %opt) = @_;
1483
1484   my $cust_main = $self->cust_main;
1485
1486   if ( $opt{'dest'} ) {
1487     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1488                              $cust_main->invoicing_list;
1489     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1490                      || ! keys %invoicing_list;
1491   }
1492
1493   if ( $opt{'balanceover'} ) {
1494     return 'N/A'
1495       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1496   }
1497
1498   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1499   mkdir $spooldir, 0700 unless -d $spooldir;
1500
1501   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1502
1503   my $file =
1504     "$spooldir/".
1505     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1506     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1507     '.csv';
1508   
1509   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1510
1511   open(CSV, ">>$file") or die "can't open $file: $!";
1512   flock(CSV, LOCK_EX);
1513   seek(CSV, 0, 2);
1514
1515   print CSV $header;
1516
1517   if ( lc($opt{'format'}) eq 'billco' ) {
1518
1519     flock(CSV, LOCK_UN);
1520     close CSV;
1521
1522     $file =
1523       "$spooldir/".
1524       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1525       '-detail.csv';
1526
1527     open(CSV,">>$file") or die "can't open $file: $!";
1528     flock(CSV, LOCK_EX);
1529     seek(CSV, 0, 2);
1530   }
1531
1532   print CSV $detail;
1533
1534   flock(CSV, LOCK_UN);
1535   close CSV;
1536
1537   return '';
1538
1539 }
1540
1541 =item print_csv OPTION => VALUE, ...
1542
1543 Returns CSV data for this invoice.
1544
1545 Options are:
1546
1547 format - 'default' or 'billco'
1548
1549 Returns a list consisting of two scalars.  The first is a single line of CSV
1550 header information for this invoice.  The second is one or more lines of CSV
1551 detail information for this invoice.
1552
1553 If I<format> is not specified or "default", the fields of the CSV file are as
1554 follows:
1555
1556 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1557
1558 =over 4
1559
1560 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1561
1562 B<record_type> is C<cust_bill> for the initial header line only.  The
1563 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1564 fields are filled in.
1565
1566 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1567 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1568 are filled in.
1569
1570 =item invnum - invoice number
1571
1572 =item custnum - customer number
1573
1574 =item _date - invoice date
1575
1576 =item charged - total invoice amount
1577
1578 =item first - customer first name
1579
1580 =item last - customer first name
1581
1582 =item company - company name
1583
1584 =item address1 - address line 1
1585
1586 =item address2 - address line 1
1587
1588 =item city
1589
1590 =item state
1591
1592 =item zip
1593
1594 =item country
1595
1596 =item pkg - line item description
1597
1598 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1599
1600 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1601
1602 =item sdate - start date for recurring fee
1603
1604 =item edate - end date for recurring fee
1605
1606 =back
1607
1608 If I<format> is "billco", the fields of the header CSV file are as follows:
1609
1610   +-------------------------------------------------------------------+
1611   |                        FORMAT HEADER FILE                         |
1612   |-------------------------------------------------------------------|
1613   | Field | Description                   | Name       | Type | Width |
1614   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1615   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1616   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1617   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1618   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1619   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1620   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1621   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1622   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1623   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1624   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1625   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1626   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1627   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1628   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1629   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1630   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1631   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1632   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1633   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1634   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1635   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1636   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1637   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1638   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1639   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1640   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1641   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1642   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1643   +-------+-------------------------------+------------+------+-------+
1644
1645 If I<format> is "billco", the fields of the detail CSV file are as follows:
1646
1647                                   FORMAT FOR DETAIL FILE
1648         |                            |           |      |
1649   Field | Description                | Name      | Type | Width
1650   1     | N/A-Leave Empty            | RC        | CHAR |     2
1651   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1652   3     | Account Number             | TRACCTNUM | CHAR |    15
1653   4     | Invoice Number             | TRINVOICE | CHAR |    15
1654   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1655   6     | Transaction Detail         | DETAILS   | CHAR |   100
1656   7     | Amount                     | AMT       | NUM* |     9
1657   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1658   9     | Grouping Code              | GROUP     | CHAR |     2
1659   10    | User Defined               | ACCT CODE | CHAR |    15
1660
1661 =cut
1662
1663 sub print_csv {
1664   my($self, %opt) = @_;
1665   
1666   eval "use Text::CSV_XS";
1667   die $@ if $@;
1668
1669   my $cust_main = $self->cust_main;
1670
1671   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1672
1673   if ( lc($opt{'format'}) eq 'billco' ) {
1674
1675     my $taxtotal = 0;
1676     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1677
1678     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1679
1680     my( $previous_balance, @unused ) = $self->previous; #previous balance
1681
1682     my $pmt_cr_applied = 0;
1683     $pmt_cr_applied += $_->{'amount'}
1684       foreach ( $self->_items_payments, $self->_items_credits ) ;
1685
1686     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1687
1688     $csv->combine(
1689       '',                         #  1 | N/A-Leave Empty               CHAR   2
1690       '',                         #  2 | N/A-Leave Empty               CHAR  15
1691       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1692       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1693       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1694       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1695       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1696       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1697       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1698       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1699       '',                         # 10 | Ancillary Billing Information CHAR  30
1700       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1701       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1702
1703       # XXX ?
1704       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1705
1706       # XXX ?
1707       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1708
1709       $previous_balance,          # 15 | Previous Balance              NUM*   9
1710       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1711       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1712       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1713       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1714       '',                         # 20 | 30 Day Aging                  NUM*   9
1715       '',                         # 21 | 60 Day Aging                  NUM*   9
1716       '',                         # 22 | 90 Day Aging                  NUM*   9
1717       'N',                        # 23 | Y/N                           CHAR   1
1718       '',                         # 24 | Remittance automation         CHAR 100
1719       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1720       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1721       '0',                        # 27 | Federal Tax***                NUM*   9
1722       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1723       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1724     );
1725
1726   } else {
1727   
1728     $csv->combine(
1729       'cust_bill',
1730       $self->invnum,
1731       $self->custnum,
1732       time2str("%x", $self->_date),
1733       sprintf("%.2f", $self->charged),
1734       ( map { $cust_main->getfield($_) }
1735           qw( first last company address1 address2 city state zip country ) ),
1736       map { '' } (1..5),
1737     ) or die "can't create csv";
1738   }
1739
1740   my $header = $csv->string. "\n";
1741
1742   my $detail = '';
1743   if ( lc($opt{'format'}) eq 'billco' ) {
1744
1745     my $lineseq = 0;
1746     foreach my $item ( $self->_items_pkg ) {
1747
1748       $csv->combine(
1749         '',                     #  1 | N/A-Leave Empty            CHAR   2
1750         '',                     #  2 | N/A-Leave Empty            CHAR  15
1751         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1752         $self->invnum,          #  4 | Invoice Number             CHAR  15
1753         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1754         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1755         $item->{'amount'},      #  7 | Amount                     NUM*   9
1756         '',                     #  8 | Line Format Control**      CHAR   2
1757         '',                     #  9 | Grouping Code              CHAR   2
1758         '',                     # 10 | User Defined               CHAR  15
1759       );
1760
1761       $detail .= $csv->string. "\n";
1762
1763     }
1764
1765   } else {
1766
1767     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1768
1769       my($pkg, $setup, $recur, $sdate, $edate);
1770       if ( $cust_bill_pkg->pkgnum ) {
1771       
1772         ($pkg, $setup, $recur, $sdate, $edate) = (
1773           $cust_bill_pkg->part_pkg->pkg,
1774           ( $cust_bill_pkg->setup != 0
1775             ? sprintf("%.2f", $cust_bill_pkg->setup )
1776             : '' ),
1777           ( $cust_bill_pkg->recur != 0
1778             ? sprintf("%.2f", $cust_bill_pkg->recur )
1779             : '' ),
1780           ( $cust_bill_pkg->sdate 
1781             ? time2str("%x", $cust_bill_pkg->sdate)
1782             : '' ),
1783           ($cust_bill_pkg->edate 
1784             ?time2str("%x", $cust_bill_pkg->edate)
1785             : '' ),
1786         );
1787   
1788       } else { #pkgnum tax
1789         next unless $cust_bill_pkg->setup != 0;
1790         $pkg = $cust_bill_pkg->desc;
1791         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1792         ( $sdate, $edate ) = ( '', '' );
1793       }
1794   
1795       $csv->combine(
1796         'cust_bill_pkg',
1797         $self->invnum,
1798         ( map { '' } (1..11) ),
1799         ($pkg, $setup, $recur, $sdate, $edate)
1800       ) or die "can't create csv";
1801
1802       $detail .= $csv->string. "\n";
1803
1804     }
1805
1806   }
1807
1808   ( $header, $detail );
1809
1810 }
1811
1812 =item comp
1813
1814 Pays this invoice with a compliemntary payment.  If there is an error,
1815 returns the error, otherwise returns false.
1816
1817 =cut
1818
1819 sub comp {
1820   my $self = shift;
1821   my $cust_pay = new FS::cust_pay ( {
1822     'invnum'   => $self->invnum,
1823     'paid'     => $self->owed,
1824     '_date'    => '',
1825     'payby'    => 'COMP',
1826     'payinfo'  => $self->cust_main->payinfo,
1827     'paybatch' => '',
1828   } );
1829   $cust_pay->insert;
1830 }
1831
1832 =item realtime_card
1833
1834 Attempts to pay this invoice with a credit card payment via a
1835 Business::OnlinePayment realtime gateway.  See
1836 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1837 for supported processors.
1838
1839 =cut
1840
1841 sub realtime_card {
1842   my $self = shift;
1843   $self->realtime_bop( 'CC', @_ );
1844 }
1845
1846 =item realtime_ach
1847
1848 Attempts to pay this invoice with an electronic check (ACH) payment via a
1849 Business::OnlinePayment realtime gateway.  See
1850 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1851 for supported processors.
1852
1853 =cut
1854
1855 sub realtime_ach {
1856   my $self = shift;
1857   $self->realtime_bop( 'ECHECK', @_ );
1858 }
1859
1860 =item realtime_lec
1861
1862 Attempts to pay this invoice with phone bill (LEC) payment via a
1863 Business::OnlinePayment realtime gateway.  See
1864 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1865 for supported processors.
1866
1867 =cut
1868
1869 sub realtime_lec {
1870   my $self = shift;
1871   $self->realtime_bop( 'LEC', @_ );
1872 }
1873
1874 sub realtime_bop {
1875   my( $self, $method ) = @_;
1876
1877   my $cust_main = $self->cust_main;
1878   my $balance = $cust_main->balance;
1879   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1880   $amount = sprintf("%.2f", $amount);
1881   return "not run (balance $balance)" unless $amount > 0;
1882
1883   my $description = 'Internet Services';
1884   if ( $conf->exists('business-onlinepayment-description') ) {
1885     my $dtempl = $conf->config('business-onlinepayment-description');
1886
1887     my $agent_obj = $cust_main->agent
1888       or die "can't retreive agent for $cust_main (agentnum ".
1889              $cust_main->agentnum. ")";
1890     my $agent = $agent_obj->agent;
1891     my $pkgs = join(', ',
1892       map { $_->part_pkg->pkg }
1893         grep { $_->pkgnum } $self->cust_bill_pkg
1894     );
1895     $description = eval qq("$dtempl");
1896   }
1897
1898   $cust_main->realtime_bop($method, $amount,
1899     'description' => $description,
1900     'invnum'      => $self->invnum,
1901   );
1902
1903 }
1904
1905 =item batch_card OPTION => VALUE...
1906
1907 Adds a payment for this invoice to the pending credit card batch (see
1908 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1909 runs the payment using a realtime gateway.
1910
1911 =cut
1912
1913 sub batch_card {
1914   my ($self, %options) = @_;
1915   my $cust_main = $self->cust_main;
1916
1917   $options{invnum} = $self->invnum;
1918   
1919   $cust_main->batch_card(%options);
1920 }
1921
1922 sub _agent_template {
1923   my $self = shift;
1924   $self->cust_main->agent_template;
1925 }
1926
1927 sub _agent_invoice_from {
1928   my $self = shift;
1929   $self->cust_main->agent_invoice_from;
1930 }
1931
1932 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1933
1934 Returns an text invoice, as a list of lines.
1935
1936 Options can be passed as a hashref (recommended) or as a list of time, template
1937 and then any key/value pairs for any other options.
1938
1939 I<time>, if specified, is used to control the printing of overdue messages.  The
1940 default is now.  It isn't the date of the invoice; that's the `_date' field.
1941 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1942 L<Time::Local> and L<Date::Parse> for conversion functions.
1943
1944 I<template>, if specified, is the name of a suffix for alternate invoices.
1945
1946 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1947
1948 =cut
1949
1950 sub print_text {
1951   my $self = shift;
1952   my( $today, $template, %opt );
1953   if ( ref($_[0]) ) {
1954     %opt = %{ shift() };
1955     $today = delete($opt{'time'}) || '';
1956     $template = delete($opt{template}) || '';
1957   } else {
1958     ( $today, $template, %opt ) = @_;
1959   }
1960
1961   my %params = ( 'format' => 'template' );
1962   $params{'time'} = $today if $today;
1963   $params{'template'} = $template if $template;
1964   $params{$_} = $opt{$_} 
1965     foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
1966
1967   $self->print_generic( %params );
1968 }
1969
1970 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
1971
1972 Internal method - returns a filename of a filled-in LaTeX template for this
1973 invoice (Note: add ".tex" to get the actual filename), and a filename of
1974 an associated logo (with the .eps extension included).
1975
1976 See print_ps and print_pdf for methods that return PostScript and PDF output.
1977
1978 Options can be passed as a hashref (recommended) or as a list of time, template
1979 and then any key/value pairs for any other options.
1980
1981 I<time>, if specified, is used to control the printing of overdue messages.  The
1982 default is now.  It isn't the date of the invoice; that's the `_date' field.
1983 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1984 L<Time::Local> and L<Date::Parse> for conversion functions.
1985
1986 I<template>, if specified, is the name of a suffix for alternate invoices.
1987
1988 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1989
1990 =cut
1991
1992 sub print_latex {
1993   my $self = shift;
1994   my( $today, $template, %opt );
1995   if ( ref($_[0]) ) {
1996     %opt = %{ shift() };
1997     $today = delete($opt{'time'}) || '';
1998     $template = delete($opt{template}) || '';
1999   } else {
2000     ( $today, $template, %opt ) = @_;
2001   }
2002
2003   my %params = ( 'format' => 'latex' );
2004   $params{'time'} = $today if $today;
2005   $params{'template'} = $template if $template;
2006   $params{$_} = $opt{$_} 
2007     foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2008
2009   $template ||= $self->_agent_template;
2010
2011   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2012   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2013                            DIR      => $dir,
2014                            SUFFIX   => '.eps',
2015                            UNLINK   => 0,
2016                          ) or die "can't open temp file: $!\n";
2017
2018   my $agentnum = $self->cust_main->agentnum;
2019
2020   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2021     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2022       or die "can't write temp file: $!\n";
2023   } else {
2024     print $lh $conf->config_binary('logo.eps', $agentnum)
2025       or die "can't write temp file: $!\n";
2026   }
2027   close $lh;
2028   $params{'logo_file'} = $lh->filename;
2029
2030   my @filled_in = $self->print_generic( %params );
2031   
2032   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2033                            DIR      => $dir,
2034                            SUFFIX   => '.tex',
2035                            UNLINK   => 0,
2036                          ) or die "can't open temp file: $!\n";
2037   print $fh join('', @filled_in );
2038   close $fh;
2039
2040   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2041   return ($1, $params{'logo_file'});
2042
2043 }
2044
2045 =item print_generic OPTION => VALUE ...
2046
2047 Internal method - returns a filled-in template for this invoice as a scalar.
2048
2049 See print_ps and print_pdf for methods that return PostScript and PDF output.
2050
2051 Non optional options include 
2052   format - latex, html, template
2053
2054 Optional options include
2055
2056 template - a value used as a suffix for a configuration template
2057
2058 time - a value used to control the printing of overdue messages.  The
2059 default is now.  It isn't the date of the invoice; that's the `_date' field.
2060 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2061 L<Time::Local> and L<Date::Parse> for conversion functions.
2062
2063 cid - 
2064
2065 unsquelch_cdr - overrides any per customer cdr squelching when true
2066
2067 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2068
2069 =cut
2070
2071 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
2072 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2073 # yes: fixed width (dot matrix) text printing will be borked
2074 sub print_generic {
2075
2076   my( $self, %params ) = @_;
2077   my $today = $params{today} ? $params{today} : time;
2078   warn "$me print_generic called on $self with suffix $params{template}\n"
2079     if $DEBUG;
2080
2081   my $format = $params{format};
2082   die "Unknown format: $format"
2083     unless $format =~ /^(latex|html|template)$/;
2084
2085   my $cust_main = $self->cust_main;
2086   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2087     unless $cust_main->payname
2088         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2089
2090   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
2091                      'html'     => [ '<%=', '%>' ],
2092                      'template' => [ '{', '}' ],
2093                    );
2094
2095   #create the template
2096   my $template = $params{template} ? $params{template} : $self->_agent_template;
2097   my $templatefile = "invoice_$format";
2098   $templatefile .= "_$template"
2099     if length($template);
2100   my @invoice_template = map "$_\n", $conf->config($templatefile)
2101     or die "cannot load config data $templatefile";
2102
2103   my $old_latex = '';
2104   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2105     #change this to a die when the old code is removed
2106     warn "old-style invoice template $templatefile; ".
2107          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2108     $old_latex = 'true';
2109     @invoice_template = _translate_old_latex_format(@invoice_template);
2110   } 
2111
2112   my $text_template = new Text::Template(
2113     TYPE => 'ARRAY',
2114     SOURCE => \@invoice_template,
2115     DELIMITERS => $delimiters{$format},
2116   );
2117
2118   $text_template->compile()
2119     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2120
2121
2122   # additional substitution could possibly cause breakage in existing templates
2123   my %convert_maps = ( 
2124     'latex' => {
2125                  'notes'         => sub { map "$_", @_ },
2126                  'footer'        => sub { map "$_", @_ },
2127                  'smallfooter'   => sub { map "$_", @_ },
2128                  'returnaddress' => sub { map "$_", @_ },
2129                  'coupon'        => sub { map "$_", @_ },
2130                  'summary'       => sub { map "$_", @_ },
2131                },
2132     'html'  => {
2133                  'notes' =>
2134                    sub {
2135                      map { 
2136                        s/%%(.*)$/<!-- $1 -->/g;
2137                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2138                        s/\\begin\{enumerate\}/<ol>/g;
2139                        s/\\item /  <li>/g;
2140                        s/\\end\{enumerate\}/<\/ol>/g;
2141                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2142                        s/\\\\\*/<br>/g;
2143                        s/\\dollar ?/\$/g;
2144                        s/\\#/#/g;
2145                        s/~/&nbsp;/g;
2146                        $_;
2147                      }  @_
2148                    },
2149                  'footer' =>
2150                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2151                  'smallfooter' =>
2152                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2153                  'returnaddress' =>
2154                    sub {
2155                      map { 
2156                        s/~/&nbsp;/g;
2157                        s/\\\\\*?\s*$/<BR>/;
2158                        s/\\hyphenation\{[\w\s\-]+}//;
2159                        s/\\([&])/$1/g;
2160                        $_;
2161                      }  @_
2162                    },
2163                  'coupon'        => sub { "" },
2164                  'summary'       => sub { "" },
2165                },
2166     'template' => {
2167                  'notes' =>
2168                    sub {
2169                      map { 
2170                        s/%%.*$//g;
2171                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2172                        s/\\begin\{enumerate\}//g;
2173                        s/\\item /  * /g;
2174                        s/\\end\{enumerate\}//g;
2175                        s/\\textbf\{(.*)\}/$1/g;
2176                        s/\\\\\*/ /;
2177                        s/\\dollar ?/\$/g;
2178                        $_;
2179                      }  @_
2180                    },
2181                  'footer' =>
2182                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2183                  'smallfooter' =>
2184                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2185                  'returnaddress' =>
2186                    sub {
2187                      map { 
2188                        s/~/ /g;
2189                        s/\\\\\*?\s*$/\n/;             # dubious
2190                        s/\\hyphenation\{[\w\s\-]+}//;
2191                        $_;
2192                      }  @_
2193                    },
2194                  'coupon'        => sub { "" },
2195                  'summary'       => sub { "" },
2196                },
2197   );
2198
2199
2200   # hashes for differing output formats
2201   my %nbsps = ( 'latex'    => '~',
2202                 'html'     => '',    # '&nbps;' would be nice
2203                 'template' => '',    # not used
2204               );
2205   my $nbsp = $nbsps{$format};
2206
2207   my %escape_functions = ( 'latex'    => \&_latex_escape,
2208                            'html'     => \&encode_entities,
2209                            'template' => sub { shift },
2210                          );
2211   my $escape_function = $escape_functions{$format};
2212
2213   my %date_formats = ( 'latex'    => '%b %o, %Y',
2214                        'html'     => '%b&nbsp;%o,&nbsp;%Y',
2215                        'template' => '%s',
2216                      );
2217   my $date_format = $date_formats{$format};
2218
2219   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
2220                                                },
2221                              'html'     => sub { return '<b>'. shift(). '</b>'
2222                                                },
2223                              'template' => sub { shift },
2224                            );
2225   my $embolden_function = $embolden_functions{$format};
2226
2227
2228   # generate template variables
2229   my $returnaddress;
2230   if (
2231          defined( $conf->config_orbase( "invoice_${format}returnaddress",
2232                                         $template
2233                                       )
2234                 )
2235        && length( $conf->config_orbase( "invoice_${format}returnaddress",
2236                                         $template
2237                                       )
2238                 )
2239   ) {
2240
2241     $returnaddress = join("\n",
2242       $conf->config_orbase("invoice_${format}returnaddress", $template)
2243     );
2244
2245   } elsif ( grep /\S/,
2246             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2247
2248     my $convert_map = $convert_maps{$format}{'returnaddress'};
2249     $returnaddress =
2250       join( "\n",
2251             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2252                                                  $template
2253                                                )
2254                          )
2255           );
2256   } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2257
2258     my $convert_map = $convert_maps{$format}{'returnaddress'};
2259     $returnaddress = join( "\n", &$convert_map(
2260                                    map { s/( {2,})/'~' x length($1)/eg;
2261                                          s/$/\\\\\*/;
2262                                          $_
2263                                        }
2264                                      ( $conf->config('company_name', $self->cust_main->agentnum),
2265                                        $conf->config('company_address', $self->cust_main->agentnum),
2266                                      )
2267                                  )
2268                      );
2269
2270   } else {
2271
2272     my $warning = "Couldn't find a return address; ".
2273                   "do you need to set the company_address configuration value?";
2274     warn "$warning\n";
2275     $returnaddress = $nbsp;
2276     #$returnaddress = $warning;
2277
2278   }
2279
2280   my %invoice_data = (
2281
2282     #invoice from info
2283     'company_name'    => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2284     'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2285     'returnaddress'   => $returnaddress,
2286     'agent'           => &$escape_function($cust_main->agent->agent),
2287
2288     #invoice info
2289     'invnum'          => $self->invnum,
2290     'date'            => time2str($date_format, $self->_date),
2291     'today'           => time2str('%b %o, %Y', $today),
2292     'terms'           => $self->terms,
2293     'template'        => $template, #params{'template'},
2294     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
2295     'current_charges' => sprintf("%.2f", $self->charged),
2296     'duedate'         => $self->due_date2str('%m/%d/%Y'), #date_format?
2297
2298     #customer info
2299     'custnum'         => $cust_main->display_custnum,
2300     'agent_custid'    => &$escape_function($cust_main->agent_custid),
2301     ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2302       payname company address1 address2 city state zip fax
2303     )),
2304
2305     #global config
2306     'ship_enable'     => $conf->exists('invoice-ship_address'),
2307     'unitprices'      => $conf->exists('invoice-unitprice'),
2308     'smallernotes'    => $conf->exists('invoice-smallernotes'),
2309     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
2310    
2311     # better hang on to conf_dir for a while (for old templates)
2312     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2313
2314     #these are only used when doing paged plaintext
2315     'page'            => 1,
2316     'total_pages'     => 1,
2317
2318   );
2319
2320   $invoice_data{finance_section} = '';
2321   if ( $conf->config('finance_pkgclass') ) {
2322     my $pkg_class =
2323       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2324     $invoice_data{finance_section} = $pkg_class->categoryname;
2325   } 
2326  $invoice_data{finance_amount} = '0.00';
2327
2328   my $countrydefault = $conf->config('countrydefault') || 'US';
2329   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2330   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2331     my $method = $prefix.$_;
2332     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2333   }
2334   $invoice_data{'ship_country'} = ''
2335     if ( $invoice_data{'ship_country'} eq $countrydefault );
2336   
2337   $invoice_data{'cid'} = $params{'cid'}
2338     if $params{'cid'};
2339
2340   if ( $cust_main->country eq $countrydefault ) {
2341     $invoice_data{'country'} = '';
2342   } else {
2343     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2344   }
2345
2346   my @address = ();
2347   $invoice_data{'address'} = \@address;
2348   push @address,
2349     $cust_main->payname.
2350       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2351         ? " (P.O. #". $cust_main->payinfo. ")"
2352         : ''
2353       )
2354   ;
2355   push @address, $cust_main->company
2356     if $cust_main->company;
2357   push @address, $cust_main->address1;
2358   push @address, $cust_main->address2
2359     if $cust_main->address2;
2360   push @address,
2361     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
2362   push @address, $invoice_data{'country'}
2363     if $invoice_data{'country'};
2364   push @address, ''
2365     while (scalar(@address) < 5);
2366
2367   $invoice_data{'logo_file'} = $params{'logo_file'}
2368     if $params{'logo_file'};
2369
2370   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2371 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2372   #my $balance_due = $self->owed + $pr_total - $cr_total;
2373   my $balance_due = $self->owed + $pr_total;
2374   $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2375   $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2376   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2377   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2378
2379   my $agentnum = $self->cust_main->agentnum;
2380
2381   my $summarypage = '';
2382   if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2383     $summarypage = 1;
2384   }
2385   $invoice_data{'summarypage'} = $summarypage;
2386
2387   #do variable substitution in notes, footer, smallfooter
2388   foreach my $include (qw( notes footer smallfooter coupon )) {
2389
2390     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2391     my @inc_src;
2392
2393     if ( $conf->exists($inc_file, $agentnum)
2394          && length( $conf->config($inc_file, $agentnum) ) ) {
2395
2396       @inc_src = $conf->config($inc_file, $agentnum);
2397
2398     } else {
2399
2400       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2401
2402       my $convert_map = $convert_maps{$format}{$include};
2403
2404       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2405                        s/--\@\]/$delimiters{$format}[1]/g;
2406                        $_;
2407                      } 
2408                  &$convert_map( $conf->config($inc_file, $agentnum) );
2409
2410     }
2411
2412     my $inc_tt = new Text::Template (
2413       TYPE       => 'ARRAY',
2414       SOURCE     => [ map "$_\n", @inc_src ],
2415       DELIMITERS => $delimiters{$format},
2416     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2417
2418     unless ( $inc_tt->compile() ) {
2419       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2420       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2421       die $error;
2422     }
2423
2424     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2425
2426     $invoice_data{$include} =~ s/\n+$//
2427       if ($format eq 'latex');
2428   }
2429
2430   $invoice_data{'po_line'} =
2431     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2432       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2433       : $nbsp;
2434
2435   my %money_chars = ( 'latex'    => '',
2436                       'html'     => $conf->config('money_char') || '$',
2437                       'template' => '',
2438                     );
2439   my $money_char = $money_chars{$format};
2440
2441   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2442                             'html'     => $conf->config('money_char') || '$',
2443                             'template' => '',
2444                           );
2445   my $other_money_char = $other_money_chars{$format};
2446   $invoice_data{'dollar'} = $other_money_char;
2447
2448   my @detail_items = ();
2449   my @total_items = ();
2450   my @buf = ();
2451   my @sections = ();
2452
2453   $invoice_data{'detail_items'} = \@detail_items;
2454   $invoice_data{'total_items'} = \@total_items;
2455   $invoice_data{'buf'} = \@buf;
2456   $invoice_data{'sections'} = \@sections;
2457
2458   my $previous_section = { 'description' => 'Previous Charges',
2459                            'subtotal'    => $other_money_char.
2460                                             sprintf('%.2f', $pr_total),
2461                            'summarized'  => $summarypage ? 'Y' : '',
2462                          };
2463   $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '. 
2464     join(' / ', map { $cust_main->balance_date_range(@$_) }
2465                 $self->_prior_month30s
2466         )
2467     if $conf->exists('invoice_include_aging');
2468
2469   my $taxtotal = 0;
2470   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2471                       'subtotal'    => $taxtotal,   # adjusted below
2472                       'summarized'  => $summarypage ? 'Y' : '',
2473                     };
2474   my $tax_weight = _pkg_category($tax_section->{description})
2475                         ? _pkg_category($tax_section->{description})->weight
2476                         : 0;
2477   $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2478   $tax_section->{'sort_weight'} = $tax_weight;
2479
2480
2481   my $adjusttotal = 0;
2482   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2483                          'subtotal'    => 0,   # adjusted below
2484                          'summarized'  => $summarypage ? 'Y' : '',
2485                        };
2486   my $adjust_weight = _pkg_category($adjust_section->{description})
2487                         ? _pkg_category($adjust_section->{description})->weight
2488                         : 0;
2489   $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2490   $adjust_section->{'sort_weight'} = $adjust_weight;
2491
2492   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2493   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2494   my $late_sections = [];
2495   my $extra_sections = [];
2496   my $extra_lines = ();
2497   if ( $multisection ) {
2498     ($extra_sections, $extra_lines) =
2499       $self->_items_extra_usage_sections($escape_function, $format)
2500       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2501
2502     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2503
2504     push @detail_items, @$extra_lines if $extra_lines;
2505     push @sections,
2506       $self->_items_sections( $late_sections,      # this could stand a refactor
2507                               $summarypage,
2508                               $escape_function,
2509                               $extra_sections,
2510                               $format,             #bah
2511                             );
2512     if ($conf->exists('svc_phone_sections')) {
2513       my ($phone_sections, $phone_lines) =
2514         $self->_items_svc_phone_sections($escape_function, $format);
2515       push @{$late_sections}, @$phone_sections;
2516       push @detail_items, @$phone_lines;
2517     }
2518   }else{
2519     push @sections, { 'description' => '', 'subtotal' => '' };
2520   }
2521
2522   unless (    $conf->exists('disable_previous_balance')
2523            || $conf->exists('previous_balance-summary_only')
2524          )
2525   {
2526
2527     foreach my $line_item ( $self->_items_previous ) {
2528
2529       my $detail = {
2530         ext_description => [],
2531       };
2532       $detail->{'ref'} = $line_item->{'pkgnum'};
2533       $detail->{'quantity'} = 1;
2534       $detail->{'section'} = $previous_section;
2535       $detail->{'description'} = &$escape_function($line_item->{'description'});
2536       if ( exists $line_item->{'ext_description'} ) {
2537         @{$detail->{'ext_description'}} = map {
2538           &$escape_function($_);
2539         } @{$line_item->{'ext_description'}};
2540       }
2541       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2542                             $line_item->{'amount'};
2543       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2544
2545       push @detail_items, $detail;
2546       push @buf, [ $detail->{'description'},
2547                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2548                  ];
2549     }
2550
2551   }
2552
2553   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2554     push @buf, ['','-----------'];
2555     push @buf, [ 'Total Previous Balance',
2556                  $money_char. sprintf("%10.2f", $pr_total) ];
2557     push @buf, ['',''];
2558   }
2559
2560   foreach my $section (@sections, @$late_sections) {
2561
2562     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2563       if ( $invoice_data{finance_section} &&
2564            $section->{'description'} eq $invoice_data{finance_section} );
2565
2566     $section->{'subtotal'} = $other_money_char.
2567                              sprintf('%.2f', $section->{'subtotal'})
2568       if $multisection;
2569
2570     # begin some normalization
2571     $section->{'amount'}   = $section->{'subtotal'}
2572       if $multisection;
2573
2574
2575     if ( $section->{'description'} ) {
2576       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2577                    [ '', '' ],
2578                  );
2579     }
2580
2581     my $multilocation = scalar($cust_main->cust_location); #too expensive?
2582     my %options = ();
2583     $options{'section'} = $section if $multisection;
2584     $options{'format'} = $format;
2585     $options{'escape_function'} = $escape_function;
2586     $options{'format_function'} = sub { () } unless $unsquelched;
2587     $options{'unsquelched'} = $unsquelched;
2588     $options{'summary_page'} = $summarypage;
2589     $options{'skip_usage'} =
2590       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2591     $options{'multilocation'} = $multilocation;
2592
2593     foreach my $line_item ( $self->_items_pkg(%options) ) {
2594       my $detail = {
2595         ext_description => [],
2596       };
2597       $detail->{'ref'} = $line_item->{'pkgnum'};
2598       $detail->{'quantity'} = $line_item->{'quantity'};
2599       $detail->{'section'} = $section;
2600       $detail->{'description'} = &$escape_function($line_item->{'description'});
2601       if ( exists $line_item->{'ext_description'} ) {
2602         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2603       }
2604       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2605                               $line_item->{'amount'};
2606       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2607                                  $line_item->{'unit_amount'};
2608       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2609   
2610       push @detail_items, $detail;
2611       push @buf, ( [ $detail->{'description'},
2612                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2613                    ],
2614                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2615                  );
2616     }
2617
2618     if ( $section->{'description'} ) {
2619       push @buf, ( ['','-----------'],
2620                    [ $section->{'description'}. ' sub-total',
2621                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2622                    ],
2623                    [ '', '' ],
2624                    [ '', '' ],
2625                  );
2626     }
2627   
2628   }
2629   
2630   $invoice_data{current_less_finance} =
2631     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2632
2633   if ( $multisection && !$conf->exists('disable_previous_balance')
2634     || $conf->exists('previous_balance-summary_only') )
2635   {
2636     unshift @sections, $previous_section if $pr_total;
2637   }
2638
2639   foreach my $tax ( $self->_items_tax ) {
2640
2641     $taxtotal += $tax->{'amount'};
2642
2643     my $description = &$escape_function( $tax->{'description'} );
2644     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2645
2646     if ( $multisection ) {
2647
2648       my $money = $old_latex ? '' : $money_char;
2649       push @detail_items, {
2650         ext_description => [],
2651         ref          => '',
2652         quantity     => '',
2653         description  => $description,
2654         amount       => $money. $amount,
2655         product_code => '',
2656         section      => $tax_section,
2657       };
2658
2659     } else {
2660
2661       push @total_items, {
2662         'total_item'   => $description,
2663         'total_amount' => $other_money_char. $amount,
2664       };
2665
2666     }
2667
2668     push @buf,[ $description,
2669                 $money_char. $amount,
2670               ];
2671
2672   }
2673   
2674   if ( $taxtotal ) {
2675     my $total = {};
2676     $total->{'total_item'} = 'Sub-total';
2677     $total->{'total_amount'} =
2678       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2679
2680     if ( $multisection ) {
2681       $tax_section->{'subtotal'} = $other_money_char.
2682                                    sprintf('%.2f', $taxtotal);
2683       $tax_section->{'pretotal'} = 'New charges sub-total '.
2684                                    $total->{'total_amount'};
2685       push @sections, $tax_section if $taxtotal;
2686     }else{
2687       unshift @total_items, $total;
2688     }
2689   }
2690   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2691
2692   push @buf,['','-----------'];
2693   push @buf,[( $conf->exists('disable_previous_balance') 
2694                ? 'Total Charges'
2695                : 'Total New Charges'
2696              ),
2697              $money_char. sprintf("%10.2f",$self->charged) ];
2698   push @buf,['',''];
2699
2700   {
2701     my $total = {};
2702     $total->{'total_item'} = &$embolden_function('Total');
2703     $total->{'total_amount'} =
2704       &$embolden_function(
2705         $other_money_char.
2706         sprintf( '%.2f',
2707                  $self->charged + ( $conf->exists('disable_previous_balance')
2708                                     ? 0
2709                                     : $pr_total
2710                                   )
2711                )
2712       );
2713     if ( $multisection ) {
2714       if ( $adjust_section->{'sort_weight'} ) {
2715         $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2716           sprintf("%.2f", ($self->billing_balance || 0) );
2717       } else {
2718         $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2719                                         sprintf('%.2f', $self->charged );
2720       } 
2721     }else{
2722       push @total_items, $total;
2723     }
2724     push @buf,['','-----------'];
2725     push @buf,['Total Charges',
2726                $money_char.
2727                sprintf( '%10.2f', $self->charged +
2728                                     ( $conf->exists('disable_previous_balance')
2729                                         ? 0
2730                                         : $pr_total
2731                                     )
2732                       )
2733               ];
2734     push @buf,['',''];
2735   }
2736   
2737   unless ( $conf->exists('disable_previous_balance') ) {
2738     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2739   
2740     # credits
2741     my $credittotal = 0;
2742     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2743
2744       my $total;
2745       $total->{'total_item'} = &$escape_function($credit->{'description'});
2746       $credittotal += $credit->{'amount'};
2747       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2748       $adjusttotal += $credit->{'amount'};
2749       if ( $multisection ) {
2750         my $money = $old_latex ? '' : $money_char;
2751         push @detail_items, {
2752           ext_description => [],
2753           ref          => '',
2754           quantity     => '',
2755           description  => &$escape_function($credit->{'description'}),
2756           amount       => $money. $credit->{'amount'},
2757           product_code => '',
2758           section      => $adjust_section,
2759         };
2760       } else {
2761         push @total_items, $total;
2762       }
2763
2764     }
2765     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2766
2767     #credits (again)
2768     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2769       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2770     }
2771
2772     # payments
2773     my $paymenttotal = 0;
2774     foreach my $payment ( $self->_items_payments ) {
2775       my $total = {};
2776       $total->{'total_item'} = &$escape_function($payment->{'description'});
2777       $paymenttotal += $payment->{'amount'};
2778       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2779       $adjusttotal += $payment->{'amount'};
2780       if ( $multisection ) {
2781         my $money = $old_latex ? '' : $money_char;
2782         push @detail_items, {
2783           ext_description => [],
2784           ref          => '',
2785           quantity     => '',
2786           description  => &$escape_function($payment->{'description'}),
2787           amount       => $money. $payment->{'amount'},
2788           product_code => '',
2789           section      => $adjust_section,
2790         };
2791       }else{
2792         push @total_items, $total;
2793       }
2794       push @buf, [ $payment->{'description'},
2795                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
2796                  ];
2797     }
2798     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2799   
2800     if ( $multisection ) {
2801       $adjust_section->{'subtotal'} = $other_money_char.
2802                                       sprintf('%.2f', $adjusttotal);
2803       push @sections, $adjust_section
2804         unless $adjust_section->{sort_weight};
2805     }
2806
2807     { 
2808       my $total;
2809       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2810       $total->{'total_amount'} =
2811         &$embolden_function(
2812           $other_money_char. sprintf('%.2f', $summarypage 
2813                                                ? $self->charged +
2814                                                  $self->billing_balance
2815                                                : $self->owed + $pr_total
2816                                     )
2817         );
2818       if ( $multisection && !$adjust_section->{sort_weight} ) {
2819         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2820                                          $total->{'total_amount'};
2821       }else{
2822         push @total_items, $total;
2823       }
2824       push @buf,['','-----------'];
2825       push @buf,[$self->balance_due_msg, $money_char. 
2826         sprintf("%10.2f", $balance_due ) ];
2827     }
2828   }
2829
2830   if ( $multisection ) {
2831     if ($conf->exists('svc_phone_sections')) {
2832       my $total;
2833       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2834       $total->{'total_amount'} =
2835         &$embolden_function(
2836           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
2837         );
2838       my $last_section = pop @sections;
2839       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
2840                                      $total->{'total_amount'};
2841       push @sections, $last_section;
2842     }
2843     push @sections, @$late_sections
2844       if $unsquelched;
2845   }
2846
2847   my @includelist = ();
2848   push @includelist, 'summary' if $summarypage;
2849   foreach my $include ( @includelist ) {
2850
2851     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2852     my @inc_src;
2853
2854     if ( length( $conf->config($inc_file, $agentnum) ) ) {
2855
2856       @inc_src = $conf->config($inc_file, $agentnum);
2857
2858     } else {
2859
2860       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2861
2862       my $convert_map = $convert_maps{$format}{$include};
2863
2864       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2865                        s/--\@\]/$delimiters{$format}[1]/g;
2866                        $_;
2867                      } 
2868                  &$convert_map( $conf->config($inc_file, $agentnum) );
2869
2870     }
2871
2872     my $inc_tt = new Text::Template (
2873       TYPE       => 'ARRAY',
2874       SOURCE     => [ map "$_\n", @inc_src ],
2875       DELIMITERS => $delimiters{$format},
2876     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2877
2878     unless ( $inc_tt->compile() ) {
2879       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2880       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2881       die $error;
2882     }
2883
2884     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2885
2886     $invoice_data{$include} =~ s/\n+$//
2887       if ($format eq 'latex');
2888   }
2889
2890   $invoice_lines = 0;
2891   my $wasfunc = 0;
2892   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2893     /invoice_lines\((\d*)\)/;
2894     $invoice_lines += $1 || scalar(@buf);
2895     $wasfunc=1;
2896   }
2897   die "no invoice_lines() functions in template?"
2898     if ( $format eq 'template' && !$wasfunc );
2899
2900   if ($format eq 'template') {
2901
2902     if ( $invoice_lines ) {
2903       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2904       $invoice_data{'total_pages'}++
2905         if scalar(@buf) % $invoice_lines;
2906     }
2907
2908     #setup subroutine for the template
2909     sub FS::cust_bill::_template::invoice_lines {
2910       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2911       map { 
2912         scalar(@FS::cust_bill::_template::buf)
2913           ? shift @FS::cust_bill::_template::buf
2914           : [ '', '' ];
2915       }
2916       ( 1 .. $lines );
2917     }
2918
2919     my $lines;
2920     my @collect;
2921     while (@buf) {
2922       push @collect, split("\n",
2923         $text_template->fill_in( HASH => \%invoice_data,
2924                                  PACKAGE => 'FS::cust_bill::_template'
2925                                )
2926       );
2927       $FS::cust_bill::_template::page++;
2928     }
2929     map "$_\n", @collect;
2930   }else{
2931     warn "filling in template for invoice ". $self->invnum. "\n"
2932       if $DEBUG;
2933     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2934       if $DEBUG > 1;
2935
2936     $text_template->fill_in(HASH => \%invoice_data);
2937   }
2938 }
2939
2940 # helper routine for generating date ranges
2941 sub _prior_month30s {
2942   my $self = shift;
2943   my @ranges = (
2944    [ 1,       2592000 ], # 0-30 days ago
2945    [ 2592000, 5184000 ], # 30-60 days ago
2946    [ 5184000, 7776000 ], # 60-90 days ago
2947    [ 7776000, 0       ], # 90+   days ago
2948   );
2949
2950   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
2951           $_->[1] ? $self->_date - $_->[1] - 1 : '',
2952       ] }
2953   @ranges;
2954 }
2955
2956 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2957
2958 Returns an postscript invoice, as a scalar.
2959
2960 Options can be passed as a hashref (recommended) or as a list of time, template
2961 and then any key/value pairs for any other options.
2962
2963 I<time> an optional value used to control the printing of overdue messages.  The
2964 default is now.  It isn't the date of the invoice; that's the `_date' field.
2965 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2966 L<Time::Local> and L<Date::Parse> for conversion functions.
2967
2968 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2969
2970 =cut
2971
2972 sub print_ps {
2973   my $self = shift;
2974
2975   my ($file, $lfile) = $self->print_latex(@_);
2976   my $ps = generate_ps($file);
2977   unlink($lfile);
2978
2979   $ps;
2980 }
2981
2982 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
2983
2984 Returns an PDF invoice, as a scalar.
2985
2986 Options can be passed as a hashref (recommended) or as a list of time, template
2987 and then any key/value pairs for any other options.
2988
2989 I<time> an optional value used to control the printing of overdue messages.  The
2990 default is now.  It isn't the date of the invoice; that's the `_date' field.
2991 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2992 L<Time::Local> and L<Date::Parse> for conversion functions.
2993
2994 I<template>, if specified, is the name of a suffix for alternate invoices.
2995
2996 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2997
2998 =cut
2999
3000 sub print_pdf {
3001   my $self = shift;
3002
3003   my ($file, $lfile) = $self->print_latex(@_);
3004   my $pdf = generate_pdf($file);
3005   unlink($lfile);
3006
3007   $pdf;
3008 }
3009
3010 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3011
3012 Returns an HTML invoice, as a scalar.
3013
3014 I<time> an optional value used to control the printing of overdue messages.  The
3015 default is now.  It isn't the date of the invoice; that's the `_date' field.
3016 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3017 L<Time::Local> and L<Date::Parse> for conversion functions.
3018
3019 I<template>, if specified, is the name of a suffix for alternate invoices.
3020
3021 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3022
3023 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3024 when emailing the invoice as part of a multipart/related MIME email.
3025
3026 =cut
3027
3028 sub print_html {
3029   my $self = shift;
3030   my %params;
3031   if ( ref($_[0]) ) {
3032     %params = %{ shift() }; 
3033   }else{
3034     $params{'time'} = shift;
3035     $params{'template'} = shift;
3036     $params{'cid'} = shift;
3037   }
3038
3039   $params{'format'} = 'html';
3040
3041   $self->print_generic( %params );
3042 }
3043
3044 # quick subroutine for print_latex
3045 #
3046 # There are ten characters that LaTeX treats as special characters, which
3047 # means that they do not simply typeset themselves: 
3048 #      # $ % & ~ _ ^ \ { }
3049 #
3050 # TeX ignores blanks following an escaped character; if you want a blank (as
3051 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3052
3053 sub _latex_escape {
3054   my $value = shift;
3055   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3056   $value =~ s/([<>])/\$$1\$/g;
3057   $value;
3058 }
3059
3060 #utility methods for print_*
3061
3062 sub _translate_old_latex_format {
3063   warn "_translate_old_latex_format called\n"
3064     if $DEBUG; 
3065
3066   my @template = ();
3067   while ( @_ ) {
3068     my $line = shift;
3069   
3070     if ( $line =~ /^%%Detail\s*$/ ) {
3071   
3072       push @template, q![@--!,
3073                       q!  foreach my $_tr_line (@detail_items) {!,
3074                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3075                       q!      $_tr_line->{'description'} .= !, 
3076                       q!        "\\tabularnewline\n~~".!,
3077                       q!        join( "\\tabularnewline\n~~",!,
3078                       q!          @{$_tr_line->{'ext_description'}}!,
3079                       q!        );!,
3080                       q!    }!;
3081
3082       while ( ( my $line_item_line = shift )
3083               !~ /^%%EndDetail\s*$/                            ) {
3084         $line_item_line =~ s/'/\\'/g;    # nice LTS
3085         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3086         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3087         push @template, "    \$OUT .= '$line_item_line';";
3088       }
3089
3090       push @template, '}',
3091                       '--@]';
3092       #' doh, gvim
3093     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3094
3095       push @template, '[@--',
3096                       '  foreach my $_tr_line (@total_items) {';
3097
3098       while ( ( my $total_item_line = shift )
3099               !~ /^%%EndTotalDetails\s*$/                      ) {
3100         $total_item_line =~ s/'/\\'/g;    # nice LTS
3101         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3102         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3103         push @template, "    \$OUT .= '$total_item_line';";
3104       }
3105
3106       push @template, '}',
3107                       '--@]';
3108
3109     } else {
3110       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3111       push @template, $line;  
3112     }
3113   
3114   }
3115
3116   if ($DEBUG) {
3117     warn "$_\n" foreach @template;
3118   }
3119
3120   (@template);
3121 }
3122
3123 sub terms {
3124   my $self = shift;
3125
3126   #check for an invoice-specific override
3127   return $self->invoice_terms if $self->invoice_terms;
3128   
3129   #check for a customer- specific override
3130   my $cust_main = $self->cust_main;
3131   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3132
3133   #use configured default
3134   $conf->config('invoice_default_terms') || '';
3135 }
3136
3137 sub due_date {
3138   my $self = shift;
3139   my $duedate = '';
3140   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3141     $duedate = $self->_date() + ( $1 * 86400 );
3142   }
3143   $duedate;
3144 }
3145
3146 sub due_date2str {
3147   my $self = shift;
3148   $self->due_date ? time2str(shift, $self->due_date) : '';
3149 }
3150
3151 sub balance_due_msg {
3152   my $self = shift;
3153   my $msg = 'Balance Due';
3154   return $msg unless $self->terms;
3155   if ( $self->due_date ) {
3156     $msg .= ' - Please pay by '. $self->due_date2str('%x');
3157   } elsif ( $self->terms ) {
3158     $msg .= ' - '. $self->terms;
3159   }
3160   $msg;
3161 }
3162
3163 sub balance_due_date {
3164   my $self = shift;
3165   my $duedate = '';
3166   if (    $conf->exists('invoice_default_terms') 
3167        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3168     $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
3169   }
3170   $duedate;
3171 }
3172
3173 =item invnum_date_pretty
3174
3175 Returns a string with the invoice number and date, for example:
3176 "Invoice #54 (3/20/2008)"
3177
3178 =cut
3179
3180 sub invnum_date_pretty {
3181   my $self = shift;
3182   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3183 }
3184
3185 =item _date_pretty
3186
3187 Returns a string with the date, for example: "3/20/2008"
3188
3189 =cut
3190
3191 sub _date_pretty {
3192   my $self = shift;
3193   time2str('%x', $self->_date);
3194 }
3195
3196 use vars qw(%pkg_category_cache);
3197 sub _items_sections {
3198   my $self = shift;
3199   my $late = shift;
3200   my $summarypage = shift;
3201   my $escape = shift;
3202   my $extra_sections = shift;
3203   my $format = shift;
3204
3205   my %subtotal = ();
3206   my %late_subtotal = ();
3207   my %not_tax = ();
3208
3209   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3210   {
3211
3212       my $usage = $cust_bill_pkg->usage;
3213
3214       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3215         next if ( $display->summary && $summarypage );
3216
3217         my $section = $display->section;
3218         my $type    = $display->type;
3219
3220         $not_tax{$section} = 1
3221           unless $cust_bill_pkg->pkgnum == 0;
3222
3223         if ( $display->post_total && !$summarypage ) {
3224           if (! $type || $type eq 'S') {
3225             $late_subtotal{$section} += $cust_bill_pkg->setup
3226               if $cust_bill_pkg->setup != 0;
3227           }
3228
3229           if (! $type) {
3230             $late_subtotal{$section} += $cust_bill_pkg->recur
3231               if $cust_bill_pkg->recur != 0;
3232           }
3233
3234           if ($type && $type eq 'R') {
3235             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3236               if $cust_bill_pkg->recur != 0;
3237           }
3238           
3239           if ($type && $type eq 'U') {
3240             $late_subtotal{$section} += $usage
3241               unless scalar(@$extra_sections);
3242           }
3243
3244         } else {
3245
3246           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3247
3248           if (! $type || $type eq 'S') {
3249             $subtotal{$section} += $cust_bill_pkg->setup
3250               if $cust_bill_pkg->setup != 0;
3251           }
3252
3253           if (! $type) {
3254             $subtotal{$section} += $cust_bill_pkg->recur
3255               if $cust_bill_pkg->recur != 0;
3256           }
3257
3258           if ($type && $type eq 'R') {
3259             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3260               if $cust_bill_pkg->recur != 0;
3261           }
3262           
3263           if ($type && $type eq 'U') {
3264             $subtotal{$section} += $usage
3265               unless scalar(@$extra_sections);
3266           }
3267
3268         }
3269
3270       }
3271
3272   }
3273
3274   %pkg_category_cache = ();
3275
3276   push @$late, map { { 'description' => &{$escape}($_),
3277                        'subtotal'    => $late_subtotal{$_},
3278                        'post_total'  => 1,
3279                        'sort_weight' => ( _pkg_category($_)
3280                                             ? _pkg_category($_)->weight
3281                                             : 0
3282                                        ),
3283                        ((_pkg_category($_) && _pkg_category($_)->condense)
3284                                            ? $self->_condense_section($format)
3285                                            : ()
3286                        ),
3287                    } }
3288                  sort _sectionsort keys %late_subtotal;
3289
3290   my @sections;
3291   if ( $summarypage ) {
3292     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3293                 map { $_->categoryname } qsearch('pkg_category', {});
3294   } else {
3295     @sections = keys %subtotal;
3296   }
3297
3298   my @early = map { { 'description' => &{$escape}($_),
3299                       'subtotal'    => $subtotal{$_},
3300                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3301                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3302                       'sort_weight' => ( _pkg_category($_)
3303                                            ? _pkg_category($_)->weight
3304                                            : 0
3305                                        ),
3306                        ((_pkg_category($_) && _pkg_category($_)->condense)
3307                                            ? $self->_condense_section($format)
3308                                            : ()
3309                        ),
3310                     }
3311                   } @sections;
3312   push @early, @$extra_sections if $extra_sections;
3313  
3314   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3315
3316 }
3317
3318 #helper subs for above
3319
3320 sub _sectionsort {
3321   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3322 }
3323
3324 sub _pkg_category {
3325   my $categoryname = shift;
3326   $pkg_category_cache{$categoryname} ||=
3327     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3328 }
3329
3330 my %condensed_format = (
3331   'label' => [ qw( Description Qty Amount ) ],
3332   'fields' => [
3333                 sub { shift->{description} },
3334                 sub { shift->{quantity} },
3335                 sub { shift->{amount} },
3336               ],
3337   'align'  => [ qw( l r r ) ],
3338   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
3339   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
3340 );
3341
3342 sub _condense_section {
3343   my ( $self, $format ) = ( shift, shift );
3344   ( 'condensed' => 1,
3345     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3346       qw( description_generator
3347           header_generator
3348           total_generator
3349           total_line_generator
3350         )
3351   );
3352 }
3353
3354 sub _condensed_generator_defaults {
3355   my ( $self, $format ) = ( shift, shift );
3356   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3357 }
3358
3359 my %html_align = (
3360   'c' => 'center',
3361   'l' => 'left',
3362   'r' => 'right',
3363 );
3364
3365 sub _condensed_header_generator {
3366   my ( $self, $format ) = ( shift, shift );
3367
3368   my ( $f, $prefix, $suffix, $separator, $column ) =
3369     _condensed_generator_defaults($format);
3370
3371   if ($format eq 'latex') {
3372     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3373     $suffix = "\\\\\n\\hline";
3374     $separator = "&\n";
3375     $column =
3376       sub { my ($d,$a,$s,$w) = @_;
3377             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3378           };
3379   } elsif ( $format eq 'html' ) {
3380     $prefix = '<th></th>';
3381     $suffix = '';
3382     $separator = '';
3383     $column =
3384       sub { my ($d,$a,$s,$w) = @_;
3385             return qq!<th align="$html_align{$a}">$d</th>!;
3386       };
3387   }
3388
3389   sub {
3390     my @args = @_;
3391     my @result = ();
3392
3393     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3394       push @result,
3395         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3396     }
3397
3398     $prefix. join($separator, @result). $suffix;
3399   };
3400
3401 }
3402
3403 sub _condensed_description_generator {
3404   my ( $self, $format ) = ( shift, shift );
3405
3406   my ( $f, $prefix, $suffix, $separator, $column ) =
3407     _condensed_generator_defaults($format);
3408
3409   if ($format eq 'latex') {
3410     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3411     $suffix = '\\\\';
3412     $separator = " & \n";
3413     $column =
3414       sub { my ($d,$a,$s,$w) = @_;
3415             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3416           };
3417   }elsif ( $format eq 'html' ) {
3418     $prefix = '"><td align="center"></td>';
3419     $suffix = '';
3420     $separator = '';
3421     $column =
3422       sub { my ($d,$a,$s,$w) = @_;
3423             return qq!<td align="$html_align{$a}">$d</td>!;
3424       };
3425   }
3426
3427   sub {
3428     my @args = @_;
3429     my @result = ();
3430
3431     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3432       push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
3433                                 map { $f->{$_}->[$i] } qw(align span width)
3434                               );
3435     }
3436
3437     $prefix. join( $separator, @result ). $suffix;
3438   };
3439
3440 }
3441
3442 sub _condensed_total_generator {
3443   my ( $self, $format ) = ( shift, shift );
3444
3445   my ( $f, $prefix, $suffix, $separator, $column ) =
3446     _condensed_generator_defaults($format);
3447   my $style = '';
3448
3449   if ($format eq 'latex') {
3450     $prefix = "& ";
3451     $suffix = "\\\\\n";
3452     $separator = " & \n";
3453     $column =
3454       sub { my ($d,$a,$s,$w) = @_;
3455             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3456           };
3457   }elsif ( $format eq 'html' ) {
3458     $prefix = '';
3459     $suffix = '';
3460     $separator = '';
3461     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3462     $column =
3463       sub { my ($d,$a,$s,$w) = @_;
3464             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3465       };
3466   }
3467
3468
3469   sub {
3470     my @args = @_;
3471     my @result = ();
3472
3473     #  my $r = &{$f->{fields}->[$i]}(@args);
3474     #  $r .= ' Total' unless $i;
3475
3476     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3477       push @result,
3478         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3479                     map { $f->{$_}->[$i] } qw(align span width)
3480                   );
3481     }
3482
3483     $prefix. join( $separator, @result ). $suffix;
3484   };
3485
3486 }
3487
3488 =item total_line_generator FORMAT
3489
3490 Returns a coderef used for generation of invoice total line items for this
3491 usage_class.  FORMAT is either html or latex
3492
3493 =cut
3494
3495 # should not be used: will have issues with hash element names (description vs
3496 # total_item and amount vs total_amount -- another array of functions?
3497
3498 sub _condensed_total_line_generator {
3499   my ( $self, $format ) = ( shift, shift );
3500
3501   my ( $f, $prefix, $suffix, $separator, $column ) =
3502     _condensed_generator_defaults($format);
3503   my $style = '';
3504
3505   if ($format eq 'latex') {
3506     $prefix = "& ";
3507     $suffix = "\\\\\n";
3508     $separator = " & \n";
3509     $column =
3510       sub { my ($d,$a,$s,$w) = @_;
3511             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3512           };
3513   }elsif ( $format eq 'html' ) {
3514     $prefix = '';
3515     $suffix = '';
3516     $separator = '';
3517     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3518     $column =
3519       sub { my ($d,$a,$s,$w) = @_;
3520             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3521       };
3522   }
3523
3524
3525   sub {
3526     my @args = @_;
3527     my @result = ();
3528
3529     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3530       push @result,
3531         &{$column}( &{$f->{fields}->[$i]}(@args),
3532                     map { $f->{$_}->[$i] } qw(align span width)
3533                   );
3534     }
3535
3536     $prefix. join( $separator, @result ). $suffix;
3537   };
3538
3539 }
3540
3541 #sub _items_extra_usage_sections {
3542 #  my $self = shift;
3543 #  my $escape = shift;
3544 #
3545 #  my %sections = ();
3546 #
3547 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
3548 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3549 #  {
3550 #    next unless $cust_bill_pkg->pkgnum > 0;
3551 #
3552 #    foreach my $section ( keys %usage_class ) {
3553 #
3554 #      my $usage = $cust_bill_pkg->usage($section);
3555 #
3556 #      next unless $usage && $usage > 0;
3557 #
3558 #      $sections{$section} ||= 0;
3559 #      $sections{$section} += $usage;
3560 #
3561 #    }
3562 #
3563 #  }
3564 #
3565 #  map { { 'description' => &{$escape}($_),
3566 #          'subtotal'    => $sections{$_},
3567 #          'summarized'  => '',
3568 #          'tax_section' => '',
3569 #        }
3570 #      }
3571 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3572 #
3573 #}
3574
3575 sub _items_extra_usage_sections {
3576   my $self = shift;
3577   my $escape = shift;
3578   my $format = shift;
3579
3580   my %sections = ();
3581   my %classnums = ();
3582   my %lines = ();
3583
3584   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3585   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3586     next unless $cust_bill_pkg->pkgnum > 0;
3587
3588     foreach my $classnum ( keys %usage_class ) {
3589       my $section = $usage_class{$classnum}->classname;
3590       $classnums{$section} = $classnum;
3591
3592       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3593         my $amount = $detail->amount;
3594         next unless $amount && $amount > 0;
3595  
3596         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3597         $sections{$section}{amount} += $amount;  #subtotal
3598         $sections{$section}{calls}++;
3599         $sections{$section}{duration} += $detail->duration;
3600
3601         my $desc = $detail->regionname; 
3602         my $description = $desc;
3603         $description = substr($desc, 0, 50). '...'
3604           if $format eq 'latex' && length($desc) > 50;
3605
3606         $lines{$section}{$desc} ||= {
3607           description     => &{$escape}($description),
3608           #pkgpart         => $part_pkg->pkgpart,
3609           pkgnum          => $cust_bill_pkg->pkgnum,
3610           ref             => '',
3611           amount          => 0,
3612           calls           => 0,
3613           duration        => 0,
3614           #unit_amount     => $cust_bill_pkg->unitrecur,
3615           quantity        => $cust_bill_pkg->quantity,
3616           product_code    => 'N/A',
3617           ext_description => [],
3618         };
3619
3620         $lines{$section}{$desc}{amount} += $amount;
3621         $lines{$section}{$desc}{calls}++;
3622         $lines{$section}{$desc}{duration} += $detail->duration;
3623
3624       }
3625     }
3626   }
3627
3628   my %sectionmap = ();
3629   foreach (keys %sections) {
3630     my $usage_class = $usage_class{$classnums{$_}};
3631     $sectionmap{$_} = { 'description' => &{$escape}($_),
3632                         'amount'    => $sections{$_}{amount},    #subtotal
3633                         'calls'       => $sections{$_}{calls},
3634                         'duration'    => $sections{$_}{duration},
3635                         'summarized'  => '',
3636                         'tax_section' => '',
3637                         'sort_weight' => $usage_class->weight,
3638                         ( $usage_class->format
3639                           ? ( map { $_ => $usage_class->$_($format) }
3640                               qw( description_generator header_generator total_generator total_line_generator )
3641                             )
3642                           : ()
3643                         ), 
3644                       };
3645   }
3646
3647   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3648                  values %sectionmap;
3649
3650   my @lines = ();
3651   foreach my $section ( keys %lines ) {
3652     foreach my $line ( keys %{$lines{$section}} ) {
3653       my $l = $lines{$section}{$line};
3654       $l->{section}     = $sectionmap{$section};
3655       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
3656       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3657       push @lines, $l;
3658     }
3659   }
3660
3661   return(\@sections, \@lines);
3662
3663 }
3664
3665 sub _items_svc_phone_sections {
3666   my $self = shift;
3667   my $escape = shift;
3668   my $format = shift;
3669
3670   my %sections = ();
3671   my %classnums = ();
3672   my %lines = ();
3673
3674   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3675
3676   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3677     next unless $cust_bill_pkg->pkgnum > 0;
3678
3679     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3680
3681       my $phonenum = $detail->phonenum;
3682       next unless $phonenum;
3683
3684       my $amount = $detail->amount;
3685       next unless $amount && $amount > 0;
3686
3687       $sections{$phonenum} ||= { 'amount'      => 0,
3688                                  'calls'       => 0,
3689                                  'duration'    => 0,
3690                                  'sort_weight' => -1,
3691                                  'phonenum'    => $phonenum,
3692                                 };
3693       $sections{$phonenum}{amount} += $amount;  #subtotal
3694       $sections{$phonenum}{calls}++;
3695       $sections{$phonenum}{duration} += $detail->duration;
3696
3697       my $desc = $detail->regionname; 
3698       my $description = $desc;
3699       $description = substr($desc, 0, 50). '...'
3700         if $format eq 'latex' && length($desc) > 50;
3701
3702       $lines{$phonenum}{$desc} ||= {
3703         description     => &{$escape}($description),
3704         #pkgpart         => $part_pkg->pkgpart,
3705         pkgnum          => '',
3706         ref             => '',
3707         amount          => 0,
3708         calls           => 0,
3709         duration        => 0,
3710         #unit_amount     => '',
3711         quantity        => '',
3712         product_code    => 'N/A',
3713         ext_description => [],
3714       };
3715
3716       $lines{$phonenum}{$desc}{amount} += $amount;
3717       $lines{$phonenum}{$desc}{calls}++;
3718       $lines{$phonenum}{$desc}{duration} += $detail->duration;
3719
3720       my $line = $usage_class{$detail->classnum}->classname;
3721       $sections{"$phonenum $line"} ||=
3722         { 'amount' => 0,
3723           'calls' => 0,
3724           'duration' => 0,
3725           'sort_weight' => $usage_class{$detail->classnum}->weight,
3726           'phonenum' => $phonenum,
3727         };
3728       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
3729       $sections{"$phonenum $line"}{calls}++;
3730       $sections{"$phonenum $line"}{duration} += $detail->duration;
3731
3732       $lines{"$phonenum $line"}{$desc} ||= {
3733         description     => &{$escape}($description),
3734         #pkgpart         => $part_pkg->pkgpart,
3735         pkgnum          => '',
3736         ref             => '',
3737         amount          => 0,
3738         calls           => 0,
3739         duration        => 0,
3740         #unit_amount     => '',
3741         quantity        => '',
3742         product_code    => 'N/A',
3743         ext_description => [],
3744       };
3745
3746       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3747       $lines{"$phonenum $line"}{$desc}{calls}++;
3748       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3749       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3750            $detail->formatted('format' => $format);
3751
3752     }
3753   }
3754
3755   my %sectionmap = ();
3756   my $simple = new FS::usage_class { format => 'simple' }; #bleh
3757   my $usage_simple = new FS::usage_class { format => 'usage_simple' }; #bleh
3758   foreach ( keys %sections ) {
3759     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3760     my $usage_class = $summary ? $simple : $usage_simple;
3761     my $ending = $summary ? ' usage charges' : '';
3762     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3763                         'amount'    => $sections{$_}{amount},    #subtotal
3764                         'calls'       => $sections{$_}{calls},
3765                         'duration'    => $sections{$_}{duration},
3766                         'summarized'  => '',
3767                         'tax_section' => '',
3768                         'phonenum'    => $sections{$_}{phonenum},
3769                         'sort_weight' => $sections{$_}{sort_weight},
3770                         'post_total'  => $summary, #inspire pagebreak
3771                         (
3772                           ( map { $_ => $usage_class->$_($format) }
3773                             qw( description_generator
3774                                 header_generator
3775                                 total_generator
3776                                 total_line_generator
3777                               )
3778                           )
3779                         ), 
3780                       };
3781   }
3782
3783   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
3784                         $a->{sort_weight} <=> $b->{sort_weight}
3785                       }
3786                  values %sectionmap;
3787
3788   my @lines = ();
3789   foreach my $section ( keys %lines ) {
3790     foreach my $line ( keys %{$lines{$section}} ) {
3791       my $l = $lines{$section}{$line};
3792       $l->{section}     = $sectionmap{$section};
3793       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
3794       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3795       push @lines, $l;
3796     }
3797   }
3798
3799   return(\@sections, \@lines);
3800
3801 }
3802
3803 sub _items {
3804   my $self = shift;
3805
3806   #my @display = scalar(@_)
3807   #              ? @_
3808   #              : qw( _items_previous _items_pkg );
3809   #              #: qw( _items_pkg );
3810   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3811   my @display = qw( _items_previous _items_pkg );
3812
3813   my @b = ();
3814   foreach my $display ( @display ) {
3815     push @b, $self->$display(@_);
3816   }
3817   @b;
3818 }
3819
3820 sub _items_previous {
3821   my $self = shift;
3822   my $cust_main = $self->cust_main;
3823   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3824   my @b = ();
3825   foreach ( @pr_cust_bill ) {
3826     my $date = $conf->exists('invoice_show_prior_due_date')
3827                ? 'due '. $_->due_date2str($date_format)
3828                : time2str('%x', $_->_date); # date_format here, too,
3829                                             # but fix _items_cust_bill_pkg,
3830                                             # header, others?
3831     push @b, {
3832       'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
3833       #'pkgpart'     => 'N/A',
3834       'pkgnum'      => 'N/A',
3835       'amount'      => sprintf("%.2f", $_->owed),
3836     };
3837   }
3838   @b;
3839
3840   #{
3841   #    'description'     => 'Previous Balance',
3842   #    #'pkgpart'         => 'N/A',
3843   #    'pkgnum'          => 'N/A',
3844   #    'amount'          => sprintf("%10.2f", $pr_total ),
3845   #    'ext_description' => [ map {
3846   #                                 "Invoice ". $_->invnum.
3847   #                                 " (". time2str("%x",$_->_date). ") ".
3848   #                                 sprintf("%10.2f", $_->owed)
3849   #                         } @pr_cust_bill ],
3850
3851   #};
3852 }
3853
3854 sub _items_pkg {
3855   my $self = shift;
3856   my %options = @_;
3857   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3858   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3859   if ($options{section} && $options{section}->{condensed}) {
3860     my %itemshash = ();
3861     local $Storable::canonical = 1;
3862     foreach ( @items ) {
3863       my $item = { %$_ };
3864       delete $item->{ref};
3865       delete $item->{ext_description};
3866       my $key = freeze($item);
3867       $itemshash{$key} ||= 0;
3868       $itemshash{$key} ++; # += $item->{quantity};
3869     }
3870     @items = sort { $a->{description} cmp $b->{description} }
3871              map { my $i = thaw($_);
3872                    $i->{quantity} = $itemshash{$_};
3873                    $i->{amount} =
3874                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3875                    $i;
3876                  }
3877              keys %itemshash;
3878   }
3879   @items;
3880 }
3881
3882 sub _taxsort {
3883   return 0 unless $a cmp $b;
3884   return -1 if $b eq 'Tax';
3885   return 1 if $a eq 'Tax';
3886   return -1 if $b eq 'Other surcharges';
3887   return 1 if $a eq 'Other surcharges';
3888   $a cmp $b;
3889 }
3890
3891 sub _items_tax {
3892   my $self = shift;
3893   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3894   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3895 }
3896
3897 sub _items_cust_bill_pkg {
3898   my $self = shift;
3899   my $cust_bill_pkg = shift;
3900   my %opt = @_;
3901
3902   my $format = $opt{format} || '';
3903   my $escape_function = $opt{escape_function} || sub { shift };
3904   my $format_function = $opt{format_function} || '';
3905   my $unsquelched = $opt{unsquelched} || '';
3906   my $section = $opt{section}->{description} if $opt{section};
3907   my $summary_page = $opt{summary_page} || '';
3908   my $multilocation = $opt{multilocation} || '';
3909
3910   my @b = ();
3911   my ($s, $r, $u) = ( undef, undef, undef );
3912   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3913   {
3914
3915     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
3916       if ( $_ && !$cust_bill_pkg->hidden ) {
3917         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3918         $_->{amount}      =~ s/^\-0\.00$/0.00/;
3919         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3920         push @b, { %$_ }
3921           unless $_->{amount} == 0;
3922         $_ = undef;
3923       }
3924     }
3925
3926     foreach my $display ( grep { defined($section)
3927                                  ? $_->section eq $section
3928                                  : 1
3929                                }
3930                           grep { !$_->summary || !$summary_page }
3931                           $cust_bill_pkg->cust_bill_pkg_display
3932                         )
3933     {
3934
3935       my $type = $display->type;
3936
3937       my $desc = $cust_bill_pkg->desc;
3938       $desc = substr($desc, 0, 50). '...'
3939         if $format eq 'latex' && length($desc) > 50;
3940
3941       my %details_opt = ( 'format'          => $format,
3942                           'escape_function' => $escape_function,
3943                           'format_function' => $format_function,
3944                         );
3945
3946       if ( $cust_bill_pkg->pkgnum > 0 ) {
3947
3948         my $cust_pkg = $cust_bill_pkg->cust_pkg;
3949
3950         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3951
3952           my $description = $desc;
3953           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3954
3955           my @d = ();
3956           unless ( $cust_pkg->part_pkg->hide_svc_detail
3957                 || $cust_bill_pkg->hidden )
3958           {
3959             push @d, map &{$escape_function}($_),
3960                          $cust_pkg->h_labels_short($self->_date);
3961             if ( $multilocation ) {
3962               my $loc = $cust_pkg->location_label;
3963               $loc = substr($desc, 0, 50). '...'
3964                 if $format eq 'latex' && length($loc) > 50;
3965               push @d, &{$escape_function}($loc);
3966             }
3967           }
3968           push @d, $cust_bill_pkg->details(%details_opt)
3969             if $cust_bill_pkg->recur == 0;
3970
3971           if ( $cust_bill_pkg->hidden ) {
3972             $s->{amount}      += $cust_bill_pkg->setup;
3973             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3974             push @{ $s->{ext_description} }, @d;
3975           } else {
3976             $s = {
3977               description     => $description,
3978               #pkgpart         => $part_pkg->pkgpart,
3979               pkgnum          => $cust_bill_pkg->pkgnum,
3980               amount          => $cust_bill_pkg->setup,
3981               unit_amount     => $cust_bill_pkg->unitsetup,
3982               quantity        => $cust_bill_pkg->quantity,
3983               ext_description => \@d,
3984             };
3985           };
3986
3987         }
3988
3989         if ( $cust_bill_pkg->recur != 0 &&
3990              ( !$type || $type eq 'R' || $type eq 'U' )
3991            )
3992         {
3993
3994           my $is_summary = $display->summary;
3995           my $description = ($is_summary && $type && $type eq 'U')
3996                             ? "Usage charges" : $desc;
3997
3998           unless ( $conf->exists('disable_line_item_date_ranges') ) {
3999             $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
4000                             " - ". time2str("%x", $cust_bill_pkg->edate). ")";
4001           }
4002
4003           my @d = ();
4004
4005           #at least until cust_bill_pkg has "past" ranges in addition to
4006           #the "future" sdate/edate ones... see #3032
4007           my @dates = ( $self->_date );
4008           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4009           push @dates, $prev->sdate if $prev;
4010
4011           unless ( $cust_pkg->part_pkg->hide_svc_detail
4012                 || $cust_bill_pkg->itemdesc
4013                 || $cust_bill_pkg->hidden
4014                 || $is_summary && $type && $type eq 'U' )
4015           {
4016             push @d, map &{$escape_function}($_),
4017                          $cust_pkg->h_labels_short(@dates)
4018                                                    #$cust_bill_pkg->edate,
4019                                                    #$cust_bill_pkg->sdate)
4020             ;
4021             if ( $multilocation ) {
4022               my $loc = $cust_pkg->location_label;
4023               $loc = substr($desc, 0, 50). '...'
4024                 if $format eq 'latex' && length($loc) > 50;
4025               push @d, &{$escape_function}($loc);
4026             }
4027           }
4028
4029           push @d, $cust_bill_pkg->details(%details_opt)
4030             unless ($is_summary || $type && $type eq 'R');
4031   
4032           my $amount = 0;
4033           if (!$type) {
4034             $amount = $cust_bill_pkg->recur;
4035           }elsif($type eq 'R') {
4036             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4037           }elsif($type eq 'U') {
4038             $amount = $cust_bill_pkg->usage;
4039           }
4040   
4041           if ( !$type || $type eq 'R' ) {
4042
4043             if ( $cust_bill_pkg->hidden ) {
4044               $r->{amount}      += $amount;
4045               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4046               push @{ $r->{ext_description} }, @d;
4047             } else {
4048               $r = {
4049                 description     => $description,
4050                 #pkgpart         => $part_pkg->pkgpart,
4051                 pkgnum          => $cust_bill_pkg->pkgnum,
4052                 amount          => $amount,
4053                 unit_amount     => $cust_bill_pkg->unitrecur,
4054                 quantity        => $cust_bill_pkg->quantity,
4055                 ext_description => \@d,
4056               };
4057             }
4058
4059           } elsif ( $amount ) {  # && $type eq 'U'
4060
4061             if ( $cust_bill_pkg->hidden ) {
4062               $u->{amount}      += $amount;
4063               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4064               push @{ $u->{ext_description} }, @d;
4065             } else {
4066               $u = {
4067                 description     => $description,
4068                 #pkgpart         => $part_pkg->pkgpart,
4069                 pkgnum          => $cust_bill_pkg->pkgnum,
4070                 amount          => $amount,
4071                 unit_amount     => $cust_bill_pkg->unitrecur,
4072                 quantity        => $cust_bill_pkg->quantity,
4073                 ext_description => \@d,
4074               };
4075             }
4076
4077           }
4078
4079         } # recurring or usage with recurring charge
4080
4081       } else { #pkgnum tax or one-shot line item (??)
4082
4083         if ( $cust_bill_pkg->setup != 0 ) {
4084           push @b, {
4085             'description' => $desc,
4086             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
4087           };
4088         }
4089         if ( $cust_bill_pkg->recur != 0 ) {
4090           push @b, {
4091             'description' => "$desc (".
4092                              time2str("%x", $cust_bill_pkg->sdate). ' - '.
4093                              time2str("%x", $cust_bill_pkg->edate). ')',
4094             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
4095           };
4096         }
4097
4098       }
4099
4100     }
4101
4102   }
4103
4104   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4105     if ( $_  ) {
4106       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4107       $_->{amount}      =~ s/^\-0\.00$/0.00/;
4108       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4109       push @b, { %$_ }
4110         unless $_->{amount} == 0;
4111     }
4112   }
4113
4114   @b;
4115
4116 }
4117
4118 sub _items_credits {
4119   my( $self, %opt ) = @_;
4120   my $trim_len = $opt{'trim_len'} || 60;
4121
4122   my @b;
4123   #credits
4124   foreach ( $self->cust_credited ) {
4125
4126     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4127
4128     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4129     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4130     $reason = " ($reason) " if $reason;
4131
4132     push @b, {
4133       #'description' => 'Credit ref\#'. $_->crednum.
4134       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
4135       #                 $reason,
4136       'description' => 'Credit applied '.
4137                        time2str("%x",$_->cust_credit->_date). $reason,
4138       'amount'      => sprintf("%.2f",$_->amount),
4139     };
4140   }
4141
4142   @b;
4143
4144 }
4145
4146 sub _items_payments {
4147   my $self = shift;
4148
4149   my @b;
4150   #get & print payments
4151   foreach ( $self->cust_bill_pay ) {
4152
4153     #something more elaborate if $_->amount ne ->cust_pay->paid ?
4154
4155     push @b, {
4156       'description' => "Payment received ".
4157                        time2str("%x",$_->cust_pay->_date ),
4158       'amount'      => sprintf("%.2f", $_->amount )
4159     };
4160   }
4161
4162   @b;
4163
4164 }
4165
4166 =item call_details [ OPTION => VALUE ... ]
4167
4168 Returns an array of CSV strings representing the call details for this invoice
4169 The only option available is the boolean prepend_billed_number
4170
4171 =cut
4172
4173 sub call_details {
4174   my ($self, %opt) = @_;
4175
4176   my $format_function = sub { shift };
4177
4178   if ($opt{prepend_billed_number}) {
4179     $format_function = sub {
4180       my $detail = shift;
4181       my $row = shift;
4182
4183       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4184       
4185     };
4186   }
4187
4188   my @details = map { $_->details( 'format_function' => $format_function,
4189                                    'escape_function' => sub{ return() },
4190                                  )
4191                     }
4192                   grep { $_->pkgnum }
4193                   $self->cust_bill_pkg;
4194   my $header = $details[0];
4195   ( $header, grep { $_ ne $header } @details );
4196 }
4197
4198
4199 =back
4200
4201 =head1 SUBROUTINES
4202
4203 =over 4
4204
4205 =item process_reprint
4206
4207 =cut
4208
4209 sub process_reprint {
4210   process_re_X('print', @_);
4211 }
4212
4213 =item process_reemail
4214
4215 =cut
4216
4217 sub process_reemail {
4218   process_re_X('email', @_);
4219 }
4220
4221 =item process_refax
4222
4223 =cut
4224
4225 sub process_refax {
4226   process_re_X('fax', @_);
4227 }
4228
4229 =item process_reftp
4230
4231 =cut
4232
4233 sub process_reftp {
4234   process_re_X('ftp', @_);
4235 }
4236
4237 =item respool
4238
4239 =cut
4240
4241 sub process_respool {
4242   process_re_X('spool', @_);
4243 }
4244
4245 use Storable qw(thaw);
4246 use Data::Dumper;
4247 use MIME::Base64;
4248 sub process_re_X {
4249   my( $method, $job ) = ( shift, shift );
4250   warn "$me process_re_X $method for job $job\n" if $DEBUG;
4251
4252   my $param = thaw(decode_base64(shift));
4253   warn Dumper($param) if $DEBUG;
4254
4255   re_X(
4256     $method,
4257     $job,
4258     %$param,
4259   );
4260
4261 }
4262
4263 sub re_X {
4264   my($method, $job, %param ) = @_;
4265   if ( $DEBUG ) {
4266     warn "re_X $method for job $job with param:\n".
4267          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
4268   }
4269
4270   #some false laziness w/search/cust_bill.html
4271   my $distinct = '';
4272   my $orderby = 'ORDER BY cust_bill._date';
4273
4274   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4275
4276   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4277      
4278   my @cust_bill = qsearch( {
4279     #'select'    => "cust_bill.*",
4280     'table'     => 'cust_bill',
4281     'addl_from' => $addl_from,
4282     'hashref'   => {},
4283     'extra_sql' => $extra_sql,
4284     'order_by'  => $orderby,
4285     'debug' => 1,
4286   } );
4287
4288   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4289
4290   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4291     if $DEBUG;
4292
4293   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4294   foreach my $cust_bill ( @cust_bill ) {
4295     $cust_bill->$method();
4296
4297     if ( $job ) { #progressbar foo
4298       $num++;
4299       if ( time - $min_sec > $last ) {
4300         my $error = $job->update_statustext(
4301           int( 100 * $num / scalar(@cust_bill) )
4302         );
4303         die $error if $error;
4304         $last = time;
4305       }
4306     }
4307
4308   }
4309
4310 }
4311
4312 =back
4313
4314 =head1 CLASS METHODS
4315
4316 =over 4
4317
4318 =item owed_sql
4319
4320 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4321
4322 =cut
4323
4324 sub owed_sql {
4325   my $class = shift;
4326   'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
4327 }
4328
4329 =item net_sql
4330
4331 Returns an SQL fragment to retreive the net amount (charged minus credited).
4332
4333 =cut
4334
4335 sub net_sql {
4336   my $class = shift;
4337   'charged - '. $class->credited_sql;
4338 }
4339
4340 =item paid_sql
4341
4342 Returns an SQL fragment to retreive the amount paid against this invoice.
4343
4344 =cut
4345
4346 sub paid_sql {
4347   #my $class = shift;
4348   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4349        WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
4350 }
4351
4352 =item credited_sql
4353
4354 Returns an SQL fragment to retreive the amount credited against this invoice.
4355
4356 =cut
4357
4358 sub credited_sql {
4359   #my $class = shift;
4360   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4361        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
4362 }
4363
4364 =item search_sql_where HASHREF
4365
4366 Class method which returns an SQL WHERE fragment to search for parameters
4367 specified in HASHREF.  Valid parameters are
4368
4369 =over 4
4370
4371 =item _date
4372
4373 List reference of start date, end date, as UNIX timestamps.
4374
4375 =item invnum_min
4376
4377 =item invnum_max
4378
4379 =item agentnum
4380
4381 =item charged
4382
4383 List reference of charged limits (exclusive).
4384
4385 =item owed
4386
4387 List reference of charged limits (exclusive).
4388
4389 =item open
4390
4391 flag, return open invoices only
4392
4393 =item net
4394
4395 flag, return net invoices only
4396
4397 =item days
4398
4399 =item newest_percust
4400
4401 =back
4402
4403 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4404
4405 =cut
4406
4407 sub search_sql_where {
4408   my($class, $param) = @_;
4409   if ( $DEBUG ) {
4410     warn "$me search_sql_where called with params: \n".
4411          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
4412   }
4413
4414   my @search = ();
4415
4416   #agentnum
4417   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4418     push @search, "cust_main.agentnum = $1";
4419   }
4420
4421   #_date
4422   if ( $param->{_date} ) {
4423     my($beginning, $ending) = @{$param->{_date}};
4424
4425     push @search, "cust_bill._date >= $beginning",
4426                   "cust_bill._date <  $ending";
4427   }
4428
4429   #invnum
4430   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4431     push @search, "cust_bill.invnum >= $1";
4432   }
4433   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4434     push @search, "cust_bill.invnum <= $1";
4435   }
4436
4437   #charged
4438   if ( $param->{charged} ) {
4439     my @charged = ref($param->{charged})
4440                     ? @{ $param->{charged} }
4441                     : ($param->{charged});
4442
4443     push @search, map { s/^charged/cust_bill.charged/; $_; }
4444                       @charged;
4445   }
4446
4447   my $owed_sql = FS::cust_bill->owed_sql;
4448
4449   #owed
4450   if ( $param->{owed} ) {
4451     my @owed = ref($param->{owed})
4452                  ? @{ $param->{owed} }
4453                  : ($param->{owed});
4454     push @search, map { s/^owed/$owed_sql/; $_; }
4455                       @owed;
4456   }
4457
4458   #open/net flags
4459   push @search, "0 != $owed_sql"
4460     if $param->{'open'};
4461   push @search, '0 != '. FS::cust_bill->net_sql
4462     if $param->{'net'};
4463
4464   #days
4465   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4466     if $param->{'days'};
4467
4468   #newest_percust
4469   if ( $param->{'newest_percust'} ) {
4470
4471     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4472     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4473
4474     my @newest_where = map { my $x = $_;
4475                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
4476                              $x;
4477                            }
4478                            grep ! /^cust_main./, @search;
4479     my $newest_where = scalar(@newest_where)
4480                          ? ' AND '. join(' AND ', @newest_where)
4481                          : '';
4482
4483
4484     push @search, "cust_bill._date = (
4485       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4486         WHERE newest_cust_bill.custnum = cust_bill.custnum
4487           $newest_where
4488     )";
4489
4490   }
4491
4492   #agent virtualization
4493   my $curuser = $FS::CurrentUser::CurrentUser;
4494   if ( $curuser->username eq 'fs_queue'
4495        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4496     my $username = $1;
4497     my $newuser = qsearchs('access_user', {
4498       'username' => $username,
4499       'disabled' => '',
4500     } );
4501     if ( $newuser ) {
4502       $curuser = $newuser;
4503     } else {
4504       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4505     }
4506   }
4507   push @search, $curuser->agentnums_sql;
4508
4509   join(' AND ', @search );
4510
4511 }
4512
4513 =back
4514
4515 =head1 BUGS
4516
4517 The delete method.
4518
4519 =head1 SEE ALSO
4520
4521 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4522 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
4523 documentation.
4524
4525 =cut
4526
4527 1;
4528