correct invoice package address display and reduce false laziness
[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     unshift @sections, $previous_section if $pr_total;
2635   }
2636
2637   foreach my $tax ( $self->_items_tax ) {
2638
2639     $taxtotal += $tax->{'amount'};
2640
2641     my $description = &$escape_function( $tax->{'description'} );
2642     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2643
2644     if ( $multisection ) {
2645
2646       my $money = $old_latex ? '' : $money_char;
2647       push @detail_items, {
2648         ext_description => [],
2649         ref          => '',
2650         quantity     => '',
2651         description  => $description,
2652         amount       => $money. $amount,
2653         product_code => '',
2654         section      => $tax_section,
2655       };
2656
2657     } else {
2658
2659       push @total_items, {
2660         'total_item'   => $description,
2661         'total_amount' => $other_money_char. $amount,
2662       };
2663
2664     }
2665
2666     push @buf,[ $description,
2667                 $money_char. $amount,
2668               ];
2669
2670   }
2671   
2672   if ( $taxtotal ) {
2673     my $total = {};
2674     $total->{'total_item'} = 'Sub-total';
2675     $total->{'total_amount'} =
2676       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2677
2678     if ( $multisection ) {
2679       $tax_section->{'subtotal'} = $other_money_char.
2680                                    sprintf('%.2f', $taxtotal);
2681       $tax_section->{'pretotal'} = 'New charges sub-total '.
2682                                    $total->{'total_amount'};
2683       push @sections, $tax_section if $taxtotal;
2684     }else{
2685       unshift @total_items, $total;
2686     }
2687   }
2688   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2689
2690   push @buf,['','-----------'];
2691   push @buf,[( $conf->exists('disable_previous_balance') 
2692                ? 'Total Charges'
2693                : 'Total New Charges'
2694              ),
2695              $money_char. sprintf("%10.2f",$self->charged) ];
2696   push @buf,['',''];
2697
2698   {
2699     my $total = {};
2700     $total->{'total_item'} = &$embolden_function('Total');
2701     $total->{'total_amount'} =
2702       &$embolden_function(
2703         $other_money_char.
2704         sprintf( '%.2f',
2705                  $self->charged + ( $conf->exists('disable_previous_balance')
2706                                     ? 0
2707                                     : $pr_total
2708                                   )
2709                )
2710       );
2711     if ( $multisection ) {
2712       if ( $adjust_section->{'sort_weight'} ) {
2713         $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2714           sprintf("%.2f", ($self->billing_balance || 0) );
2715       } else {
2716         $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2717                                         sprintf('%.2f', $self->charged );
2718       } 
2719     }else{
2720       push @total_items, $total;
2721     }
2722     push @buf,['','-----------'];
2723     push @buf,['Total Charges',
2724                $money_char.
2725                sprintf( '%10.2f', $self->charged +
2726                                     ( $conf->exists('disable_previous_balance')
2727                                         ? 0
2728                                         : $pr_total
2729                                     )
2730                       )
2731               ];
2732     push @buf,['',''];
2733   }
2734   
2735   unless ( $conf->exists('disable_previous_balance') ) {
2736     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2737   
2738     # credits
2739     my $credittotal = 0;
2740     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2741
2742       my $total;
2743       $total->{'total_item'} = &$escape_function($credit->{'description'});
2744       $credittotal += $credit->{'amount'};
2745       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2746       $adjusttotal += $credit->{'amount'};
2747       if ( $multisection ) {
2748         my $money = $old_latex ? '' : $money_char;
2749         push @detail_items, {
2750           ext_description => [],
2751           ref          => '',
2752           quantity     => '',
2753           description  => &$escape_function($credit->{'description'}),
2754           amount       => $money. $credit->{'amount'},
2755           product_code => '',
2756           section      => $adjust_section,
2757         };
2758       } else {
2759         push @total_items, $total;
2760       }
2761
2762     }
2763     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2764
2765     #credits (again)
2766     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2767       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2768     }
2769
2770     # payments
2771     my $paymenttotal = 0;
2772     foreach my $payment ( $self->_items_payments ) {
2773       my $total = {};
2774       $total->{'total_item'} = &$escape_function($payment->{'description'});
2775       $paymenttotal += $payment->{'amount'};
2776       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2777       $adjusttotal += $payment->{'amount'};
2778       if ( $multisection ) {
2779         my $money = $old_latex ? '' : $money_char;
2780         push @detail_items, {
2781           ext_description => [],
2782           ref          => '',
2783           quantity     => '',
2784           description  => &$escape_function($payment->{'description'}),
2785           amount       => $money. $payment->{'amount'},
2786           product_code => '',
2787           section      => $adjust_section,
2788         };
2789       }else{
2790         push @total_items, $total;
2791       }
2792       push @buf, [ $payment->{'description'},
2793                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
2794                  ];
2795     }
2796     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2797   
2798     if ( $multisection ) {
2799       $adjust_section->{'subtotal'} = $other_money_char.
2800                                       sprintf('%.2f', $adjusttotal);
2801       push @sections, $adjust_section
2802         unless $adjust_section->{sort_weight};
2803     }
2804
2805     { 
2806       my $total;
2807       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2808       $total->{'total_amount'} =
2809         &$embolden_function(
2810           $other_money_char. sprintf('%.2f', $summarypage 
2811                                                ? $self->charged +
2812                                                  $self->billing_balance
2813                                                : $self->owed + $pr_total
2814                                     )
2815         );
2816       if ( $multisection && !$adjust_section->{sort_weight} ) {
2817         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2818                                          $total->{'total_amount'};
2819       }else{
2820         push @total_items, $total;
2821       }
2822       push @buf,['','-----------'];
2823       push @buf,[$self->balance_due_msg, $money_char. 
2824         sprintf("%10.2f", $balance_due ) ];
2825     }
2826   }
2827
2828   if ( $multisection ) {
2829     if ($conf->exists('svc_phone_sections')) {
2830       my $total;
2831       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2832       $total->{'total_amount'} =
2833         &$embolden_function(
2834           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
2835         );
2836       my $last_section = pop @sections;
2837       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
2838                                      $total->{'total_amount'};
2839       push @sections, $last_section;
2840     }
2841     push @sections, @$late_sections
2842       if $unsquelched;
2843   }
2844
2845   my @includelist = ();
2846   push @includelist, 'summary' if $summarypage;
2847   foreach my $include ( @includelist ) {
2848
2849     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2850     my @inc_src;
2851
2852     if ( length( $conf->config($inc_file, $agentnum) ) ) {
2853
2854       @inc_src = $conf->config($inc_file, $agentnum);
2855
2856     } else {
2857
2858       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2859
2860       my $convert_map = $convert_maps{$format}{$include};
2861
2862       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2863                        s/--\@\]/$delimiters{$format}[1]/g;
2864                        $_;
2865                      } 
2866                  &$convert_map( $conf->config($inc_file, $agentnum) );
2867
2868     }
2869
2870     my $inc_tt = new Text::Template (
2871       TYPE       => 'ARRAY',
2872       SOURCE     => [ map "$_\n", @inc_src ],
2873       DELIMITERS => $delimiters{$format},
2874     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2875
2876     unless ( $inc_tt->compile() ) {
2877       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2878       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2879       die $error;
2880     }
2881
2882     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2883
2884     $invoice_data{$include} =~ s/\n+$//
2885       if ($format eq 'latex');
2886   }
2887
2888   $invoice_lines = 0;
2889   my $wasfunc = 0;
2890   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2891     /invoice_lines\((\d*)\)/;
2892     $invoice_lines += $1 || scalar(@buf);
2893     $wasfunc=1;
2894   }
2895   die "no invoice_lines() functions in template?"
2896     if ( $format eq 'template' && !$wasfunc );
2897
2898   if ($format eq 'template') {
2899
2900     if ( $invoice_lines ) {
2901       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2902       $invoice_data{'total_pages'}++
2903         if scalar(@buf) % $invoice_lines;
2904     }
2905
2906     #setup subroutine for the template
2907     sub FS::cust_bill::_template::invoice_lines {
2908       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2909       map { 
2910         scalar(@FS::cust_bill::_template::buf)
2911           ? shift @FS::cust_bill::_template::buf
2912           : [ '', '' ];
2913       }
2914       ( 1 .. $lines );
2915     }
2916
2917     my $lines;
2918     my @collect;
2919     while (@buf) {
2920       push @collect, split("\n",
2921         $text_template->fill_in( HASH => \%invoice_data,
2922                                  PACKAGE => 'FS::cust_bill::_template'
2923                                )
2924       );
2925       $FS::cust_bill::_template::page++;
2926     }
2927     map "$_\n", @collect;
2928   }else{
2929     warn "filling in template for invoice ". $self->invnum. "\n"
2930       if $DEBUG;
2931     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2932       if $DEBUG > 1;
2933
2934     $text_template->fill_in(HASH => \%invoice_data);
2935   }
2936 }
2937
2938 # helper routine for generating date ranges
2939 sub _prior_month30s {
2940   my $self = shift;
2941   my @ranges = (
2942    [ 1,       2592000 ], # 0-30 days ago
2943    [ 2592000, 5184000 ], # 30-60 days ago
2944    [ 5184000, 7776000 ], # 60-90 days ago
2945    [ 7776000, 0       ], # 90+   days ago
2946   );
2947
2948   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
2949           $_->[1] ? $self->_date - $_->[1] - 1 : '',
2950       ] }
2951   @ranges;
2952 }
2953
2954 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2955
2956 Returns an postscript invoice, as a scalar.
2957
2958 Options can be passed as a hashref (recommended) or as a list of time, template
2959 and then any key/value pairs for any other options.
2960
2961 I<time> an optional value used to control the printing of overdue messages.  The
2962 default is now.  It isn't the date of the invoice; that's the `_date' field.
2963 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2964 L<Time::Local> and L<Date::Parse> for conversion functions.
2965
2966 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2967
2968 =cut
2969
2970 sub print_ps {
2971   my $self = shift;
2972
2973   my ($file, $lfile) = $self->print_latex(@_);
2974   my $ps = generate_ps($file);
2975   unlink($lfile);
2976
2977   $ps;
2978 }
2979
2980 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
2981
2982 Returns an PDF invoice, as a scalar.
2983
2984 Options can be passed as a hashref (recommended) or as a list of time, template
2985 and then any key/value pairs for any other options.
2986
2987 I<time> an optional value used to control the printing of overdue messages.  The
2988 default is now.  It isn't the date of the invoice; that's the `_date' field.
2989 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2990 L<Time::Local> and L<Date::Parse> for conversion functions.
2991
2992 I<template>, if specified, is the name of a suffix for alternate invoices.
2993
2994 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2995
2996 =cut
2997
2998 sub print_pdf {
2999   my $self = shift;
3000
3001   my ($file, $lfile) = $self->print_latex(@_);
3002   my $pdf = generate_pdf($file);
3003   unlink($lfile);
3004
3005   $pdf;
3006 }
3007
3008 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3009
3010 Returns an HTML invoice, as a scalar.
3011
3012 I<time> an optional value used to control the printing of overdue messages.  The
3013 default is now.  It isn't the date of the invoice; that's the `_date' field.
3014 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3015 L<Time::Local> and L<Date::Parse> for conversion functions.
3016
3017 I<template>, if specified, is the name of a suffix for alternate invoices.
3018
3019 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3020
3021 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3022 when emailing the invoice as part of a multipart/related MIME email.
3023
3024 =cut
3025
3026 sub print_html {
3027   my $self = shift;
3028   my %params;
3029   if ( ref($_[0]) ) {
3030     %params = %{ shift() }; 
3031   }else{
3032     $params{'time'} = shift;
3033     $params{'template'} = shift;
3034     $params{'cid'} = shift;
3035   }
3036
3037   $params{'format'} = 'html';
3038
3039   $self->print_generic( %params );
3040 }
3041
3042 # quick subroutine for print_latex
3043 #
3044 # There are ten characters that LaTeX treats as special characters, which
3045 # means that they do not simply typeset themselves: 
3046 #      # $ % & ~ _ ^ \ { }
3047 #
3048 # TeX ignores blanks following an escaped character; if you want a blank (as
3049 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3050
3051 sub _latex_escape {
3052   my $value = shift;
3053   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3054   $value =~ s/([<>])/\$$1\$/g;
3055   $value;
3056 }
3057
3058 #utility methods for print_*
3059
3060 sub _translate_old_latex_format {
3061   warn "_translate_old_latex_format called\n"
3062     if $DEBUG; 
3063
3064   my @template = ();
3065   while ( @_ ) {
3066     my $line = shift;
3067   
3068     if ( $line =~ /^%%Detail\s*$/ ) {
3069   
3070       push @template, q![@--!,
3071                       q!  foreach my $_tr_line (@detail_items) {!,
3072                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3073                       q!      $_tr_line->{'description'} .= !, 
3074                       q!        "\\tabularnewline\n~~".!,
3075                       q!        join( "\\tabularnewline\n~~",!,
3076                       q!          @{$_tr_line->{'ext_description'}}!,
3077                       q!        );!,
3078                       q!    }!;
3079
3080       while ( ( my $line_item_line = shift )
3081               !~ /^%%EndDetail\s*$/                            ) {
3082         $line_item_line =~ s/'/\\'/g;    # nice LTS
3083         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3084         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3085         push @template, "    \$OUT .= '$line_item_line';";
3086       }
3087
3088       push @template, '}',
3089                       '--@]';
3090       #' doh, gvim
3091     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3092
3093       push @template, '[@--',
3094                       '  foreach my $_tr_line (@total_items) {';
3095
3096       while ( ( my $total_item_line = shift )
3097               !~ /^%%EndTotalDetails\s*$/                      ) {
3098         $total_item_line =~ s/'/\\'/g;    # nice LTS
3099         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3100         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3101         push @template, "    \$OUT .= '$total_item_line';";
3102       }
3103
3104       push @template, '}',
3105                       '--@]';
3106
3107     } else {
3108       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3109       push @template, $line;  
3110     }
3111   
3112   }
3113
3114   if ($DEBUG) {
3115     warn "$_\n" foreach @template;
3116   }
3117
3118   (@template);
3119 }
3120
3121 sub terms {
3122   my $self = shift;
3123
3124   #check for an invoice-specific override
3125   return $self->invoice_terms if $self->invoice_terms;
3126   
3127   #check for a customer- specific override
3128   my $cust_main = $self->cust_main;
3129   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3130
3131   #use configured default
3132   $conf->config('invoice_default_terms') || '';
3133 }
3134
3135 sub due_date {
3136   my $self = shift;
3137   my $duedate = '';
3138   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3139     $duedate = $self->_date() + ( $1 * 86400 );
3140   }
3141   $duedate;
3142 }
3143
3144 sub due_date2str {
3145   my $self = shift;
3146   $self->due_date ? time2str(shift, $self->due_date) : '';
3147 }
3148
3149 sub balance_due_msg {
3150   my $self = shift;
3151   my $msg = 'Balance Due';
3152   return $msg unless $self->terms;
3153   if ( $self->due_date ) {
3154     $msg .= ' - Please pay by '. $self->due_date2str('%x');
3155   } elsif ( $self->terms ) {
3156     $msg .= ' - '. $self->terms;
3157   }
3158   $msg;
3159 }
3160
3161 sub balance_due_date {
3162   my $self = shift;
3163   my $duedate = '';
3164   if (    $conf->exists('invoice_default_terms') 
3165        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3166     $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
3167   }
3168   $duedate;
3169 }
3170
3171 =item invnum_date_pretty
3172
3173 Returns a string with the invoice number and date, for example:
3174 "Invoice #54 (3/20/2008)"
3175
3176 =cut
3177
3178 sub invnum_date_pretty {
3179   my $self = shift;
3180   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3181 }
3182
3183 =item _date_pretty
3184
3185 Returns a string with the date, for example: "3/20/2008"
3186
3187 =cut
3188
3189 sub _date_pretty {
3190   my $self = shift;
3191   time2str('%x', $self->_date);
3192 }
3193
3194 use vars qw(%pkg_category_cache);
3195 sub _items_sections {
3196   my $self = shift;
3197   my $late = shift;
3198   my $summarypage = shift;
3199   my $escape = shift;
3200   my $extra_sections = shift;
3201   my $format = shift;
3202
3203   my %subtotal = ();
3204   my %late_subtotal = ();
3205   my %not_tax = ();
3206
3207   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3208   {
3209
3210       my $usage = $cust_bill_pkg->usage;
3211
3212       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3213         next if ( $display->summary && $summarypage );
3214
3215         my $section = $display->section;
3216         my $type    = $display->type;
3217
3218         $not_tax{$section} = 1
3219           unless $cust_bill_pkg->pkgnum == 0;
3220
3221         if ( $display->post_total && !$summarypage ) {
3222           if (! $type || $type eq 'S') {
3223             $late_subtotal{$section} += $cust_bill_pkg->setup
3224               if $cust_bill_pkg->setup != 0;
3225           }
3226
3227           if (! $type) {
3228             $late_subtotal{$section} += $cust_bill_pkg->recur
3229               if $cust_bill_pkg->recur != 0;
3230           }
3231
3232           if ($type && $type eq 'R') {
3233             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3234               if $cust_bill_pkg->recur != 0;
3235           }
3236           
3237           if ($type && $type eq 'U') {
3238             $late_subtotal{$section} += $usage
3239               unless scalar(@$extra_sections);
3240           }
3241
3242         } else {
3243
3244           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3245
3246           if (! $type || $type eq 'S') {
3247             $subtotal{$section} += $cust_bill_pkg->setup
3248               if $cust_bill_pkg->setup != 0;
3249           }
3250
3251           if (! $type) {
3252             $subtotal{$section} += $cust_bill_pkg->recur
3253               if $cust_bill_pkg->recur != 0;
3254           }
3255
3256           if ($type && $type eq 'R') {
3257             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3258               if $cust_bill_pkg->recur != 0;
3259           }
3260           
3261           if ($type && $type eq 'U') {
3262             $subtotal{$section} += $usage
3263               unless scalar(@$extra_sections);
3264           }
3265
3266         }
3267
3268       }
3269
3270   }
3271
3272   %pkg_category_cache = ();
3273
3274   push @$late, map { { 'description' => &{$escape}($_),
3275                        'subtotal'    => $late_subtotal{$_},
3276                        'post_total'  => 1,
3277                        'sort_weight' => ( _pkg_category($_)
3278                                             ? _pkg_category($_)->weight
3279                                             : 0
3280                                        ),
3281                        ((_pkg_category($_) && _pkg_category($_)->condense)
3282                                            ? $self->_condense_section($format)
3283                                            : ()
3284                        ),
3285                    } }
3286                  sort _sectionsort keys %late_subtotal;
3287
3288   my @sections;
3289   if ( $summarypage ) {
3290     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3291                 map { $_->categoryname } qsearch('pkg_category', {});
3292   } else {
3293     @sections = keys %subtotal;
3294   }
3295
3296   my @early = map { { 'description' => &{$escape}($_),
3297                       'subtotal'    => $subtotal{$_},
3298                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3299                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3300                       'sort_weight' => ( _pkg_category($_)
3301                                            ? _pkg_category($_)->weight
3302                                            : 0
3303                                        ),
3304                        ((_pkg_category($_) && _pkg_category($_)->condense)
3305                                            ? $self->_condense_section($format)
3306                                            : ()
3307                        ),
3308                     }
3309                   } @sections;
3310   push @early, @$extra_sections if $extra_sections;
3311  
3312   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3313
3314 }
3315
3316 #helper subs for above
3317
3318 sub _sectionsort {
3319   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3320 }
3321
3322 sub _pkg_category {
3323   my $categoryname = shift;
3324   $pkg_category_cache{$categoryname} ||=
3325     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3326 }
3327
3328 my %condensed_format = (
3329   'label' => [ qw( Description Qty Amount ) ],
3330   'fields' => [
3331                 sub { shift->{description} },
3332                 sub { shift->{quantity} },
3333                 sub { shift->{amount} },
3334               ],
3335   'align'  => [ qw( l r r ) ],
3336   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
3337   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
3338 );
3339
3340 sub _condense_section {
3341   my ( $self, $format ) = ( shift, shift );
3342   ( 'condensed' => 1,
3343     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3344       qw( description_generator
3345           header_generator
3346           total_generator
3347           total_line_generator
3348         )
3349   );
3350 }
3351
3352 sub _condensed_generator_defaults {
3353   my ( $self, $format ) = ( shift, shift );
3354   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3355 }
3356
3357 my %html_align = (
3358   'c' => 'center',
3359   'l' => 'left',
3360   'r' => 'right',
3361 );
3362
3363 sub _condensed_header_generator {
3364   my ( $self, $format ) = ( shift, shift );
3365
3366   my ( $f, $prefix, $suffix, $separator, $column ) =
3367     _condensed_generator_defaults($format);
3368
3369   if ($format eq 'latex') {
3370     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3371     $suffix = "\\\\\n\\hline";
3372     $separator = "&\n";
3373     $column =
3374       sub { my ($d,$a,$s,$w) = @_;
3375             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3376           };
3377   } elsif ( $format eq 'html' ) {
3378     $prefix = '<th></th>';
3379     $suffix = '';
3380     $separator = '';
3381     $column =
3382       sub { my ($d,$a,$s,$w) = @_;
3383             return qq!<th align="$html_align{$a}">$d</th>!;
3384       };
3385   }
3386
3387   sub {
3388     my @args = @_;
3389     my @result = ();
3390
3391     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3392       push @result,
3393         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3394     }
3395
3396     $prefix. join($separator, @result). $suffix;
3397   };
3398
3399 }
3400
3401 sub _condensed_description_generator {
3402   my ( $self, $format ) = ( shift, shift );
3403
3404   my ( $f, $prefix, $suffix, $separator, $column ) =
3405     _condensed_generator_defaults($format);
3406
3407   if ($format eq 'latex') {
3408     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3409     $suffix = '\\\\';
3410     $separator = " & \n";
3411     $column =
3412       sub { my ($d,$a,$s,$w) = @_;
3413             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3414           };
3415   }elsif ( $format eq 'html' ) {
3416     $prefix = '"><td align="center"></td>';
3417     $suffix = '';
3418     $separator = '';
3419     $column =
3420       sub { my ($d,$a,$s,$w) = @_;
3421             return qq!<td align="$html_align{$a}">$d</td>!;
3422       };
3423   }
3424
3425   sub {
3426     my @args = @_;
3427     my @result = ();
3428
3429     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3430       push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
3431                                 map { $f->{$_}->[$i] } qw(align span width)
3432                               );
3433     }
3434
3435     $prefix. join( $separator, @result ). $suffix;
3436   };
3437
3438 }
3439
3440 sub _condensed_total_generator {
3441   my ( $self, $format ) = ( shift, shift );
3442
3443   my ( $f, $prefix, $suffix, $separator, $column ) =
3444     _condensed_generator_defaults($format);
3445   my $style = '';
3446
3447   if ($format eq 'latex') {
3448     $prefix = "& ";
3449     $suffix = "\\\\\n";
3450     $separator = " & \n";
3451     $column =
3452       sub { my ($d,$a,$s,$w) = @_;
3453             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3454           };
3455   }elsif ( $format eq 'html' ) {
3456     $prefix = '';
3457     $suffix = '';
3458     $separator = '';
3459     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3460     $column =
3461       sub { my ($d,$a,$s,$w) = @_;
3462             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3463       };
3464   }
3465
3466
3467   sub {
3468     my @args = @_;
3469     my @result = ();
3470
3471     #  my $r = &{$f->{fields}->[$i]}(@args);
3472     #  $r .= ' Total' unless $i;
3473
3474     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3475       push @result,
3476         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3477                     map { $f->{$_}->[$i] } qw(align span width)
3478                   );
3479     }
3480
3481     $prefix. join( $separator, @result ). $suffix;
3482   };
3483
3484 }
3485
3486 =item total_line_generator FORMAT
3487
3488 Returns a coderef used for generation of invoice total line items for this
3489 usage_class.  FORMAT is either html or latex
3490
3491 =cut
3492
3493 # should not be used: will have issues with hash element names (description vs
3494 # total_item and amount vs total_amount -- another array of functions?
3495
3496 sub _condensed_total_line_generator {
3497   my ( $self, $format ) = ( shift, shift );
3498
3499   my ( $f, $prefix, $suffix, $separator, $column ) =
3500     _condensed_generator_defaults($format);
3501   my $style = '';
3502
3503   if ($format eq 'latex') {
3504     $prefix = "& ";
3505     $suffix = "\\\\\n";
3506     $separator = " & \n";
3507     $column =
3508       sub { my ($d,$a,$s,$w) = @_;
3509             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3510           };
3511   }elsif ( $format eq 'html' ) {
3512     $prefix = '';
3513     $suffix = '';
3514     $separator = '';
3515     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3516     $column =
3517       sub { my ($d,$a,$s,$w) = @_;
3518             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3519       };
3520   }
3521
3522
3523   sub {
3524     my @args = @_;
3525     my @result = ();
3526
3527     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3528       push @result,
3529         &{$column}( &{$f->{fields}->[$i]}(@args),
3530                     map { $f->{$_}->[$i] } qw(align span width)
3531                   );
3532     }
3533
3534     $prefix. join( $separator, @result ). $suffix;
3535   };
3536
3537 }
3538
3539 #sub _items_extra_usage_sections {
3540 #  my $self = shift;
3541 #  my $escape = shift;
3542 #
3543 #  my %sections = ();
3544 #
3545 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
3546 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3547 #  {
3548 #    next unless $cust_bill_pkg->pkgnum > 0;
3549 #
3550 #    foreach my $section ( keys %usage_class ) {
3551 #
3552 #      my $usage = $cust_bill_pkg->usage($section);
3553 #
3554 #      next unless $usage && $usage > 0;
3555 #
3556 #      $sections{$section} ||= 0;
3557 #      $sections{$section} += $usage;
3558 #
3559 #    }
3560 #
3561 #  }
3562 #
3563 #  map { { 'description' => &{$escape}($_),
3564 #          'subtotal'    => $sections{$_},
3565 #          'summarized'  => '',
3566 #          'tax_section' => '',
3567 #        }
3568 #      }
3569 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3570 #
3571 #}
3572
3573 sub _items_extra_usage_sections {
3574   my $self = shift;
3575   my $escape = shift;
3576   my $format = shift;
3577
3578   my %sections = ();
3579   my %classnums = ();
3580   my %lines = ();
3581
3582   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3583   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3584     next unless $cust_bill_pkg->pkgnum > 0;
3585
3586     foreach my $classnum ( keys %usage_class ) {
3587       my $section = $usage_class{$classnum}->classname;
3588       $classnums{$section} = $classnum;
3589
3590       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3591         my $amount = $detail->amount;
3592         next unless $amount && $amount > 0;
3593  
3594         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3595         $sections{$section}{amount} += $amount;  #subtotal
3596         $sections{$section}{calls}++;
3597         $sections{$section}{duration} += $detail->duration;
3598
3599         my $desc = $detail->regionname; 
3600         my $description = $desc;
3601         $description = substr($desc, 0, 50). '...'
3602           if $format eq 'latex' && length($desc) > 50;
3603
3604         $lines{$section}{$desc} ||= {
3605           description     => &{$escape}($description),
3606           #pkgpart         => $part_pkg->pkgpart,
3607           pkgnum          => $cust_bill_pkg->pkgnum,
3608           ref             => '',
3609           amount          => 0,
3610           calls           => 0,
3611           duration        => 0,
3612           #unit_amount     => $cust_bill_pkg->unitrecur,
3613           quantity        => $cust_bill_pkg->quantity,
3614           product_code    => 'N/A',
3615           ext_description => [],
3616         };
3617
3618         $lines{$section}{$desc}{amount} += $amount;
3619         $lines{$section}{$desc}{calls}++;
3620         $lines{$section}{$desc}{duration} += $detail->duration;
3621
3622       }
3623     }
3624   }
3625
3626   my %sectionmap = ();
3627   foreach (keys %sections) {
3628     my $usage_class = $usage_class{$classnums{$_}};
3629     $sectionmap{$_} = { 'description' => &{$escape}($_),
3630                         'amount'    => $sections{$_}{amount},    #subtotal
3631                         'calls'       => $sections{$_}{calls},
3632                         'duration'    => $sections{$_}{duration},
3633                         'summarized'  => '',
3634                         'tax_section' => '',
3635                         'sort_weight' => $usage_class->weight,
3636                         ( $usage_class->format
3637                           ? ( map { $_ => $usage_class->$_($format) }
3638                               qw( description_generator header_generator total_generator total_line_generator )
3639                             )
3640                           : ()
3641                         ), 
3642                       };
3643   }
3644
3645   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3646                  values %sectionmap;
3647
3648   my @lines = ();
3649   foreach my $section ( keys %lines ) {
3650     foreach my $line ( keys %{$lines{$section}} ) {
3651       my $l = $lines{$section}{$line};
3652       $l->{section}     = $sectionmap{$section};
3653       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
3654       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3655       push @lines, $l;
3656     }
3657   }
3658
3659   return(\@sections, \@lines);
3660
3661 }
3662
3663 sub _items_svc_phone_sections {
3664   my $self = shift;
3665   my $escape = shift;
3666   my $format = shift;
3667
3668   my %sections = ();
3669   my %classnums = ();
3670   my %lines = ();
3671
3672   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3673
3674   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3675     next unless $cust_bill_pkg->pkgnum > 0;
3676
3677     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3678
3679       my $phonenum = $detail->phonenum;
3680       next unless $phonenum;
3681
3682       my $amount = $detail->amount;
3683       next unless $amount && $amount > 0;
3684
3685       $sections{$phonenum} ||= { 'amount'      => 0,
3686                                  'calls'       => 0,
3687                                  'duration'    => 0,
3688                                  'sort_weight' => -1,
3689                                  'phonenum'    => $phonenum,
3690                                 };
3691       $sections{$phonenum}{amount} += $amount;  #subtotal
3692       $sections{$phonenum}{calls}++;
3693       $sections{$phonenum}{duration} += $detail->duration;
3694
3695       my $desc = $detail->regionname; 
3696       my $description = $desc;
3697       $description = substr($desc, 0, 50). '...'
3698         if $format eq 'latex' && length($desc) > 50;
3699
3700       $lines{$phonenum}{$desc} ||= {
3701         description     => &{$escape}($description),
3702         #pkgpart         => $part_pkg->pkgpart,
3703         pkgnum          => '',
3704         ref             => '',
3705         amount          => 0,
3706         calls           => 0,
3707         duration        => 0,
3708         #unit_amount     => '',
3709         quantity        => '',
3710         product_code    => 'N/A',
3711         ext_description => [],
3712       };
3713
3714       $lines{$phonenum}{$desc}{amount} += $amount;
3715       $lines{$phonenum}{$desc}{calls}++;
3716       $lines{$phonenum}{$desc}{duration} += $detail->duration;
3717
3718       my $line = $usage_class{$detail->classnum}->classname;
3719       $sections{"$phonenum $line"} ||=
3720         { 'amount' => 0,
3721           'calls' => 0,
3722           'duration' => 0,
3723           'sort_weight' => $usage_class{$detail->classnum}->weight,
3724           'phonenum' => $phonenum,
3725         };
3726       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
3727       $sections{"$phonenum $line"}{calls}++;
3728       $sections{"$phonenum $line"}{duration} += $detail->duration;
3729
3730       $lines{"$phonenum $line"}{$desc} ||= {
3731         description     => &{$escape}($description),
3732         #pkgpart         => $part_pkg->pkgpart,
3733         pkgnum          => '',
3734         ref             => '',
3735         amount          => 0,
3736         calls           => 0,
3737         duration        => 0,
3738         #unit_amount     => '',
3739         quantity        => '',
3740         product_code    => 'N/A',
3741         ext_description => [],
3742       };
3743
3744       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
3745       $lines{"$phonenum $line"}{$desc}{calls}++;
3746       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
3747       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
3748            $detail->formatted('format' => $format);
3749
3750     }
3751   }
3752
3753   my %sectionmap = ();
3754   my $simple = new FS::usage_class { format => 'simple' }; #bleh
3755   my $usage_simple = new FS::usage_class { format => 'usage_simple' }; #bleh
3756   foreach ( keys %sections ) {
3757     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
3758     my $usage_class = $summary ? $simple : $usage_simple;
3759     my $ending = $summary ? ' usage charges' : '';
3760     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
3761                         'amount'    => $sections{$_}{amount},    #subtotal
3762                         'calls'       => $sections{$_}{calls},
3763                         'duration'    => $sections{$_}{duration},
3764                         'summarized'  => '',
3765                         'tax_section' => '',
3766                         'phonenum'    => $sections{$_}{phonenum},
3767                         'sort_weight' => $sections{$_}{sort_weight},
3768                         'post_total'  => $summary, #inspire pagebreak
3769                         (
3770                           ( map { $_ => $usage_class->$_($format) }
3771                             qw( description_generator
3772                                 header_generator
3773                                 total_generator
3774                                 total_line_generator
3775                               )
3776                           )
3777                         ), 
3778                       };
3779   }
3780
3781   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
3782                         $a->{sort_weight} <=> $b->{sort_weight}
3783                       }
3784                  values %sectionmap;
3785
3786   my @lines = ();
3787   foreach my $section ( keys %lines ) {
3788     foreach my $line ( keys %{$lines{$section}} ) {
3789       my $l = $lines{$section}{$line};
3790       $l->{section}     = $sectionmap{$section};
3791       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
3792       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3793       push @lines, $l;
3794     }
3795   }
3796
3797   return(\@sections, \@lines);
3798
3799 }
3800
3801 sub _items {
3802   my $self = shift;
3803
3804   #my @display = scalar(@_)
3805   #              ? @_
3806   #              : qw( _items_previous _items_pkg );
3807   #              #: qw( _items_pkg );
3808   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3809   my @display = qw( _items_previous _items_pkg );
3810
3811   my @b = ();
3812   foreach my $display ( @display ) {
3813     push @b, $self->$display(@_);
3814   }
3815   @b;
3816 }
3817
3818 sub _items_previous {
3819   my $self = shift;
3820   my $cust_main = $self->cust_main;
3821   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3822   my @b = ();
3823   foreach ( @pr_cust_bill ) {
3824     my $date = $conf->exists('invoice_show_prior_due_date')
3825                ? 'due '. $_->due_date2str($date_format)
3826                : time2str('%x', $_->_date); # date_format here, too,
3827                                             # but fix _items_cust_bill_pkg,
3828                                             # header, others?
3829     push @b, {
3830       'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
3831       #'pkgpart'     => 'N/A',
3832       'pkgnum'      => 'N/A',
3833       'amount'      => sprintf("%.2f", $_->owed),
3834     };
3835   }
3836   @b;
3837
3838   #{
3839   #    'description'     => 'Previous Balance',
3840   #    #'pkgpart'         => 'N/A',
3841   #    'pkgnum'          => 'N/A',
3842   #    'amount'          => sprintf("%10.2f", $pr_total ),
3843   #    'ext_description' => [ map {
3844   #                                 "Invoice ". $_->invnum.
3845   #                                 " (". time2str("%x",$_->_date). ") ".
3846   #                                 sprintf("%10.2f", $_->owed)
3847   #                         } @pr_cust_bill ],
3848
3849   #};
3850 }
3851
3852 sub _items_pkg {
3853   my $self = shift;
3854   my %options = @_;
3855   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3856   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3857   if ($options{section} && $options{section}->{condensed}) {
3858     my %itemshash = ();
3859     local $Storable::canonical = 1;
3860     foreach ( @items ) {
3861       my $item = { %$_ };
3862       delete $item->{ref};
3863       delete $item->{ext_description};
3864       my $key = freeze($item);
3865       $itemshash{$key} ||= 0;
3866       $itemshash{$key} ++; # += $item->{quantity};
3867     }
3868     @items = sort { $a->{description} cmp $b->{description} }
3869              map { my $i = thaw($_);
3870                    $i->{quantity} = $itemshash{$_};
3871                    $i->{amount} =
3872                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
3873                    $i;
3874                  }
3875              keys %itemshash;
3876   }
3877   @items;
3878 }
3879
3880 sub _taxsort {
3881   return 0 unless $a cmp $b;
3882   return -1 if $b eq 'Tax';
3883   return 1 if $a eq 'Tax';
3884   return -1 if $b eq 'Other surcharges';
3885   return 1 if $a eq 'Other surcharges';
3886   $a cmp $b;
3887 }
3888
3889 sub _items_tax {
3890   my $self = shift;
3891   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3892   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3893 }
3894
3895 sub _items_cust_bill_pkg {
3896   my $self = shift;
3897   my $cust_bill_pkg = shift;
3898   my %opt = @_;
3899
3900   my $format = $opt{format} || '';
3901   my $escape_function = $opt{escape_function} || sub { shift };
3902   my $format_function = $opt{format_function} || '';
3903   my $unsquelched = $opt{unsquelched} || '';
3904   my $section = $opt{section}->{description} if $opt{section};
3905   my $summary_page = $opt{summary_page} || '';
3906   my $multilocation = $opt{multilocation} || '';
3907
3908   my @b = ();
3909   my ($s, $r, $u) = ( undef, undef, undef );
3910   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3911   {
3912
3913     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
3914       if ( $_ && !$cust_bill_pkg->hidden ) {
3915         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3916         $_->{amount}      =~ s/^\-0\.00$/0.00/;
3917         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3918         push @b, { %$_ }
3919           unless $_->{amount} == 0;
3920         $_ = undef;
3921       }
3922     }
3923
3924     foreach my $display ( grep { defined($section)
3925                                  ? $_->section eq $section
3926                                  : 1
3927                                }
3928                           grep { !$_->summary || !$summary_page }
3929                           $cust_bill_pkg->cust_bill_pkg_display
3930                         )
3931     {
3932
3933       my $type = $display->type;
3934
3935       my $desc = $cust_bill_pkg->desc;
3936       $desc = substr($desc, 0, 50). '...'
3937         if $format eq 'latex' && length($desc) > 50;
3938
3939       my %details_opt = ( 'format'          => $format,
3940                           'escape_function' => $escape_function,
3941                           'format_function' => $format_function,
3942                         );
3943
3944       if ( $cust_bill_pkg->pkgnum > 0 ) {
3945
3946         my $cust_pkg = $cust_bill_pkg->cust_pkg;
3947
3948         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3949
3950           my $description = $desc;
3951           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3952
3953           my @d = ();
3954           unless ( $cust_pkg->part_pkg->hide_svc_detail
3955                 || $cust_bill_pkg->hidden )
3956           {
3957             push @d, map &{$escape_function}($_),
3958                          $cust_pkg->h_labels_short($self->_date);
3959             if ( $multilocation ) {
3960               my $loc = $cust_pkg->location_label;
3961               $loc = substr($desc, 0, 50). '...'
3962                 if $format eq 'latex' && length($loc) > 50;
3963               push @d, &{$escape_function}($loc);
3964             }
3965           }
3966           push @d, $cust_bill_pkg->details(%details_opt)
3967             if $cust_bill_pkg->recur == 0;
3968
3969           if ( $cust_bill_pkg->hidden ) {
3970             $s->{amount}      += $cust_bill_pkg->setup;
3971             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3972             push @{ $s->{ext_description} }, @d;
3973           } else {
3974             $s = {
3975               description     => $description,
3976               #pkgpart         => $part_pkg->pkgpart,
3977               pkgnum          => $cust_bill_pkg->pkgnum,
3978               amount          => $cust_bill_pkg->setup,
3979               unit_amount     => $cust_bill_pkg->unitsetup,
3980               quantity        => $cust_bill_pkg->quantity,
3981               ext_description => \@d,
3982             };
3983           };
3984
3985         }
3986
3987         if ( $cust_bill_pkg->recur != 0 &&
3988              ( !$type || $type eq 'R' || $type eq 'U' )
3989            )
3990         {
3991
3992           my $is_summary = $display->summary;
3993           my $description = ($is_summary && $type && $type eq 'U')
3994                             ? "Usage charges" : $desc;
3995
3996           unless ( $conf->exists('disable_line_item_date_ranges') ) {
3997             $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3998                             " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3999           }
4000
4001           my @d = ();
4002
4003           #at least until cust_bill_pkg has "past" ranges in addition to
4004           #the "future" sdate/edate ones... see #3032
4005           my @dates = ( $self->_date );
4006           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4007           push @dates, $prev->sdate if $prev;
4008
4009           unless ( $cust_pkg->part_pkg->hide_svc_detail
4010                 || $cust_bill_pkg->itemdesc
4011                 || $cust_bill_pkg->hidden
4012                 || $is_summary && $type && $type eq 'U' )
4013           {
4014             push @d, map &{$escape_function}($_),
4015                          $cust_pkg->h_labels_short(@dates)
4016                                                    #$cust_bill_pkg->edate,
4017                                                    #$cust_bill_pkg->sdate)
4018             ;
4019             if ( $multilocation ) {
4020               my $loc = $cust_pkg->location_label;
4021               $loc = substr($desc, 0, 50). '...'
4022                 if $format eq 'latex' && length($loc) > 50;
4023               push @d, &{$escape_function}($loc);
4024             }
4025           }
4026
4027           push @d, $cust_bill_pkg->details(%details_opt)
4028             unless ($is_summary || $type && $type eq 'R');
4029   
4030           my $amount = 0;
4031           if (!$type) {
4032             $amount = $cust_bill_pkg->recur;
4033           }elsif($type eq 'R') {
4034             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4035           }elsif($type eq 'U') {
4036             $amount = $cust_bill_pkg->usage;
4037           }
4038   
4039           if ( !$type || $type eq 'R' ) {
4040
4041             if ( $cust_bill_pkg->hidden ) {
4042               $r->{amount}      += $amount;
4043               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4044               push @{ $r->{ext_description} }, @d;
4045             } else {
4046               $r = {
4047                 description     => $description,
4048                 #pkgpart         => $part_pkg->pkgpart,
4049                 pkgnum          => $cust_bill_pkg->pkgnum,
4050                 amount          => $amount,
4051                 unit_amount     => $cust_bill_pkg->unitrecur,
4052                 quantity        => $cust_bill_pkg->quantity,
4053                 ext_description => \@d,
4054               };
4055             }
4056
4057           } elsif ( $amount ) {  # && $type eq 'U'
4058
4059             if ( $cust_bill_pkg->hidden ) {
4060               $u->{amount}      += $amount;
4061               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4062               push @{ $u->{ext_description} }, @d;
4063             } else {
4064               $u = {
4065                 description     => $description,
4066                 #pkgpart         => $part_pkg->pkgpart,
4067                 pkgnum          => $cust_bill_pkg->pkgnum,
4068                 amount          => $amount,
4069                 unit_amount     => $cust_bill_pkg->unitrecur,
4070                 quantity        => $cust_bill_pkg->quantity,
4071                 ext_description => \@d,
4072               };
4073             }
4074
4075           }
4076
4077         } # recurring or usage with recurring charge
4078
4079       } else { #pkgnum tax or one-shot line item (??)
4080
4081         if ( $cust_bill_pkg->setup != 0 ) {
4082           push @b, {
4083             'description' => $desc,
4084             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
4085           };
4086         }
4087         if ( $cust_bill_pkg->recur != 0 ) {
4088           push @b, {
4089             'description' => "$desc (".
4090                              time2str("%x", $cust_bill_pkg->sdate). ' - '.
4091                              time2str("%x", $cust_bill_pkg->edate). ')',
4092             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
4093           };
4094         }
4095
4096       }
4097
4098     }
4099
4100   }
4101
4102   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4103     if ( $_  ) {
4104       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4105       $_->{amount}      =~ s/^\-0\.00$/0.00/;
4106       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4107       push @b, { %$_ }
4108         unless $_->{amount} == 0;
4109     }
4110   }
4111
4112   @b;
4113
4114 }
4115
4116 sub _items_credits {
4117   my( $self, %opt ) = @_;
4118   my $trim_len = $opt{'trim_len'} || 60;
4119
4120   my @b;
4121   #credits
4122   foreach ( $self->cust_credited ) {
4123
4124     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4125
4126     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4127     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4128     $reason = " ($reason) " if $reason;
4129
4130     push @b, {
4131       #'description' => 'Credit ref\#'. $_->crednum.
4132       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
4133       #                 $reason,
4134       'description' => 'Credit applied '.
4135                        time2str("%x",$_->cust_credit->_date). $reason,
4136       'amount'      => sprintf("%.2f",$_->amount),
4137     };
4138   }
4139
4140   @b;
4141
4142 }
4143
4144 sub _items_payments {
4145   my $self = shift;
4146
4147   my @b;
4148   #get & print payments
4149   foreach ( $self->cust_bill_pay ) {
4150
4151     #something more elaborate if $_->amount ne ->cust_pay->paid ?
4152
4153     push @b, {
4154       'description' => "Payment received ".
4155                        time2str("%x",$_->cust_pay->_date ),
4156       'amount'      => sprintf("%.2f", $_->amount )
4157     };
4158   }
4159
4160   @b;
4161
4162 }
4163
4164 =item call_details [ OPTION => VALUE ... ]
4165
4166 Returns an array of CSV strings representing the call details for this invoice
4167 The only option available is the boolean prepend_billed_number
4168
4169 =cut
4170
4171 sub call_details {
4172   my ($self, %opt) = @_;
4173
4174   my $format_function = sub { shift };
4175
4176   if ($opt{prepend_billed_number}) {
4177     $format_function = sub {
4178       my $detail = shift;
4179       my $row = shift;
4180
4181       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4182       
4183     };
4184   }
4185
4186   my @details = map { $_->details( 'format_function' => $format_function,
4187                                    'escape_function' => sub{ return() },
4188                                  )
4189                     }
4190                   grep { $_->pkgnum }
4191                   $self->cust_bill_pkg;
4192   my $header = $details[0];
4193   ( $header, grep { $_ ne $header } @details );
4194 }
4195
4196
4197 =back
4198
4199 =head1 SUBROUTINES
4200
4201 =over 4
4202
4203 =item process_reprint
4204
4205 =cut
4206
4207 sub process_reprint {
4208   process_re_X('print', @_);
4209 }
4210
4211 =item process_reemail
4212
4213 =cut
4214
4215 sub process_reemail {
4216   process_re_X('email', @_);
4217 }
4218
4219 =item process_refax
4220
4221 =cut
4222
4223 sub process_refax {
4224   process_re_X('fax', @_);
4225 }
4226
4227 =item process_reftp
4228
4229 =cut
4230
4231 sub process_reftp {
4232   process_re_X('ftp', @_);
4233 }
4234
4235 =item respool
4236
4237 =cut
4238
4239 sub process_respool {
4240   process_re_X('spool', @_);
4241 }
4242
4243 use Storable qw(thaw);
4244 use Data::Dumper;
4245 use MIME::Base64;
4246 sub process_re_X {
4247   my( $method, $job ) = ( shift, shift );
4248   warn "$me process_re_X $method for job $job\n" if $DEBUG;
4249
4250   my $param = thaw(decode_base64(shift));
4251   warn Dumper($param) if $DEBUG;
4252
4253   re_X(
4254     $method,
4255     $job,
4256     %$param,
4257   );
4258
4259 }
4260
4261 sub re_X {
4262   my($method, $job, %param ) = @_;
4263   if ( $DEBUG ) {
4264     warn "re_X $method for job $job with param:\n".
4265          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
4266   }
4267
4268   #some false laziness w/search/cust_bill.html
4269   my $distinct = '';
4270   my $orderby = 'ORDER BY cust_bill._date';
4271
4272   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4273
4274   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4275      
4276   my @cust_bill = qsearch( {
4277     #'select'    => "cust_bill.*",
4278     'table'     => 'cust_bill',
4279     'addl_from' => $addl_from,
4280     'hashref'   => {},
4281     'extra_sql' => $extra_sql,
4282     'order_by'  => $orderby,
4283     'debug' => 1,
4284   } );
4285
4286   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4287
4288   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4289     if $DEBUG;
4290
4291   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4292   foreach my $cust_bill ( @cust_bill ) {
4293     $cust_bill->$method();
4294
4295     if ( $job ) { #progressbar foo
4296       $num++;
4297       if ( time - $min_sec > $last ) {
4298         my $error = $job->update_statustext(
4299           int( 100 * $num / scalar(@cust_bill) )
4300         );
4301         die $error if $error;
4302         $last = time;
4303       }
4304     }
4305
4306   }
4307
4308 }
4309
4310 =back
4311
4312 =head1 CLASS METHODS
4313
4314 =over 4
4315
4316 =item owed_sql
4317
4318 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4319
4320 =cut
4321
4322 sub owed_sql {
4323   my $class = shift;
4324   'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
4325 }
4326
4327 =item net_sql
4328
4329 Returns an SQL fragment to retreive the net amount (charged minus credited).
4330
4331 =cut
4332
4333 sub net_sql {
4334   my $class = shift;
4335   'charged - '. $class->credited_sql;
4336 }
4337
4338 =item paid_sql
4339
4340 Returns an SQL fragment to retreive the amount paid against this invoice.
4341
4342 =cut
4343
4344 sub paid_sql {
4345   #my $class = shift;
4346   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4347        WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
4348 }
4349
4350 =item credited_sql
4351
4352 Returns an SQL fragment to retreive the amount credited against this invoice.
4353
4354 =cut
4355
4356 sub credited_sql {
4357   #my $class = shift;
4358   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4359        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
4360 }
4361
4362 =item search_sql_where HASHREF
4363
4364 Class method which returns an SQL WHERE fragment to search for parameters
4365 specified in HASHREF.  Valid parameters are
4366
4367 =over 4
4368
4369 =item _date
4370
4371 List reference of start date, end date, as UNIX timestamps.
4372
4373 =item invnum_min
4374
4375 =item invnum_max
4376
4377 =item agentnum
4378
4379 =item charged
4380
4381 List reference of charged limits (exclusive).
4382
4383 =item owed
4384
4385 List reference of charged limits (exclusive).
4386
4387 =item open
4388
4389 flag, return open invoices only
4390
4391 =item net
4392
4393 flag, return net invoices only
4394
4395 =item days
4396
4397 =item newest_percust
4398
4399 =back
4400
4401 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4402
4403 =cut
4404
4405 sub search_sql_where {
4406   my($class, $param) = @_;
4407   if ( $DEBUG ) {
4408     warn "$me search_sql_where called with params: \n".
4409          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
4410   }
4411
4412   my @search = ();
4413
4414   #agentnum
4415   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4416     push @search, "cust_main.agentnum = $1";
4417   }
4418
4419   #_date
4420   if ( $param->{_date} ) {
4421     my($beginning, $ending) = @{$param->{_date}};
4422
4423     push @search, "cust_bill._date >= $beginning",
4424                   "cust_bill._date <  $ending";
4425   }
4426
4427   #invnum
4428   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4429     push @search, "cust_bill.invnum >= $1";
4430   }
4431   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4432     push @search, "cust_bill.invnum <= $1";
4433   }
4434
4435   #charged
4436   if ( $param->{charged} ) {
4437     my @charged = ref($param->{charged})
4438                     ? @{ $param->{charged} }
4439                     : ($param->{charged});
4440
4441     push @search, map { s/^charged/cust_bill.charged/; $_; }
4442                       @charged;
4443   }
4444
4445   my $owed_sql = FS::cust_bill->owed_sql;
4446
4447   #owed
4448   if ( $param->{owed} ) {
4449     my @owed = ref($param->{owed})
4450                  ? @{ $param->{owed} }
4451                  : ($param->{owed});
4452     push @search, map { s/^owed/$owed_sql/; $_; }
4453                       @owed;
4454   }
4455
4456   #open/net flags
4457   push @search, "0 != $owed_sql"
4458     if $param->{'open'};
4459   push @search, '0 != '. FS::cust_bill->net_sql
4460     if $param->{'net'};
4461
4462   #days
4463   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4464     if $param->{'days'};
4465
4466   #newest_percust
4467   if ( $param->{'newest_percust'} ) {
4468
4469     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4470     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4471
4472     my @newest_where = map { my $x = $_;
4473                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
4474                              $x;
4475                            }
4476                            grep ! /^cust_main./, @search;
4477     my $newest_where = scalar(@newest_where)
4478                          ? ' AND '. join(' AND ', @newest_where)
4479                          : '';
4480
4481
4482     push @search, "cust_bill._date = (
4483       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4484         WHERE newest_cust_bill.custnum = cust_bill.custnum
4485           $newest_where
4486     )";
4487
4488   }
4489
4490   #agent virtualization
4491   my $curuser = $FS::CurrentUser::CurrentUser;
4492   if ( $curuser->username eq 'fs_queue'
4493        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4494     my $username = $1;
4495     my $newuser = qsearchs('access_user', {
4496       'username' => $username,
4497       'disabled' => '',
4498     } );
4499     if ( $newuser ) {
4500       $curuser = $newuser;
4501     } else {
4502       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4503     }
4504   }
4505   push @search, $curuser->agentnums_sql;
4506
4507   join(' AND ', @search );
4508
4509 }
4510
4511 =back
4512
4513 =head1 BUGS
4514
4515 The delete method.
4516
4517 =head1 SEE ALSO
4518
4519 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4520 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
4521 documentation.
4522
4523 =cut
4524
4525 1;
4526