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