localize dates that will appear on invoices, #24850
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $me 
5              $money_char $date_format $rdate_format $date_format_long );
6              # but NOT $conf
7 use vars qw( $invoice_lines @buf ); #yuck
8 use Fcntl qw(:flock); #for spool_csv
9 use Cwd;
10 use List::Util qw(min max sum);
11 use Date::Format;
12 use Date::Language;
13 use Text::Template 1.20;
14 use File::Temp 0.14;
15 use String::ShellQuote;
16 use HTML::Entities;
17 use Locale::Country;
18 use Storable qw( freeze thaw );
19 use GD::Barcode;
20 use FS::UID qw( datasrc );
21 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
22 use FS::Record qw( qsearch qsearchs dbh );
23 use FS::cust_main_Mixin;
24 use FS::cust_main;
25 use FS::cust_statement;
26 use FS::cust_bill_pkg;
27 use FS::cust_bill_pkg_display;
28 use FS::cust_bill_pkg_detail;
29 use FS::cust_credit;
30 use FS::cust_pay;
31 use FS::cust_pkg;
32 use FS::cust_credit_bill;
33 use FS::pay_batch;
34 use FS::cust_pay_batch;
35 use FS::cust_bill_event;
36 use FS::cust_event;
37 use FS::part_pkg;
38 use FS::cust_bill_pay;
39 use FS::cust_bill_pay_batch;
40 use FS::part_bill_event;
41 use FS::payby;
42 use FS::bill_batch;
43 use FS::cust_bill_batch;
44 use FS::cust_bill_pay_pkg;
45 use FS::cust_credit_bill_pkg;
46 use FS::discount_plan;
47 use FS::L10N;
48
49 @ISA = qw( FS::cust_main_Mixin FS::Record );
50
51 $DEBUG = 0;
52 $me = '[FS::cust_bill]';
53
54 #ask FS::UID to run this stuff for us later
55 FS::UID->install_callback( sub { 
56   my $conf = new FS::Conf; #global
57   $money_char       = $conf->config('money_char')       || '$';  
58   $date_format      = $conf->config('date_format')      || '%x'; #/YY
59   $rdate_format     = $conf->config('date_format')      || '%m/%d/%Y';  #/YYYY
60   $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
61 } );
62
63 =head1 NAME
64
65 FS::cust_bill - Object methods for cust_bill records
66
67 =head1 SYNOPSIS
68
69   use FS::cust_bill;
70
71   $record = new FS::cust_bill \%hash;
72   $record = new FS::cust_bill { 'column' => 'value' };
73
74   $error = $record->insert;
75
76   $error = $new_record->replace($old_record);
77
78   $error = $record->delete;
79
80   $error = $record->check;
81
82   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
83
84   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
85
86   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
87
88   @cust_pay_objects = $cust_bill->cust_pay;
89
90   $tax_amount = $record->tax;
91
92   @lines = $cust_bill->print_text;
93   @lines = $cust_bill->print_text $time;
94
95 =head1 DESCRIPTION
96
97 An FS::cust_bill object represents an invoice; a declaration that a customer
98 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
99 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
100 following fields are currently supported:
101
102 Regular fields
103
104 =over 4
105
106 =item invnum - primary key (assigned automatically for new invoices)
107
108 =item custnum - customer (see L<FS::cust_main>)
109
110 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
111 L<Time::Local> and L<Date::Parse> for conversion functions.
112
113 =item charged - amount of this invoice
114
115 =item invoice_terms - optional terms override for this specific invoice
116
117 =back
118
119 Customer info at invoice generation time
120
121 =over 4
122
123 =item previous_balance
124
125 =item billing_balance
126
127 =back
128
129 Deprecated
130
131 =over 4
132
133 =item printed - deprecated
134
135 =back
136
137 Specific use cases
138
139 =over 4
140
141 =item closed - books closed flag, empty or `Y'
142
143 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
144
145 =item agent_invid - legacy invoice number
146
147 =item promised_date - customer promised payment date, for collection
148
149 =back
150
151 =head1 METHODS
152
153 =over 4
154
155 =item new HASHREF
156
157 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
158 Invoices are normally created by calling the bill method of a customer object
159 (see L<FS::cust_main>).
160
161 =cut
162
163 sub table { 'cust_bill'; }
164
165 sub cust_linked { $_[0]->cust_main_custnum; } 
166 sub cust_unlinked_msg {
167   my $self = shift;
168   "WARNING: can't find cust_main.custnum ". $self->custnum.
169   ' (cust_bill.invnum '. $self->invnum. ')';
170 }
171
172 =item insert
173
174 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
175 returns the error, otherwise returns false.
176
177 =cut
178
179 sub insert {
180   my $self = shift;
181   warn "$me insert called\n" if $DEBUG;
182
183   local $SIG{HUP} = 'IGNORE';
184   local $SIG{INT} = 'IGNORE';
185   local $SIG{QUIT} = 'IGNORE';
186   local $SIG{TERM} = 'IGNORE';
187   local $SIG{TSTP} = 'IGNORE';
188   local $SIG{PIPE} = 'IGNORE';
189
190   my $oldAutoCommit = $FS::UID::AutoCommit;
191   local $FS::UID::AutoCommit = 0;
192   my $dbh = dbh;
193
194   my $error = $self->SUPER::insert;
195   if ( $error ) {
196     $dbh->rollback if $oldAutoCommit;
197     return $error;
198   }
199
200   if ( $self->get('cust_bill_pkg') ) {
201     foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
202       $cust_bill_pkg->invnum($self->invnum);
203       my $error = $cust_bill_pkg->insert;
204       if ( $error ) {
205         $dbh->rollback if $oldAutoCommit;
206         return "can't create invoice line item: $error";
207       }
208     }
209   }
210
211   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
212   '';
213
214 }
215
216 =item delete
217
218 This method now works but you probably shouldn't use it.  Instead, apply a
219 credit against the invoice.
220
221 Using this method to delete invoices outright is really, really bad.  There
222 would be no record you ever posted this invoice, and there are no check to
223 make sure charged = 0 or that there are no associated cust_bill_pkg records.
224
225 Really, don't use it.
226
227 =cut
228
229 sub delete {
230   my $self = shift;
231   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
232
233   local $SIG{HUP} = 'IGNORE';
234   local $SIG{INT} = 'IGNORE';
235   local $SIG{QUIT} = 'IGNORE';
236   local $SIG{TERM} = 'IGNORE';
237   local $SIG{TSTP} = 'IGNORE';
238   local $SIG{PIPE} = 'IGNORE';
239
240   my $oldAutoCommit = $FS::UID::AutoCommit;
241   local $FS::UID::AutoCommit = 0;
242   my $dbh = dbh;
243
244   foreach my $table (qw(
245     cust_bill_event
246     cust_event
247     cust_credit_bill
248     cust_bill_pay
249     cust_credit_bill
250     cust_pay_batch
251     cust_bill_pay_batch
252     cust_bill_pkg
253     cust_bill_batch
254   )) {
255
256     foreach my $linked ( $self->$table() ) {
257       my $error = $linked->delete;
258       if ( $error ) {
259         $dbh->rollback if $oldAutoCommit;
260         return $error;
261       }
262     }
263
264   }
265
266   my $error = $self->SUPER::delete(@_);
267   if ( $error ) {
268     $dbh->rollback if $oldAutoCommit;
269     return $error;
270   }
271
272   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
273
274   '';
275
276 }
277
278 =item replace [ OLD_RECORD ]
279
280 You can, but probably shouldn't modify invoices...
281
282 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
283 supplied, replaces this record.  If there is an error, returns the error,
284 otherwise returns false.
285
286 =cut
287
288 #replace can be inherited from Record.pm
289
290 # replace_check is now the preferred way to #implement replace data checks
291 # (so $object->replace() works without an argument)
292
293 sub replace_check {
294   my( $new, $old ) = ( shift, shift );
295   return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
296   #return "Can't change _date!" unless $old->_date eq $new->_date;
297   return "Can't change _date" unless $old->_date == $new->_date;
298   return "Can't change charged" unless $old->charged == $new->charged
299                                     || $old->charged == 0
300                                     || $new->{'Hash'}{'cc_surcharge_replace_hack'};
301
302   '';
303 }
304
305
306 =item add_cc_surcharge
307
308 Giant hack
309
310 =cut
311
312 sub add_cc_surcharge {
313     my ($self, $pkgnum, $amount) = (shift, shift, shift);
314
315     my $error;
316     my $cust_bill_pkg = new FS::cust_bill_pkg({
317                                     'invnum' => $self->invnum,
318                                     'pkgnum' => $pkgnum,
319                                     'setup' => $amount,
320                         });
321     $error = $cust_bill_pkg->insert;
322     return $error if $error;
323
324     $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
325     $self->charged($self->charged+$amount);
326     $error = $self->replace;
327     return $error if $error;
328
329     $self->apply_payments_and_credits;
330 }
331
332
333 =item check
334
335 Checks all fields to make sure this is a valid invoice.  If there is an error,
336 returns the error, otherwise returns false.  Called by the insert and replace
337 methods.
338
339 =cut
340
341 sub check {
342   my $self = shift;
343
344   my $error =
345     $self->ut_numbern('invnum')
346     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
347     || $self->ut_numbern('_date')
348     || $self->ut_money('charged')
349     || $self->ut_numbern('printed')
350     || $self->ut_enum('closed', [ '', 'Y' ])
351     || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
352     || $self->ut_numbern('agent_invid') #varchar?
353   ;
354   return $error if $error;
355
356   $self->_date(time) unless $self->_date;
357
358   $self->printed(0) if $self->printed eq '';
359
360   $self->SUPER::check;
361 }
362
363 =item display_invnum
364
365 Returns the displayed invoice number for this invoice: agent_invid if
366 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
367
368 =cut
369
370 sub display_invnum {
371   my $self = shift;
372   my $conf = $self->conf;
373   if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
374     return $self->agent_invid;
375   } else {
376     return $self->invnum;
377   }
378 }
379
380 =item previous_bill
381
382 Returns the customer's last invoice before this one.
383
384 =cut
385
386 sub previous_bill {
387   my $self = shift;
388   if ( !$self->get('previous_bill') ) {
389     $self->set('previous_bill', qsearchs({
390           'table'     => 'cust_bill',
391           'hashref'   => { 'custnum'  => $self->custnum,
392                            '_date'    => { op=>'<', value=>$self->_date } },
393           'order_by'  => 'ORDER BY _date DESC LIMIT 1',
394     }) );
395   }
396   $self->get('previous_bill');
397 }
398
399 =item previous
400
401 Returns a list consisting of the total previous balance for this customer, 
402 followed by the previous outstanding invoices (as FS::cust_bill objects also).
403
404 =cut
405
406 sub previous {
407   my $self = shift;
408   my $total = 0;
409   my @cust_bill = sort { $a->_date <=> $b->_date }
410     grep { $_->owed != 0 }
411       qsearch( 'cust_bill', { 'custnum'  => $self->custnum,
412                               #'_date'   => { op=>'<', value=>$self->_date },
413                               'invnum'   => { op=>'<', value=>$self->invnum },
414                             } ) 
415   ;
416   foreach ( @cust_bill ) { $total += $_->owed; }
417   $total, @cust_bill;
418 }
419
420 =item enable_previous
421
422 Whether to show the 'Previous Charges' section when printing this invoice.
423 The negation of the 'disable_previous_balance' config setting.
424
425 =cut
426
427 sub enable_previous {
428   my $self = shift;
429   my $agentnum = $self->cust_main->agentnum;
430   !$self->conf->exists('disable_previous_balance', $agentnum);
431 }
432
433 =item cust_bill_pkg
434
435 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
436
437 =cut
438
439 sub cust_bill_pkg {
440   my $self = shift;
441   qsearch(
442     { 'table'    => 'cust_bill_pkg',
443       'hashref'  => { 'invnum' => $self->invnum },
444       'order_by' => 'ORDER BY billpkgnum',
445     }
446   );
447 }
448
449 =item cust_bill_pkg_pkgnum PKGNUM
450
451 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
452 specified pkgnum.
453
454 =cut
455
456 sub cust_bill_pkg_pkgnum {
457   my( $self, $pkgnum ) = @_;
458   qsearch(
459     { 'table'    => 'cust_bill_pkg',
460       'hashref'  => { 'invnum' => $self->invnum,
461                       'pkgnum' => $pkgnum,
462                     },
463       'order_by' => 'ORDER BY billpkgnum',
464     }
465   );
466 }
467
468 =item cust_pkg
469
470 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
471 this invoice.
472
473 =cut
474
475 sub cust_pkg {
476   my $self = shift;
477   my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
478                      $self->cust_bill_pkg;
479   my %saw = ();
480   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
481 }
482
483 =item no_auto
484
485 Returns true if any of the packages (or their definitions) corresponding to the
486 line items for this invoice have the no_auto flag set.
487
488 =cut
489
490 sub no_auto {
491   my $self = shift;
492   grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
493 }
494
495 =item open_cust_bill_pkg
496
497 Returns the open line items for this invoice.
498
499 Note that cust_bill_pkg with both setup and recur fees are returned as two
500 separate line items, each with only one fee.
501
502 =cut
503
504 # modeled after cust_main::open_cust_bill
505 sub open_cust_bill_pkg {
506   my $self = shift;
507
508   # grep { $_->owed > 0 } $self->cust_bill_pkg
509
510   my %other = ( 'recur' => 'setup',
511                 'setup' => 'recur', );
512   my @open = ();
513   foreach my $field ( qw( recur setup )) {
514     push @open, map  { $_->set( $other{$field}, 0 ); $_; }
515                 grep { $_->owed($field) > 0 }
516                 $self->cust_bill_pkg;
517   }
518
519   @open;
520 }
521
522 =item cust_bill_event
523
524 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
525
526 =cut
527
528 sub cust_bill_event {
529   my $self = shift;
530   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
531 }
532
533 =item num_cust_bill_event
534
535 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
536
537 =cut
538
539 sub num_cust_bill_event {
540   my $self = shift;
541   my $sql =
542     "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
543   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
544   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
545   $sth->fetchrow_arrayref->[0];
546 }
547
548 =item cust_event
549
550 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
551
552 =cut
553
554 #false laziness w/cust_pkg.pm
555 sub cust_event {
556   my $self = shift;
557   qsearch({
558     'table'     => 'cust_event',
559     'addl_from' => 'JOIN part_event USING ( eventpart )',
560     'hashref'   => { 'tablenum' => $self->invnum },
561     'extra_sql' => " AND eventtable = 'cust_bill' ",
562   });
563 }
564
565 =item num_cust_event
566
567 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
568
569 =cut
570
571 #false laziness w/cust_pkg.pm
572 sub num_cust_event {
573   my $self = shift;
574   my $sql =
575     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
576     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
577   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
578   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
579   $sth->fetchrow_arrayref->[0];
580 }
581
582 =item cust_main
583
584 Returns the customer (see L<FS::cust_main>) for this invoice.
585
586 =cut
587
588 sub cust_main {
589   my $self = shift;
590   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
591 }
592
593 =item cust_suspend_if_balance_over AMOUNT
594
595 Suspends the customer associated with this invoice if the total amount owed on
596 this invoice and all older invoices is greater than the specified amount.
597
598 Returns a list: an empty list on success or a list of errors.
599
600 =cut
601
602 sub cust_suspend_if_balance_over {
603   my( $self, $amount ) = ( shift, shift );
604   my $cust_main = $self->cust_main;
605   if ( $cust_main->total_owed_date($self->_date) < $amount ) {
606     return ();
607   } else {
608     $cust_main->suspend(@_);
609   }
610 }
611
612 =item cust_credit
613
614 Depreciated.  See the cust_credited method.
615
616  #Returns a list consisting of the total previous credited (see
617  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
618  #outstanding credits (FS::cust_credit objects).
619
620 =cut
621
622 sub cust_credit {
623   use Carp;
624   croak "FS::cust_bill->cust_credit depreciated; see ".
625         "FS::cust_bill->cust_credit_bill";
626   #my $self = shift;
627   #my $total = 0;
628   #my @cust_credit = sort { $a->_date <=> $b->_date }
629   #  grep { $_->credited != 0 && $_->_date < $self->_date }
630   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
631   #;
632   #foreach (@cust_credit) { $total += $_->credited; }
633   #$total, @cust_credit;
634 }
635
636 =item cust_pay
637
638 Depreciated.  See the cust_bill_pay method.
639
640 #Returns all payments (see L<FS::cust_pay>) for this invoice.
641
642 =cut
643
644 sub cust_pay {
645   use Carp;
646   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
647   #my $self = shift;
648   #sort { $a->_date <=> $b->_date }
649   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
650   #;
651 }
652
653 sub cust_pay_batch {
654   my $self = shift;
655   qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
656 }
657
658 sub cust_bill_pay_batch {
659   my $self = shift;
660   qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
661 }
662
663 =item cust_bill_pay
664
665 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
666
667 =cut
668
669 sub cust_bill_pay {
670   my $self = shift;
671   map { $_ } #return $self->num_cust_bill_pay unless wantarray;
672   sort { $a->_date <=> $b->_date }
673     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
674 }
675
676 =item cust_credited
677
678 =item cust_credit_bill
679
680 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
681
682 =cut
683
684 sub cust_credited {
685   my $self = shift;
686   map { $_ } #return $self->num_cust_credit_bill unless wantarray;
687   sort { $a->_date <=> $b->_date }
688     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
689   ;
690 }
691
692 sub cust_credit_bill {
693   shift->cust_credited(@_);
694 }
695
696 #=item cust_bill_pay_pkgnum PKGNUM
697 #
698 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
699 #with matching pkgnum.
700 #
701 #=cut
702 #
703 #sub cust_bill_pay_pkgnum {
704 #  my( $self, $pkgnum ) = @_;
705 #  map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
706 #  sort { $a->_date <=> $b->_date }
707 #    qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
708 #                                'pkgnum' => $pkgnum,
709 #                              }
710 #           );
711 #}
712
713 =item cust_bill_pay_pkg PKGNUM
714
715 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
716 applied against the matching pkgnum.
717
718 =cut
719
720 sub cust_bill_pay_pkg {
721   my( $self, $pkgnum ) = @_;
722
723   qsearch({
724     'select'    => 'cust_bill_pay_pkg.*',
725     'table'     => 'cust_bill_pay_pkg',
726     'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
727                    ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
728     'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
729                    "   AND cust_bill_pkg.pkgnum = $pkgnum",
730   });
731
732 }
733
734 #=item cust_credited_pkgnum PKGNUM
735 #
736 #=item cust_credit_bill_pkgnum PKGNUM
737 #
738 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
739 #with matching pkgnum.
740 #
741 #=cut
742 #
743 #sub cust_credited_pkgnum {
744 #  my( $self, $pkgnum ) = @_;
745 #  map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
746 #  sort { $a->_date <=> $b->_date }
747 #    qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
748 #                                   'pkgnum' => $pkgnum,
749 #                                 }
750 #           );
751 #}
752 #
753 #sub cust_credit_bill_pkgnum {
754 #  shift->cust_credited_pkgnum(@_);
755 #}
756
757 =item cust_credit_bill_pkg PKGNUM
758
759 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
760 applied against the matching pkgnum.
761
762 =cut
763
764 sub cust_credit_bill_pkg {
765   my( $self, $pkgnum ) = @_;
766
767   qsearch({
768     'select'    => 'cust_credit_bill_pkg.*',
769     'table'     => 'cust_credit_bill_pkg',
770     'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
771                    ' LEFT JOIN cust_bill_pkg    USING ( billpkgnum    ) ',
772     'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
773                    "   AND cust_bill_pkg.pkgnum = $pkgnum",
774   });
775
776 }
777
778 =item cust_bill_batch
779
780 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
781
782 =cut
783
784 sub cust_bill_batch {
785   my $self = shift;
786   qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
787 }
788
789 =item discount_plans
790
791 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a 
792 hash keyed by term length.
793
794 =cut
795
796 sub discount_plans {
797   my $self = shift;
798   FS::discount_plan->all($self);
799 }
800
801 =item tax
802
803 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
804
805 =cut
806
807 sub tax {
808   my $self = shift;
809   my $total = 0;
810   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
811                                              'pkgnum' => 0 } );
812   foreach (@taxlines) { $total += $_->setup; }
813   $total;
814 }
815
816 =item owed
817
818 Returns the amount owed (still outstanding) on this invoice, which is charged
819 minus all payment applications (see L<FS::cust_bill_pay>) and credit
820 applications (see L<FS::cust_credit_bill>).
821
822 =cut
823
824 sub owed {
825   my $self = shift;
826   my $balance = $self->charged;
827   $balance -= $_->amount foreach ( $self->cust_bill_pay );
828   $balance -= $_->amount foreach ( $self->cust_credited );
829   $balance = sprintf( "%.2f", $balance);
830   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
831   $balance;
832 }
833
834 sub owed_pkgnum {
835   my( $self, $pkgnum ) = @_;
836
837   #my $balance = $self->charged;
838   my $balance = 0;
839   $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
840
841   $balance -= $_->amount            for $self->cust_bill_pay_pkg($pkgnum);
842   $balance -= $_->amount            for $self->cust_credit_bill_pkg($pkgnum);
843
844   $balance = sprintf( "%.2f", $balance);
845   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
846   $balance;
847 }
848
849 =item hide
850
851 Returns true if this invoice should be hidden.  See the
852 selfservice-hide_invoices-taxclass configuraiton setting.
853
854 =cut
855
856 sub hide {
857   my $self = shift;
858   my $conf = $self->conf;
859   my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
860     or return '';
861   my @cust_bill_pkg = $self->cust_bill_pkg;
862   my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
863   ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
864 }
865
866 =item apply_payments_and_credits [ OPTION => VALUE ... ]
867
868 Applies unapplied payments and credits to this invoice.
869
870 A hash of optional arguments may be passed.  Currently "manual" is supported.
871 If true, a payment receipt is sent instead of a statement when
872 'payment_receipt_email' configuration option is set.
873
874 If there is an error, returns the error, otherwise returns false.
875
876 =cut
877
878 sub apply_payments_and_credits {
879   my( $self, %options ) = @_;
880   my $conf = $self->conf;
881
882   local $SIG{HUP} = 'IGNORE';
883   local $SIG{INT} = 'IGNORE';
884   local $SIG{QUIT} = 'IGNORE';
885   local $SIG{TERM} = 'IGNORE';
886   local $SIG{TSTP} = 'IGNORE';
887   local $SIG{PIPE} = 'IGNORE';
888
889   my $oldAutoCommit = $FS::UID::AutoCommit;
890   local $FS::UID::AutoCommit = 0;
891   my $dbh = dbh;
892
893   $self->select_for_update; #mutex
894
895   my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
896   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
897
898   if ( $conf->exists('pkg-balances') ) {
899     # limit @payments & @credits to those w/ a pkgnum grepped from $self
900     my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
901     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
902     @credits  = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
903   }
904
905   while ( $self->owed > 0 and ( @payments || @credits ) ) {
906
907     my $app = '';
908     if ( @payments && @credits ) {
909
910       #decide which goes first by weight of top (unapplied) line item
911
912       my @open_lineitems = $self->open_cust_bill_pkg;
913
914       my $max_pay_weight =
915         max( map  { $_->part_pkg->pay_weight || 0 }
916              grep { $_ }
917              map  { $_->cust_pkg }
918                   @open_lineitems
919            );
920       my $max_credit_weight =
921         max( map  { $_->part_pkg->credit_weight || 0 }
922              grep { $_ } 
923              map  { $_->cust_pkg }
924                   @open_lineitems
925            );
926
927       #if both are the same... payments first?  it has to be something
928       if ( $max_pay_weight >= $max_credit_weight ) {
929         $app = 'pay';
930       } else {
931         $app = 'credit';
932       }
933     
934     } elsif ( @payments ) {
935       $app = 'pay';
936     } elsif ( @credits ) {
937       $app = 'credit';
938     } else {
939       die "guru meditation #12 and 35";
940     }
941
942     my $unapp_amount;
943     if ( $app eq 'pay' ) {
944
945       my $payment = shift @payments;
946       $unapp_amount = $payment->unapplied;
947       $app = new FS::cust_bill_pay { 'paynum'  => $payment->paynum };
948       $app->pkgnum( $payment->pkgnum )
949         if $conf->exists('pkg-balances') && $payment->pkgnum;
950
951     } elsif ( $app eq 'credit' ) {
952
953       my $credit = shift @credits;
954       $unapp_amount = $credit->credited;
955       $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
956       $app->pkgnum( $credit->pkgnum )
957         if $conf->exists('pkg-balances') && $credit->pkgnum;
958
959     } else {
960       die "guru meditation #12 and 35";
961     }
962
963     my $owed;
964     if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
965       warn "owed_pkgnum ". $app->pkgnum;
966       $owed = $self->owed_pkgnum($app->pkgnum);
967     } else {
968       $owed = $self->owed;
969     }
970     next unless $owed > 0;
971
972     warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
973     $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
974
975     $app->invnum( $self->invnum );
976
977     my $error = $app->insert(%options);
978     if ( $error ) {
979       $dbh->rollback if $oldAutoCommit;
980       return "Error inserting ". $app->table. " record: $error";
981     }
982     die $error if $error;
983
984   }
985
986   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
987   ''; #no error
988
989 }
990
991 =item generate_email OPTION => VALUE ...
992
993 Options:
994
995 =over 4
996
997 =item from
998
999 sender address, required
1000
1001 =item tempate
1002
1003 alternate template name, optional
1004
1005 =item print_text
1006
1007 text attachment arrayref, optional
1008
1009 =item subject
1010
1011 email subject, optional
1012
1013 =item notice_name
1014
1015 notice name instead of "Invoice", optional
1016
1017 =back
1018
1019 Returns an argument list to be passed to L<FS::Misc::send_email>.
1020
1021 =cut
1022
1023 use MIME::Entity;
1024
1025 sub generate_email {
1026
1027   my $self = shift;
1028   my %args = @_;
1029   my $conf = $self->conf;
1030
1031   my $me = '[FS::cust_bill::generate_email]';
1032
1033   my %return = (
1034     'from'      => $args{'from'},
1035     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
1036   );
1037
1038   my %opt = (
1039     'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
1040     'template'      => $args{'template'},
1041     'notice_name'   => ( $args{'notice_name'} || 'Invoice' ),
1042     'no_coupon'     => $args{'no_coupon'},
1043   );
1044
1045   my $cust_main = $self->cust_main;
1046
1047   if (ref($args{'to'}) eq 'ARRAY') {
1048     $return{'to'} = $args{'to'};
1049   } else {
1050     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
1051                            $cust_main->invoicing_list
1052                     ];
1053   }
1054
1055   if ( $conf->exists('invoice_html') ) {
1056
1057     warn "$me creating HTML/text multipart message"
1058       if $DEBUG;
1059
1060     $return{'nobody'} = 1;
1061
1062     my $alternative = build MIME::Entity
1063       'Type'        => 'multipart/alternative',
1064       #'Encoding'    => '7bit',
1065       'Disposition' => 'inline'
1066     ;
1067
1068     my $data;
1069     if ( $conf->exists('invoice_email_pdf')
1070          and scalar($conf->config('invoice_email_pdf_note')) ) {
1071
1072       warn "$me using 'invoice_email_pdf_note' in multipart message"
1073         if $DEBUG;
1074       $data = [ map { $_ . "\n" }
1075                     $conf->config('invoice_email_pdf_note')
1076               ];
1077
1078     } else {
1079
1080       warn "$me not using 'invoice_email_pdf_note' in multipart message"
1081         if $DEBUG;
1082       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1083         $data = $args{'print_text'};
1084       } else {
1085         $data = [ $self->print_text(\%opt) ];
1086       }
1087
1088     }
1089
1090     $alternative->attach(
1091       'Type'        => 'text/plain',
1092       'Encoding'    => 'quoted-printable',
1093       #'Encoding'    => '7bit',
1094       'Data'        => $data,
1095       'Disposition' => 'inline',
1096     );
1097
1098
1099     my $htmldata;
1100     my $image = '';
1101     my $barcode = '';
1102     if ( $conf->exists('invoice_email_pdf')
1103          and scalar($conf->config('invoice_email_pdf_note')) ) {
1104
1105       $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
1106
1107     } else {
1108
1109       $args{'from'} =~ /\@([\w\.\-]+)/;
1110       my $from = $1 || 'example.com';
1111       my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1112
1113       my $logo;
1114       my $agentnum = $cust_main->agentnum;
1115       if ( defined($args{'template'}) && length($args{'template'})
1116            && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1117          )
1118       {
1119         $logo = 'logo_'. $args{'template'}. '.png';
1120       } else {
1121         $logo = "logo.png";
1122       }
1123       my $image_data = $conf->config_binary( $logo, $agentnum);
1124
1125       $image = build MIME::Entity
1126         'Type'       => 'image/png',
1127         'Encoding'   => 'base64',
1128         'Data'       => $image_data,
1129         'Filename'   => 'logo.png',
1130         'Content-ID' => "<$content_id>",
1131       ;
1132    
1133       if ($conf->exists('invoice-barcode')) {
1134         my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1135         $barcode = build MIME::Entity
1136           'Type'       => 'image/png',
1137           'Encoding'   => 'base64',
1138           'Data'       => $self->invoice_barcode(0),
1139           'Filename'   => 'barcode.png',
1140           'Content-ID' => "<$barcode_content_id>",
1141         ;
1142         $opt{'barcode_cid'} = $barcode_content_id;
1143       }
1144
1145       $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
1146     }
1147
1148     $alternative->attach(
1149       'Type'        => 'text/html',
1150       'Encoding'    => 'quoted-printable',
1151       'Data'        => [ '<html>',
1152                          '  <head>',
1153                          '    <title>',
1154                          '      '. encode_entities($return{'subject'}), 
1155                          '    </title>',
1156                          '  </head>',
1157                          '  <body bgcolor="#e8e8e8">',
1158                          $htmldata,
1159                          '  </body>',
1160                          '</html>',
1161                        ],
1162       'Disposition' => 'inline',
1163       #'Filename'    => 'invoice.pdf',
1164     );
1165
1166
1167     my @otherparts = ();
1168     if ( $cust_main->email_csv_cdr ) {
1169
1170       push @otherparts, build MIME::Entity
1171         'Type'        => 'text/csv',
1172         'Encoding'    => '7bit',
1173         'Data'        => [ map { "$_\n" }
1174                              $self->call_details('prepend_billed_number' => 1)
1175                          ],
1176         'Disposition' => 'attachment',
1177         'Filename'    => 'usage-'. $self->invnum. '.csv',
1178       ;
1179
1180     }
1181
1182     if ( $conf->exists('invoice_email_pdf') ) {
1183
1184       #attaching pdf too:
1185       # multipart/mixed
1186       #   multipart/related
1187       #     multipart/alternative
1188       #       text/plain
1189       #       text/html
1190       #     image/png
1191       #   application/pdf
1192
1193       my $related = build MIME::Entity 'Type'     => 'multipart/related',
1194                                        'Encoding' => '7bit';
1195
1196       #false laziness w/Misc::send_email
1197       $related->head->replace('Content-type',
1198         $related->mime_type.
1199         '; boundary="'. $related->head->multipart_boundary. '"'.
1200         '; type=multipart/alternative'
1201       );
1202
1203       $related->add_part($alternative);
1204
1205       $related->add_part($image) if $image;
1206
1207       my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1208
1209       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1210
1211     } else {
1212
1213       #no other attachment:
1214       # multipart/related
1215       #   multipart/alternative
1216       #     text/plain
1217       #     text/html
1218       #   image/png
1219
1220       $return{'content-type'} = 'multipart/related';
1221       if ($conf->exists('invoice-barcode') && $barcode) {
1222         $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1223       } else {
1224         $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1225       }
1226       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1227       #$return{'disposition'} = 'inline';
1228
1229     }
1230   
1231   } else {
1232
1233     if ( $conf->exists('invoice_email_pdf') ) {
1234       warn "$me creating PDF attachment"
1235         if $DEBUG;
1236
1237       #mime parts arguments a la MIME::Entity->build().
1238       $return{'mimeparts'} = [
1239         { $self->mimebuild_pdf(\%opt) }
1240       ];
1241     }
1242   
1243     if ( $conf->exists('invoice_email_pdf')
1244          and scalar($conf->config('invoice_email_pdf_note')) ) {
1245
1246       warn "$me using 'invoice_email_pdf_note'"
1247         if $DEBUG;
1248       $return{'body'} = [ map { $_ . "\n" }
1249                               $conf->config('invoice_email_pdf_note')
1250                         ];
1251
1252     } else {
1253
1254       warn "$me not using 'invoice_email_pdf_note'"
1255         if $DEBUG;
1256       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1257         $return{'body'} = $args{'print_text'};
1258       } else {
1259         $return{'body'} = [ $self->print_text(\%opt) ];
1260       }
1261
1262     }
1263
1264   }
1265
1266   %return;
1267
1268 }
1269
1270 =item mimebuild_pdf
1271
1272 Returns a list suitable for passing to MIME::Entity->build(), representing
1273 this invoice as PDF attachment.
1274
1275 =cut
1276
1277 sub mimebuild_pdf {
1278   my $self = shift;
1279   (
1280     'Type'        => 'application/pdf',
1281     'Encoding'    => 'base64',
1282     'Data'        => [ $self->print_pdf(@_) ],
1283     'Disposition' => 'attachment',
1284     'Filename'    => 'invoice-'. $self->invnum. '.pdf',
1285   );
1286 }
1287
1288 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1289
1290 Sends this invoice to the destinations configured for this customer: sends
1291 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
1292
1293 Options can be passed as a hashref (recommended) or as a list of up to 
1294 four values for templatename, agentnum, invoice_from and amount.
1295
1296 I<template>, if specified, is the name of a suffix for alternate invoices.
1297
1298 I<agentnum>, if specified, means that this invoice will only be sent for customers
1299 of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
1300 single agent) or an arrayref of agentnums.
1301
1302 I<invoice_from>, if specified, overrides the default email invoice From: address.
1303
1304 I<amount>, if specified, only sends the invoice if the total amount owed on this
1305 invoice and all older invoices is greater than the specified amount.
1306
1307 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1308
1309 =cut
1310
1311 sub queueable_send {
1312   my %opt = @_;
1313
1314   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1315     or die "invalid invoice number: " . $opt{invnum};
1316
1317   my @args = ( $opt{template}, $opt{agentnum} );
1318   push @args, $opt{invoice_from}
1319     if exists($opt{invoice_from}) && $opt{invoice_from};
1320
1321   my $error = $self->send( @args );
1322   die $error if $error;
1323
1324 }
1325
1326 sub send {
1327   my $self = shift;
1328   my $conf = $self->conf;
1329
1330   my( $template, $invoice_from, $notice_name );
1331   my $agentnums = '';
1332   my $balance_over = 0;
1333
1334   if ( ref($_[0]) ) {
1335     my $opt = shift;
1336     $template = $opt->{'template'} || '';
1337     if ( $agentnums = $opt->{'agentnum'} ) {
1338       $agentnums = [ $agentnums ] unless ref($agentnums);
1339     }
1340     $invoice_from = $opt->{'invoice_from'};
1341     $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1342     $notice_name = $opt->{'notice_name'};
1343   } else {
1344     $template = scalar(@_) ? shift : '';
1345     if ( scalar(@_) && $_[0]  ) {
1346       $agentnums = ref($_[0]) ? shift : [ shift ];
1347     }
1348     $invoice_from = shift if scalar(@_);
1349     $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1350   }
1351
1352   my $cust_main = $self->cust_main;
1353
1354   return 'N/A' unless ! $agentnums
1355                    or grep { $_ == $cust_main->agentnum } @$agentnums;
1356
1357   return ''
1358     unless $cust_main->total_owed_date($self->_date) > $balance_over;
1359
1360   $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
1361                     $conf->config('invoice_from', $cust_main->agentnum );
1362
1363   my %opt = (
1364     'template'     => $template,
1365     'invoice_from' => $invoice_from,
1366     'notice_name'  => ( $notice_name || 'Invoice' ),
1367   );
1368
1369   my @invoicing_list = $cust_main->invoicing_list;
1370
1371   #$self->email_invoice(\%opt)
1372   $self->email(\%opt)
1373     if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1374     && ! $self->invoice_noemail;
1375
1376   #$self->print_invoice(\%opt)
1377   $self->print(\%opt)
1378     if grep { $_ eq 'POST' } @invoicing_list; #postal
1379
1380   $self->fax_invoice(\%opt)
1381     if grep { $_ eq 'FAX' } @invoicing_list; #fax
1382
1383   '';
1384
1385 }
1386
1387 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ] 
1388
1389 Emails this invoice.
1390
1391 Options can be passed as a hashref (recommended) or as a list of up to 
1392 two values for templatename and invoice_from.
1393
1394 I<template>, if specified, is the name of a suffix for alternate invoices.
1395
1396 I<invoice_from>, if specified, overrides the default email invoice From: address.
1397
1398 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1399
1400 =cut
1401
1402 sub queueable_email {
1403   my %opt = @_;
1404
1405   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1406     or die "invalid invoice number: " . $opt{invnum};
1407
1408   my %args = ( 'template' => $opt{template} );
1409   $args{$_} = $opt{$_}
1410     foreach grep { exists($opt{$_}) && $opt{$_} }
1411               qw( invoice_from notice_name no_coupon );
1412
1413   my $error = $self->email( \%args );
1414   die $error if $error;
1415
1416 }
1417
1418 #sub email_invoice {
1419 sub email {
1420   my $self = shift;
1421   return if $self->hide;
1422   my $conf = $self->conf;
1423
1424   my( $template, $invoice_from, $notice_name, $no_coupon );
1425   if ( ref($_[0]) ) {
1426     my $opt = shift;
1427     $template = $opt->{'template'} || '';
1428     $invoice_from = $opt->{'invoice_from'};
1429     $notice_name = $opt->{'notice_name'} || 'Invoice';
1430     $no_coupon = $opt->{'no_coupon'} || 0;
1431   } else {
1432     $template = scalar(@_) ? shift : '';
1433     $invoice_from = shift if scalar(@_);
1434     $notice_name = 'Invoice';
1435     $no_coupon = 0;
1436   }
1437
1438   $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
1439                     $conf->config('invoice_from', $self->cust_main->agentnum );
1440
1441   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
1442                             $self->cust_main->invoicing_list;
1443
1444   if ( ! @invoicing_list ) { #no recipients
1445     if ( $conf->exists('cust_bill-no_recipients-error') ) {
1446       die 'No recipients for customer #'. $self->custnum;
1447     } else {
1448       #default: better to notify this person than silence
1449       @invoicing_list = ($invoice_from);
1450     }
1451   }
1452
1453   my $subject = $self->email_subject($template);
1454
1455   my $error = send_email(
1456     $self->generate_email(
1457       'from'        => $invoice_from,
1458       'to'          => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1459       'subject'     => $subject,
1460       'template'    => $template,
1461       'notice_name' => $notice_name,
1462       'no_coupon'   => $no_coupon,
1463     )
1464   );
1465   die "can't email invoice: $error\n" if $error;
1466   #die "$error\n" if $error;
1467
1468 }
1469
1470 sub email_subject {
1471   my $self = shift;
1472   my $conf = $self->conf;
1473
1474   #my $template = scalar(@_) ? shift : '';
1475   #per-template?
1476
1477   my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1478                 || 'Invoice';
1479
1480   my $cust_main = $self->cust_main;
1481   my $name = $cust_main->name;
1482   my $name_short = $cust_main->name_short;
1483   my $invoice_number = $self->invnum;
1484   my $invoice_date = $self->_date_pretty;
1485
1486   eval qq("$subject");
1487 }
1488
1489 =item lpr_data HASHREF | [ TEMPLATE ]
1490
1491 Returns the postscript or plaintext for this invoice as an arrayref.
1492
1493 Options can be passed as a hashref (recommended) or as a single optional value
1494 for template.
1495
1496 I<template>, if specified, is the name of a suffix for alternate invoices.
1497
1498 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1499
1500 =cut
1501
1502 sub lpr_data {
1503   my $self = shift;
1504   my $conf = $self->conf;
1505   my( $template, $notice_name );
1506   if ( ref($_[0]) ) {
1507     my $opt = shift;
1508     $template = $opt->{'template'} || '';
1509     $notice_name = $opt->{'notice_name'} || 'Invoice';
1510   } else {
1511     $template = scalar(@_) ? shift : '';
1512     $notice_name = 'Invoice';
1513   }
1514
1515   my %opt = (
1516     'template'    => $template,
1517     'notice_name' => $notice_name,
1518   );
1519
1520   my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1521   [ $self->$method( \%opt ) ];
1522 }
1523
1524 =item print HASHREF | [ TEMPLATE ]
1525
1526 Prints this invoice.
1527
1528 Options can be passed as a hashref (recommended) or as a single optional
1529 value for template.
1530
1531 I<template>, if specified, is the name of a suffix for alternate invoices.
1532
1533 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1534
1535 =cut
1536
1537 #sub print_invoice {
1538 sub print {
1539   my $self = shift;
1540   return if $self->hide;
1541   my $conf = $self->conf;
1542
1543   my( $template, $notice_name );
1544   if ( ref($_[0]) ) {
1545     my $opt = shift;
1546     $template = $opt->{'template'} || '';
1547     $notice_name = $opt->{'notice_name'} || 'Invoice';
1548   } else {
1549     $template = scalar(@_) ? shift : '';
1550     $notice_name = 'Invoice';
1551   }
1552
1553   my %opt = (
1554     'template'    => $template,
1555     'notice_name' => $notice_name,
1556   );
1557
1558   if($conf->exists('invoice_print_pdf')) {
1559     # Add the invoice to the current batch.
1560     $self->batch_invoice(\%opt);
1561   }
1562   else {
1563     do_print(
1564       $self->lpr_data(\%opt),
1565       'agentnum' => $self->cust_main->agentnum,
1566     );
1567   }
1568 }
1569
1570 =item fax_invoice HASHREF | [ TEMPLATE ] 
1571
1572 Faxes this invoice.
1573
1574 Options can be passed as a hashref (recommended) or as a single optional
1575 value for template.
1576
1577 I<template>, if specified, is the name of a suffix for alternate invoices.
1578
1579 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1580
1581 =cut
1582
1583 sub fax_invoice {
1584   my $self = shift;
1585   return if $self->hide;
1586   my $conf = $self->conf;
1587
1588   my( $template, $notice_name );
1589   if ( ref($_[0]) ) {
1590     my $opt = shift;
1591     $template = $opt->{'template'} || '';
1592     $notice_name = $opt->{'notice_name'} || 'Invoice';
1593   } else {
1594     $template = scalar(@_) ? shift : '';
1595     $notice_name = 'Invoice';
1596   }
1597
1598   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1599     unless $conf->exists('invoice_latex');
1600
1601   my $dialstring = $self->cust_main->getfield('fax');
1602   #Check $dialstring?
1603
1604   my %opt = (
1605     'template'    => $template,
1606     'notice_name' => $notice_name,
1607   );
1608
1609   my $error = send_fax( 'docdata'    => $self->lpr_data(\%opt),
1610                         'dialstring' => $dialstring,
1611                       );
1612   die $error if $error;
1613
1614 }
1615
1616 =item batch_invoice [ HASHREF ]
1617
1618 Place this invoice into the open batch (see C<FS::bill_batch>).  If there 
1619 isn't an open batch, one will be created.
1620
1621 =cut
1622
1623 sub batch_invoice {
1624   my ($self, $opt) = @_;
1625   my $bill_batch = $self->get_open_bill_batch;
1626   my $cust_bill_batch = FS::cust_bill_batch->new({
1627       batchnum => $bill_batch->batchnum,
1628       invnum   => $self->invnum,
1629   });
1630   return $cust_bill_batch->insert($opt);
1631 }
1632
1633 =item get_open_batch
1634
1635 Returns the currently open batch as an FS::bill_batch object, creating a new
1636 one if necessary.  (A per-agent batch if invoice_print_pdf-spoolagent is
1637 enabled)
1638
1639 =cut
1640
1641 sub get_open_bill_batch {
1642   my $self = shift;
1643   my $conf = $self->conf;
1644   my $hashref = { status => 'O' };
1645   $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1646                              ? $self->cust_main->agentnum
1647                              : '';
1648   my $batch = qsearchs('bill_batch', $hashref);
1649   return $batch if $batch;
1650   $batch = FS::bill_batch->new($hashref);
1651   my $error = $batch->insert;
1652   die $error if $error;
1653   return $batch;
1654 }
1655
1656 =item ftp_invoice [ TEMPLATENAME ] 
1657
1658 Sends this invoice data via FTP.
1659
1660 TEMPLATENAME is unused?
1661
1662 =cut
1663
1664 sub ftp_invoice {
1665   my $self = shift;
1666   my $conf = $self->conf;
1667   my $template = scalar(@_) ? shift : '';
1668
1669   $self->send_csv(
1670     'protocol'   => 'ftp',
1671     'server'     => $conf->config('cust_bill-ftpserver'),
1672     'username'   => $conf->config('cust_bill-ftpusername'),
1673     'password'   => $conf->config('cust_bill-ftppassword'),
1674     'dir'        => $conf->config('cust_bill-ftpdir'),
1675     'format'     => $conf->config('cust_bill-ftpformat'),
1676   );
1677 }
1678
1679 =item spool_invoice [ TEMPLATENAME ] 
1680
1681 Spools this invoice data (see L<FS::spool_csv>)
1682
1683 TEMPLATENAME is unused?
1684
1685 =cut
1686
1687 sub spool_invoice {
1688   my $self = shift;
1689   my $conf = $self->conf;
1690   my $template = scalar(@_) ? shift : '';
1691
1692   $self->spool_csv(
1693     'format'       => $conf->config('cust_bill-spoolformat'),
1694     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1695   );
1696 }
1697
1698 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1699
1700 Like B<send>, but only sends the invoice if it is the newest open invoice for
1701 this customer.
1702
1703 =cut
1704
1705 sub send_if_newest {
1706   my $self = shift;
1707
1708   return ''
1709     if scalar(
1710                grep { $_->owed > 0 } 
1711                     qsearch('cust_bill', {
1712                       'custnum' => $self->custnum,
1713                       #'_date'   => { op=>'>', value=>$self->_date },
1714                       'invnum'  => { op=>'>', value=>$self->invnum },
1715                     } )
1716              );
1717     
1718   $self->send(@_);
1719 }
1720
1721 =item send_csv OPTION => VALUE, ...
1722
1723 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1724
1725 Options are:
1726
1727 protocol - currently only "ftp"
1728 server
1729 username
1730 password
1731 dir
1732
1733 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1734 and YYMMDDHHMMSS is a timestamp.
1735
1736 See L</print_csv> for a description of the output format.
1737
1738 =cut
1739
1740 sub send_csv {
1741   my($self, %opt) = @_;
1742
1743   #create file(s)
1744
1745   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1746   mkdir $spooldir, 0700 unless -d $spooldir;
1747
1748   # don't localize dates here, they're a defined format
1749   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1750   my $file = "$spooldir/$tracctnum.csv";
1751   
1752   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1753
1754   open(CSV, ">$file") or die "can't open $file: $!";
1755   print CSV $header;
1756
1757   print CSV $detail;
1758
1759   close CSV;
1760
1761   my $net;
1762   if ( $opt{protocol} eq 'ftp' ) {
1763     eval "use Net::FTP;";
1764     die $@ if $@;
1765     $net = Net::FTP->new($opt{server}) or die @$;
1766   } else {
1767     die "unknown protocol: $opt{protocol}";
1768   }
1769
1770   $net->login( $opt{username}, $opt{password} )
1771     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1772
1773   $net->binary or die "can't set binary mode";
1774
1775   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1776
1777   $net->put($file) or die "can't put $file: $!";
1778
1779   $net->quit;
1780
1781   unlink $file;
1782
1783 }
1784
1785 =item spool_csv
1786
1787 Spools CSV invoice data.
1788
1789 Options are:
1790
1791 =over 4
1792
1793 =item format - 'default' or 'billco'
1794
1795 =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>).
1796
1797 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1798
1799 =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.
1800
1801 =back
1802
1803 =cut
1804
1805 sub spool_csv {
1806   my($self, %opt) = @_;
1807
1808   my $cust_main = $self->cust_main;
1809
1810   if ( $opt{'dest'} ) {
1811     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1812                              $cust_main->invoicing_list;
1813     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1814                      || ! keys %invoicing_list;
1815   }
1816
1817   if ( $opt{'balanceover'} ) {
1818     return 'N/A'
1819       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1820   }
1821
1822   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1823   mkdir $spooldir, 0700 unless -d $spooldir;
1824
1825   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1826
1827   my $file =
1828     "$spooldir/".
1829     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1830     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1831     '.csv';
1832   
1833   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1834
1835   open(CSV, ">>$file") or die "can't open $file: $!";
1836   flock(CSV, LOCK_EX);
1837   seek(CSV, 0, 2);
1838
1839   print CSV $header;
1840
1841   if ( lc($opt{'format'}) eq 'billco' ) {
1842
1843     flock(CSV, LOCK_UN);
1844     close CSV;
1845
1846     $file =
1847       "$spooldir/".
1848       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1849       '-detail.csv';
1850
1851     open(CSV,">>$file") or die "can't open $file: $!";
1852     flock(CSV, LOCK_EX);
1853     seek(CSV, 0, 2);
1854   }
1855
1856   print CSV $detail;
1857
1858   flock(CSV, LOCK_UN);
1859   close CSV;
1860
1861   return '';
1862
1863 }
1864
1865 =item print_csv OPTION => VALUE, ...
1866
1867 Returns CSV data for this invoice.
1868
1869 Options are:
1870
1871 format - 'default' or 'billco'
1872
1873 Returns a list consisting of two scalars.  The first is a single line of CSV
1874 header information for this invoice.  The second is one or more lines of CSV
1875 detail information for this invoice.
1876
1877 If I<format> is not specified or "default", the fields of the CSV file are as
1878 follows:
1879
1880 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1881
1882 =over 4
1883
1884 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1885
1886 B<record_type> is C<cust_bill> for the initial header line only.  The
1887 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1888 fields are filled in.
1889
1890 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1891 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1892 are filled in.
1893
1894 =item invnum - invoice number
1895
1896 =item custnum - customer number
1897
1898 =item _date - invoice date
1899
1900 =item charged - total invoice amount
1901
1902 =item first - customer first name
1903
1904 =item last - customer first name
1905
1906 =item company - company name
1907
1908 =item address1 - address line 1
1909
1910 =item address2 - address line 1
1911
1912 =item city
1913
1914 =item state
1915
1916 =item zip
1917
1918 =item country
1919
1920 =item pkg - line item description
1921
1922 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1923
1924 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1925
1926 =item sdate - start date for recurring fee
1927
1928 =item edate - end date for recurring fee
1929
1930 =back
1931
1932 If I<format> is "billco", the fields of the header CSV file are as follows:
1933
1934   +-------------------------------------------------------------------+
1935   |                        FORMAT HEADER FILE                         |
1936   |-------------------------------------------------------------------|
1937   | Field | Description                   | Name       | Type | Width |
1938   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1939   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1940   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1941   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1942   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1943   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1944   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1945   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1946   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1947   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1948   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1949   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1950   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1951   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1952   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1953   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1954   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1955   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1956   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1957   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1958   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1959   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1960   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1961   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1962   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1963   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1964   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1965   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1966   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1967   +-------+-------------------------------+------------+------+-------+
1968
1969 If I<format> is "billco", the fields of the detail CSV file are as follows:
1970
1971                                   FORMAT FOR DETAIL FILE
1972         |                            |           |      |
1973   Field | Description                | Name      | Type | Width
1974   1     | N/A-Leave Empty            | RC        | CHAR |     2
1975   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1976   3     | Account Number             | TRACCTNUM | CHAR |    15
1977   4     | Invoice Number             | TRINVOICE | CHAR |    15
1978   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1979   6     | Transaction Detail         | DETAILS   | CHAR |   100
1980   7     | Amount                     | AMT       | NUM* |     9
1981   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1982   9     | Grouping Code              | GROUP     | CHAR |     2
1983   10    | User Defined               | ACCT CODE | CHAR |    15
1984
1985 =cut
1986
1987 sub print_csv {
1988   my($self, %opt) = @_;
1989   
1990   eval "use Text::CSV_XS";
1991   die $@ if $@;
1992
1993   my $cust_main = $self->cust_main;
1994
1995   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1996
1997   if ( lc($opt{'format'}) eq 'billco' ) {
1998
1999     my $taxtotal = 0;
2000     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
2001
2002     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
2003
2004     my( $previous_balance, @unused ) = $self->previous; #previous balance
2005
2006     my $pmt_cr_applied = 0;
2007     $pmt_cr_applied += $_->{'amount'}
2008       foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
2009
2010     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2011
2012     $csv->combine(
2013       '',                         #  1 | N/A-Leave Empty               CHAR   2
2014       '',                         #  2 | N/A-Leave Empty               CHAR  15
2015       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
2016       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
2017       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
2018       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
2019       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
2020       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
2021       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
2022       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
2023       '',                         # 10 | Ancillary Billing Information CHAR  30
2024       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
2025       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
2026
2027       # XXX ?
2028       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
2029
2030       # XXX ?
2031       $duedate,                   # 14 | Bill Due Date                 CHAR  10
2032
2033       $previous_balance,          # 15 | Previous Balance              NUM*   9
2034       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
2035       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
2036       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
2037       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
2038       '',                         # 20 | 30 Day Aging                  NUM*   9
2039       '',                         # 21 | 60 Day Aging                  NUM*   9
2040       '',                         # 22 | 90 Day Aging                  NUM*   9
2041       'N',                        # 23 | Y/N                           CHAR   1
2042       '',                         # 24 | Remittance automation         CHAR 100
2043       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
2044       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
2045       '0',                        # 27 | Federal Tax***                NUM*   9
2046       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
2047       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
2048     );
2049
2050   } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2051   
2052     my ($previous_balance) = $self->previous; 
2053     $previous_balance = sprintf('%.2f', $previous_balance);
2054     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2055     my @items = map {
2056                       $_->{pkgnum},
2057                       $_->{description},
2058                       $_->{amount}
2059                     }
2060                   $self->_items_pkg, #_items_nontax?  no sections or anything
2061                                      # with this format
2062                   $self->_items_tax;
2063
2064     $csv->combine(
2065       $cust_main->agentnum,
2066       $cust_main->agent->agent,
2067       $self->custnum,
2068       $cust_main->first,
2069       $cust_main->last,
2070       $cust_main->company,
2071       $cust_main->address1,
2072       $cust_main->address2,
2073       $cust_main->city,
2074       $cust_main->state,
2075       $cust_main->zip,
2076
2077       # invoice fields
2078       time2str("%x", $self->_date),
2079       $self->invnum,
2080       $self->charged,
2081       $totaldue,
2082       $previous_balance,
2083       $self->due_date2str("%x"),
2084
2085       @items,
2086     );
2087
2088   } else {
2089   
2090     $csv->combine(
2091       'cust_bill',
2092       $self->invnum,
2093       $self->custnum,
2094       time2str("%x", $self->_date),
2095       sprintf("%.2f", $self->charged),
2096       ( map { $cust_main->getfield($_) }
2097           qw( first last company address1 address2 city state zip country ) ),
2098       map { '' } (1..5),
2099     ) or die "can't create csv";
2100   }
2101
2102   my $header = $csv->string. "\n";
2103
2104   my $detail = '';
2105   if ( lc($opt{'format'}) eq 'billco' ) {
2106
2107     my $lineseq = 0;
2108     foreach my $item ( $self->_items_pkg ) {
2109
2110       $csv->combine(
2111         '',                     #  1 | N/A-Leave Empty            CHAR   2
2112         '',                     #  2 | N/A-Leave Empty            CHAR  15
2113         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
2114         $self->invnum,          #  4 | Invoice Number             CHAR  15
2115         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
2116         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
2117         $item->{'amount'},      #  7 | Amount                     NUM*   9
2118         '',                     #  8 | Line Format Control**      CHAR   2
2119         '',                     #  9 | Grouping Code              CHAR   2
2120         '',                     # 10 | User Defined               CHAR  15
2121       );
2122
2123       $detail .= $csv->string. "\n";
2124
2125     }
2126
2127   } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2128
2129     #do nothing
2130
2131   } else {
2132
2133     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2134
2135       my($pkg, $setup, $recur, $sdate, $edate);
2136       if ( $cust_bill_pkg->pkgnum ) {
2137       
2138         ($pkg, $setup, $recur, $sdate, $edate) = (
2139           $cust_bill_pkg->part_pkg->pkg,
2140           ( $cust_bill_pkg->setup != 0
2141             ? sprintf("%.2f", $cust_bill_pkg->setup )
2142             : '' ),
2143           ( $cust_bill_pkg->recur != 0
2144             ? sprintf("%.2f", $cust_bill_pkg->recur )
2145             : '' ),
2146           ( $cust_bill_pkg->sdate 
2147             ? time2str("%x", $cust_bill_pkg->sdate)
2148             : '' ),
2149           ($cust_bill_pkg->edate 
2150             ? time2str("%x", $cust_bill_pkg->edate)
2151             : '' ),
2152         );
2153   
2154       } else { #pkgnum tax
2155         next unless $cust_bill_pkg->setup != 0;
2156         $pkg = $cust_bill_pkg->desc;
2157         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2158         ( $sdate, $edate ) = ( '', '' );
2159       }
2160   
2161       $csv->combine(
2162         'cust_bill_pkg',
2163         $self->invnum,
2164         ( map { '' } (1..11) ),
2165         ($pkg, $setup, $recur, $sdate, $edate)
2166       ) or die "can't create csv";
2167
2168       $detail .= $csv->string. "\n";
2169
2170     }
2171
2172   }
2173
2174   ( $header, $detail );
2175
2176 }
2177
2178 =item comp
2179
2180 Pays this invoice with a compliemntary payment.  If there is an error,
2181 returns the error, otherwise returns false.
2182
2183 =cut
2184
2185 sub comp {
2186   my $self = shift;
2187   my $cust_pay = new FS::cust_pay ( {
2188     'invnum'   => $self->invnum,
2189     'paid'     => $self->owed,
2190     '_date'    => '',
2191     'payby'    => 'COMP',
2192     'payinfo'  => $self->cust_main->payinfo,
2193     'paybatch' => '',
2194   } );
2195   $cust_pay->insert;
2196 }
2197
2198 =item realtime_card
2199
2200 Attempts to pay this invoice with a credit card payment via a
2201 Business::OnlinePayment realtime gateway.  See
2202 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2203 for supported processors.
2204
2205 =cut
2206
2207 sub realtime_card {
2208   my $self = shift;
2209   $self->realtime_bop( 'CC', @_ );
2210 }
2211
2212 =item realtime_ach
2213
2214 Attempts to pay this invoice with an electronic check (ACH) payment via a
2215 Business::OnlinePayment realtime gateway.  See
2216 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2217 for supported processors.
2218
2219 =cut
2220
2221 sub realtime_ach {
2222   my $self = shift;
2223   $self->realtime_bop( 'ECHECK', @_ );
2224 }
2225
2226 =item realtime_lec
2227
2228 Attempts to pay this invoice with phone bill (LEC) payment via a
2229 Business::OnlinePayment realtime gateway.  See
2230 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2231 for supported processors.
2232
2233 =cut
2234
2235 sub realtime_lec {
2236   my $self = shift;
2237   $self->realtime_bop( 'LEC', @_ );
2238 }
2239
2240 sub realtime_bop {
2241   my( $self, $method ) = (shift,shift);
2242   my $conf = $self->conf;
2243   my %opt = @_;
2244
2245   my $cust_main = $self->cust_main;
2246   my $balance = $cust_main->balance;
2247   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2248   $amount = sprintf("%.2f", $amount);
2249   return "not run (balance $balance)" unless $amount > 0;
2250
2251   my $description = 'Internet Services';
2252   if ( $conf->exists('business-onlinepayment-description') ) {
2253     my $dtempl = $conf->config('business-onlinepayment-description');
2254
2255     my $agent_obj = $cust_main->agent
2256       or die "can't retreive agent for $cust_main (agentnum ".
2257              $cust_main->agentnum. ")";
2258     my $agent = $agent_obj->agent;
2259     my $pkgs = join(', ',
2260       map { $_->part_pkg->pkg }
2261         grep { $_->pkgnum } $self->cust_bill_pkg
2262     );
2263     $description = eval qq("$dtempl");
2264   }
2265
2266   $cust_main->realtime_bop($method, $amount,
2267     'description' => $description,
2268     'invnum'      => $self->invnum,
2269 #this didn't do what we want, it just calls apply_payments_and_credits
2270 #    'apply'       => 1,
2271     'apply_to_invoice' => 1,
2272     %opt,
2273  #what we want:
2274  #this changes application behavior: auto payments
2275                         #triggered against a specific invoice are now applied
2276                         #to that invoice instead of oldest open.
2277                         #seem okay to me...
2278   );
2279
2280 }
2281
2282 =item batch_card OPTION => VALUE...
2283
2284 Adds a payment for this invoice to the pending credit card batch (see
2285 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2286 runs the payment using a realtime gateway.
2287
2288 =cut
2289
2290 sub batch_card {
2291   my ($self, %options) = @_;
2292   my $cust_main = $self->cust_main;
2293
2294   $options{invnum} = $self->invnum;
2295   
2296   $cust_main->batch_card(%options);
2297 }
2298
2299 sub _agent_template {
2300   my $self = shift;
2301   $self->cust_main->agent_template;
2302 }
2303
2304 sub _agent_invoice_from {
2305   my $self = shift;
2306   $self->cust_main->agent_invoice_from;
2307 }
2308
2309 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2310
2311 Returns an text invoice, as a list of lines.
2312
2313 Options can be passed as a hashref (recommended) or as a list of time, template
2314 and then any key/value pairs for any other options.
2315
2316 I<time>, if specified, is used to control the printing of overdue messages.  The
2317 default is now.  It isn't the date of the invoice; that's the `_date' field.
2318 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2319 L<Time::Local> and L<Date::Parse> for conversion functions.
2320
2321 I<template>, if specified, is the name of a suffix for alternate invoices.
2322
2323 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2324
2325 =cut
2326
2327 sub print_text {
2328   my $self = shift;
2329   my( $today, $template, %opt );
2330   if ( ref($_[0]) ) {
2331     %opt = %{ shift() };
2332     $today = delete($opt{'time'}) || '';
2333     $template = delete($opt{template}) || '';
2334   } else {
2335     ( $today, $template, %opt ) = @_;
2336   }
2337
2338   my %params = ( 'format' => 'template' );
2339   $params{'time'} = $today if $today;
2340   $params{'template'} = $template if $template;
2341   $params{$_} = $opt{$_} 
2342     foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2343
2344   $self->print_generic( %params );
2345 }
2346
2347 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2348
2349 Internal method - returns a filename of a filled-in LaTeX template for this
2350 invoice (Note: add ".tex" to get the actual filename), and a filename of
2351 an associated logo (with the .eps extension included).
2352
2353 See print_ps and print_pdf for methods that return PostScript and PDF output.
2354
2355 Options can be passed as a hashref (recommended) or as a list of time, template
2356 and then any key/value pairs for any other options.
2357
2358 I<time>, if specified, is used to control the printing of overdue messages.  The
2359 default is now.  It isn't the date of the invoice; that's the `_date' field.
2360 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2361 L<Time::Local> and L<Date::Parse> for conversion functions.
2362
2363 I<template>, if specified, is the name of a suffix for alternate invoices.
2364
2365 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2366
2367 =cut
2368
2369 sub print_latex {
2370   my $self = shift;
2371   my $conf = $self->conf;
2372   my( $today, $template, %opt );
2373   if ( ref($_[0]) ) {
2374     %opt = %{ shift() };
2375     $today = delete($opt{'time'}) || '';
2376     $template = delete($opt{template}) || '';
2377   } else {
2378     ( $today, $template, %opt ) = @_;
2379   }
2380
2381   my %params = ( 'format' => 'latex' );
2382   $params{'time'} = $today if $today;
2383   $params{'template'} = $template if $template;
2384   $params{$_} = $opt{$_} 
2385     foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2386
2387   $template ||= $self->_agent_template;
2388
2389   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2390   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2391                            DIR      => $dir,
2392                            SUFFIX   => '.eps',
2393                            UNLINK   => 0,
2394                          ) or die "can't open temp file: $!\n";
2395
2396   my $agentnum = $self->cust_main->agentnum;
2397
2398   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2399     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2400       or die "can't write temp file: $!\n";
2401   } else {
2402     print $lh $conf->config_binary('logo.eps', $agentnum)
2403       or die "can't write temp file: $!\n";
2404   }
2405   close $lh;
2406   $params{'logo_file'} = $lh->filename;
2407
2408   if($conf->exists('invoice-barcode')){
2409       my $png_file = $self->invoice_barcode($dir);
2410       my $eps_file = $png_file;
2411       $eps_file =~ s/\.png$/.eps/g;
2412       $png_file =~ /(barcode.*png)/;
2413       $png_file = $1;
2414       $eps_file =~ /(barcode.*eps)/;
2415       $eps_file = $1;
2416
2417       my $curr_dir = cwd();
2418       chdir($dir); 
2419       # after painfuly long experimentation, it was determined that sam2p won't
2420       # accept : and other chars in the path, no matter how hard I tried to
2421       # escape them, hence the chdir (and chdir back, just to be safe)
2422       system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2423         or die "sam2p failed: $!\n";
2424       unlink($png_file);
2425       chdir($curr_dir);
2426
2427       $params{'barcode_file'} = $eps_file;
2428   }
2429
2430   my @filled_in = $self->print_generic( %params );
2431   
2432   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2433                            DIR      => $dir,
2434                            SUFFIX   => '.tex',
2435                            UNLINK   => 0,
2436                          ) or die "can't open temp file: $!\n";
2437   binmode($fh, ':utf8'); # language support
2438   print $fh join('', @filled_in );
2439   close $fh;
2440
2441   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2442   return ($1, $params{'logo_file'}, $params{'barcode_file'});
2443
2444 }
2445
2446 =item invoice_barcode DIR_OR_FALSE
2447
2448 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2449 it is taken as the temp directory where the PNG file will be generated and the
2450 PNG file name is returned. Otherwise, the PNG image itself is returned.
2451
2452 =cut
2453
2454 sub invoice_barcode {
2455     my ($self, $dir) = (shift,shift);
2456     
2457     my $gdbar = new GD::Barcode('Code39',$self->invnum);
2458         die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2459     my $gd = $gdbar->plot(Height => 30);
2460
2461     if($dir) {
2462         my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2463                            DIR      => $dir,
2464                            SUFFIX   => '.png',
2465                            UNLINK   => 0,
2466                          ) or die "can't open temp file: $!\n";
2467         print $bh $gd->png or die "cannot write barcode to file: $!\n";
2468         my $png_file = $bh->filename;
2469         close $bh;
2470         return $png_file;
2471     }
2472     return $gd->png;
2473 }
2474
2475 =item print_generic OPTION => VALUE ...
2476
2477 Internal method - returns a filled-in template for this invoice as a scalar.
2478
2479 See print_ps and print_pdf for methods that return PostScript and PDF output.
2480
2481 Non optional options include 
2482   format - latex, html, template
2483
2484 Optional options include
2485
2486 template - a value used as a suffix for a configuration template
2487
2488 time - a value used to control the printing of overdue messages.  The
2489 default is now.  It isn't the date of the invoice; that's the `_date' field.
2490 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2491 L<Time::Local> and L<Date::Parse> for conversion functions.
2492
2493 cid - 
2494
2495 unsquelch_cdr - overrides any per customer cdr squelching when true
2496
2497 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2498
2499 locale - override customer's locale
2500
2501 =cut
2502
2503 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
2504 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2505 # yes: fixed width/plain text printing will be borked
2506 sub print_generic {
2507   my( $self, %params ) = @_;
2508   my $conf = $self->conf;
2509   my $today = $params{today} ? $params{today} : time;
2510   warn "$me print_generic called on $self with suffix $params{template}\n"
2511     if $DEBUG;
2512
2513   my $format = $params{format};
2514   die "Unknown format: $format"
2515     unless $format =~ /^(latex|html|template)$/;
2516
2517   my $cust_main = $self->cust_main;
2518   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2519     unless $cust_main->payname
2520         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2521
2522   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
2523                      'html'     => [ '<%=', '%>' ],
2524                      'template' => [ '{', '}' ],
2525                    );
2526
2527   warn "$me print_generic creating template\n"
2528     if $DEBUG > 1;
2529
2530   #create the template
2531   my $template = $params{template} ? $params{template} : $self->_agent_template;
2532   my $templatefile = "invoice_$format";
2533   $templatefile .= "_$template"
2534     if length($template) && $conf->exists($templatefile."_$template");
2535   my @invoice_template = map "$_\n", $conf->config($templatefile)
2536     or die "cannot load config data $templatefile";
2537
2538   my $old_latex = '';
2539   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2540     #change this to a die when the old code is removed
2541     warn "old-style invoice template $templatefile; ".
2542          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2543     $old_latex = 'true';
2544     @invoice_template = _translate_old_latex_format(@invoice_template);
2545   } 
2546
2547   warn "$me print_generic creating T:T object\n"
2548     if $DEBUG > 1;
2549
2550   my $text_template = new Text::Template(
2551     TYPE => 'ARRAY',
2552     SOURCE => \@invoice_template,
2553     DELIMITERS => $delimiters{$format},
2554   );
2555
2556   warn "$me print_generic compiling T:T object\n"
2557     if $DEBUG > 1;
2558
2559   $text_template->compile()
2560     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2561
2562
2563   # additional substitution could possibly cause breakage in existing templates
2564   my %convert_maps = ( 
2565     'latex' => {
2566                  'notes'         => sub { map "$_", @_ },
2567                  'footer'        => sub { map "$_", @_ },
2568                  'smallfooter'   => sub { map "$_", @_ },
2569                  'returnaddress' => sub { map "$_", @_ },
2570                  'coupon'        => sub { map "$_", @_ },
2571                  'summary'       => sub { map "$_", @_ },
2572                },
2573     'html'  => {
2574                  'notes' =>
2575                    sub {
2576                      map { 
2577                        s/%%(.*)$/<!-- $1 -->/g;
2578                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2579                        s/\\begin\{enumerate\}/<ol>/g;
2580                        s/\\item /  <li>/g;
2581                        s/\\end\{enumerate\}/<\/ol>/g;
2582                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2583                        s/\\\\\*/<br>/g;
2584                        s/\\dollar ?/\$/g;
2585                        s/\\#/#/g;
2586                        s/~/&nbsp;/g;
2587                        $_;
2588                      }  @_
2589                    },
2590                  'footer' =>
2591                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2592                  'smallfooter' =>
2593                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2594                  'returnaddress' =>
2595                    sub {
2596                      map { 
2597                        s/~/&nbsp;/g;
2598                        s/\\\\\*?\s*$/<BR>/;
2599                        s/\\hyphenation\{[\w\s\-]+}//;
2600                        s/\\([&])/$1/g;
2601                        $_;
2602                      }  @_
2603                    },
2604                  'coupon'        => sub { "" },
2605                  'summary'       => sub { "" },
2606                },
2607     'template' => {
2608                  'notes' =>
2609                    sub {
2610                      map { 
2611                        s/%%.*$//g;
2612                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2613                        s/\\begin\{enumerate\}//g;
2614                        s/\\item /  * /g;
2615                        s/\\end\{enumerate\}//g;
2616                        s/\\textbf\{(.*)\}/$1/g;
2617                        s/\\\\\*/ /;
2618                        s/\\dollar ?/\$/g;
2619                        $_;
2620                      }  @_
2621                    },
2622                  'footer' =>
2623                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2624                  'smallfooter' =>
2625                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2626                  'returnaddress' =>
2627                    sub {
2628                      map { 
2629                        s/~/ /g;
2630                        s/\\\\\*?\s*$/\n/;             # dubious
2631                        s/\\hyphenation\{[\w\s\-]+}//;
2632                        $_;
2633                      }  @_
2634                    },
2635                  'coupon'        => sub { "" },
2636                  'summary'       => sub { "" },
2637                },
2638   );
2639
2640
2641   # hashes for differing output formats
2642   my %nbsps = ( 'latex'    => '~',
2643                 'html'     => '',    # '&nbps;' would be nice
2644                 'template' => '',    # not used
2645               );
2646   my $nbsp = $nbsps{$format};
2647
2648   my %escape_functions = ( 'latex'    => \&_latex_escape,
2649                            'html'     => \&_html_escape_nbsp,#\&encode_entities,
2650                            'template' => sub { shift },
2651                          );
2652   my $escape_function = $escape_functions{$format};
2653   my $escape_function_nonbsp = ($format eq 'html')
2654                                  ? \&_html_escape : $escape_function;
2655
2656   my %date_formats = ( 'latex'    => $date_format_long,
2657                        'html'     => $date_format_long,
2658                        'template' => '%s',
2659                      );
2660   $date_formats{'html'} =~ s/ /&nbsp;/g;
2661
2662   my $date_format = $date_formats{$format};
2663
2664   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
2665                                                },
2666                              'html'     => sub { return '<b>'. shift(). '</b>'
2667                                                },
2668                              'template' => sub { shift },
2669                            );
2670   my $embolden_function = $embolden_functions{$format};
2671
2672   my %newline_tokens = (  'latex'     => '\\\\',
2673                           'html'      => '<br>',
2674                           'template'  => "\n",
2675                         );
2676   my $newline_token = $newline_tokens{$format};
2677
2678   warn "$me generating template variables\n"
2679     if $DEBUG > 1;
2680
2681   # generate template variables
2682   my $returnaddress;
2683   if (
2684          defined( $conf->config_orbase( "invoice_${format}returnaddress",
2685                                         $template
2686                                       )
2687                 )
2688        && length( $conf->config_orbase( "invoice_${format}returnaddress",
2689                                         $template
2690                                       )
2691                 )
2692   ) {
2693
2694     $returnaddress = join("\n",
2695       $conf->config_orbase("invoice_${format}returnaddress", $template)
2696     );
2697
2698   } elsif ( grep /\S/,
2699             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2700
2701     my $convert_map = $convert_maps{$format}{'returnaddress'};
2702     $returnaddress =
2703       join( "\n",
2704             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2705                                                  $template
2706                                                )
2707                          )
2708           );
2709   } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2710
2711     my $convert_map = $convert_maps{$format}{'returnaddress'};
2712     $returnaddress = join( "\n", &$convert_map(
2713                                    map { s/( {2,})/'~' x length($1)/eg;
2714                                          s/$/\\\\\*/;
2715                                          $_
2716                                        }
2717                                      ( $conf->config('company_name', $self->cust_main->agentnum),
2718                                        $conf->config('company_address', $self->cust_main->agentnum),
2719                                      )
2720                                  )
2721                      );
2722
2723   } else {
2724
2725     my $warning = "Couldn't find a return address; ".
2726                   "do you need to set the company_address configuration value?";
2727     warn "$warning\n";
2728     $returnaddress = $nbsp;
2729     #$returnaddress = $warning;
2730
2731   }
2732
2733   warn "$me generating invoice data\n"
2734     if $DEBUG > 1;
2735
2736   my $agentnum = $self->cust_main->agentnum;
2737
2738   my %invoice_data = (
2739
2740     #invoice from info
2741     'company_name'    => scalar( $conf->config('company_name', $agentnum) ),
2742     'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2743     'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2744     'returnaddress'   => $returnaddress,
2745     'agent'           => &$escape_function($cust_main->agent->agent),
2746
2747     #invoice info
2748     'invnum'          => $self->invnum,
2749     '_date'           => $self->_date,
2750     'date'            => $self->time2str_local($date_format, $self->_date),
2751     'today'           => $self->time2str_local($date_format_long, $today),
2752     'terms'           => $self->terms,
2753     'template'        => $template, #params{'template'},
2754     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
2755     'current_charges' => sprintf("%.2f", $self->charged),
2756     'duedate'         => $self->due_date2str($rdate_format), #date_format?
2757
2758     #customer info
2759     'custnum'         => $cust_main->display_custnum,
2760     'agent_custid'    => &$escape_function($cust_main->agent_custid),
2761     ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2762       payname company address1 address2 city state zip fax
2763     )),
2764
2765     #global config
2766     'ship_enable'     => $conf->exists('invoice-ship_address'),
2767     'unitprices'      => $conf->exists('invoice-unitprice'),
2768     'smallernotes'    => $conf->exists('invoice-smallernotes'),
2769     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
2770     'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2771    
2772     #layout info -- would be fancy to calc some of this and bury the template
2773     #               here in the code
2774     'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2775     'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2776     'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
2777     'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2778     'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2779     'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2780     'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2781     'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2782     'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2783     'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2784
2785     # better hang on to conf_dir for a while (for old templates)
2786     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2787
2788     #these are only used when doing paged plaintext
2789     'page'            => 1,
2790     'total_pages'     => 1,
2791
2792   );
2793  
2794   #localization (see FS::cust_main_Mixin)
2795   $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2796   # prototype here to silence warnings
2797   $invoice_data{'time2str'} = sub ($;$$) { $self->time2str_local(@_) };
2798
2799   my $min_sdate = 999999999999;
2800   my $max_edate = 0;
2801   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2802     next unless $cust_bill_pkg->pkgnum > 0;
2803     $min_sdate = $cust_bill_pkg->sdate
2804       if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2805     $max_edate = $cust_bill_pkg->edate
2806       if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2807   }
2808
2809   $invoice_data{'bill_period'} = '';
2810   $invoice_data{'bill_period'} = $self->time2str_local('%e %h', $min_sdate) 
2811                                  . " to " .
2812                                  $self->time2str_local('%e %h', $max_edate)
2813     if ($max_edate != 0 && $min_sdate != 999999999999);
2814
2815   $invoice_data{finance_section} = '';
2816   if ( $conf->config('finance_pkgclass') ) {
2817     my $pkg_class =
2818       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2819     $invoice_data{finance_section} = $pkg_class->categoryname;
2820   } 
2821   $invoice_data{finance_amount} = '0.00';
2822   $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2823
2824   my $countrydefault = $conf->config('countrydefault') || 'US';
2825   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2826   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2827     my $method = $prefix.$_;
2828     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2829   }
2830   $invoice_data{'ship_country'} = ''
2831     if ( $invoice_data{'ship_country'} eq $countrydefault );
2832   
2833   $invoice_data{'cid'} = $params{'cid'}
2834     if $params{'cid'};
2835
2836   if ( $cust_main->country eq $countrydefault ) {
2837     $invoice_data{'country'} = '';
2838   } else {
2839     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2840   }
2841
2842   my @address = ();
2843   $invoice_data{'address'} = \@address;
2844   push @address,
2845     $cust_main->payname.
2846       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2847         ? " (P.O. #". $cust_main->payinfo. ")"
2848         : ''
2849       )
2850   ;
2851   push @address, $cust_main->company
2852     if $cust_main->company;
2853   push @address, $cust_main->address1;
2854   push @address, $cust_main->address2
2855     if $cust_main->address2;
2856   push @address,
2857     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
2858   push @address, $invoice_data{'country'}
2859     if $invoice_data{'country'};
2860   push @address, ''
2861     while (scalar(@address) < 5);
2862
2863   $invoice_data{'logo_file'} = $params{'logo_file'}
2864     if $params{'logo_file'};
2865   $invoice_data{'barcode_file'} = $params{'barcode_file'}
2866     if $params{'barcode_file'};
2867   $invoice_data{'barcode_img'} = $params{'barcode_img'}
2868     if $params{'barcode_img'};
2869   $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2870     if $params{'barcode_cid'};
2871
2872   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2873 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2874   #my $balance_due = $self->owed + $pr_total - $cr_total;
2875   my $balance_due = $self->owed + $pr_total;
2876
2877   # the customer's current balance as shown on the invoice before this one
2878   $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2879
2880   # the change in balance from that invoice to this one
2881   $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2882
2883   # the sum of amount owed on all previous invoices
2884   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2885
2886   # the sum of amount owed on all invoices
2887   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2888
2889   # info from customer's last invoice before this one, for some 
2890   # summary formats
2891   $invoice_data{'last_bill'} = {};
2892   if ( $self->previous_bill ) {
2893     $invoice_data{'last_bill'} = {
2894       '_date'     => $self->previous_bill->_date, #unformatted
2895       # all we need for now
2896     };
2897   }
2898
2899   my $summarypage = '';
2900   if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2901     $summarypage = 1;
2902   }
2903   $invoice_data{'summarypage'} = $summarypage;
2904
2905   warn "$me substituting variables in notes, footer, smallfooter\n"
2906     if $DEBUG > 1;
2907
2908   my @include = (qw( notes footer smallfooter ));
2909   push @include, 'coupon' unless $params{'no_coupon'};
2910   foreach my $include (@include) {
2911
2912     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2913     my @inc_src;
2914
2915     if ( $conf->exists($inc_file, $agentnum)
2916          && length( $conf->config($inc_file, $agentnum) ) ) {
2917
2918       @inc_src = $conf->config($inc_file, $agentnum);
2919
2920     } else {
2921
2922       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2923
2924       my $convert_map = $convert_maps{$format}{$include};
2925
2926       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2927                        s/--\@\]/$delimiters{$format}[1]/g;
2928                        $_;
2929                      } 
2930                  &$convert_map( $conf->config($inc_file, $agentnum) );
2931
2932     }
2933
2934     my $inc_tt = new Text::Template (
2935       TYPE       => 'ARRAY',
2936       SOURCE     => [ map "$_\n", @inc_src ],
2937       DELIMITERS => $delimiters{$format},
2938     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2939
2940     unless ( $inc_tt->compile() ) {
2941       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2942       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2943       die $error;
2944     }
2945
2946     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2947
2948     $invoice_data{$include} =~ s/\n+$//
2949       if ($format eq 'latex');
2950   }
2951
2952   # let invoices use either of these as needed
2953   $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL') 
2954     ? $cust_main->payinfo : '';
2955   $invoice_data{'po_line'} = 
2956     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2957       ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2958       : $nbsp;
2959
2960   my %money_chars = ( 'latex'    => '',
2961                       'html'     => $conf->config('money_char') || '$',
2962                       'template' => '',
2963                     );
2964   my $money_char = $money_chars{$format};
2965
2966   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2967                             'html'     => $conf->config('money_char') || '$',
2968                             'template' => '',
2969                           );
2970   my $other_money_char = $other_money_chars{$format};
2971   $invoice_data{'dollar'} = $other_money_char;
2972
2973   my @detail_items = ();
2974   my @total_items = ();
2975   my @buf = ();
2976   my @sections = ();
2977
2978   $invoice_data{'detail_items'} = \@detail_items;
2979   $invoice_data{'total_items'} = \@total_items;
2980   $invoice_data{'buf'} = \@buf;
2981   $invoice_data{'sections'} = \@sections;
2982
2983   warn "$me generating sections\n"
2984     if $DEBUG > 1;
2985
2986   my $previous_section = { 'description' => $self->mt('Previous Charges'),
2987                            'subtotal'    => $other_money_char.
2988                                             sprintf('%.2f', $pr_total),
2989                            'summarized'  => '', #why? $summarypage ? 'Y' : '',
2990                          };
2991   $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
2992     join(' / ', map { $cust_main->balance_date_range(@$_) }
2993                 $self->_prior_month30s
2994         )
2995     if $conf->exists('invoice_include_aging');
2996
2997   my $taxtotal = 0;
2998   my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2999                       'subtotal'    => $taxtotal,   # adjusted below
3000                       'tax_section' => 1,
3001                     };
3002   my $tax_weight = _pkg_category($tax_section->{description})
3003                         ? _pkg_category($tax_section->{description})->weight
3004                         : 0;
3005   $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
3006   $tax_section->{'sort_weight'} = $tax_weight;
3007
3008
3009   my $adjusttotal = 0;
3010   my $adjust_section = {
3011     'description'    => $self->mt('Credits, Payments, and Adjustments'),
3012     'adjust_section' => 1,
3013     'subtotal'       => 0,   # adjusted below
3014   };
3015   my $adjust_weight = _pkg_category($adjust_section->{description})
3016                         ? _pkg_category($adjust_section->{description})->weight
3017                         : 0;
3018   $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
3019   $adjust_section->{'sort_weight'} = $adjust_weight;
3020
3021   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
3022   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
3023   $invoice_data{'multisection'} = $multisection;
3024   my $late_sections = [];
3025   my $extra_sections = [];
3026   my $extra_lines = ();
3027
3028   my $default_section = { 'description' => '',
3029                           'subtotal'    => '', 
3030                           'no_subtotal' => 1,
3031                         };
3032
3033   if ( $multisection ) {
3034     ($extra_sections, $extra_lines) =
3035       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3036       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3037
3038     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3039
3040     push @detail_items, @$extra_lines if $extra_lines;
3041     push @sections,
3042       $self->_items_sections( $late_sections,      # this could stand a refactor
3043                               $summarypage,
3044                               $escape_function_nonbsp,
3045                               $extra_sections,
3046                               $format,             #bah
3047                             );
3048     if ($conf->exists('svc_phone_sections')) {
3049       my ($phone_sections, $phone_lines) =
3050         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3051       push @{$late_sections}, @$phone_sections;
3052       push @detail_items, @$phone_lines;
3053     }
3054     if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3055       my ($accountcode_section, $accountcode_lines) =
3056         $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3057       if ( scalar(@$accountcode_lines) ) {
3058           push @{$late_sections}, $accountcode_section;
3059           push @detail_items, @$accountcode_lines;
3060       }
3061     }
3062   } else {# not multisection
3063     # make a default section
3064     push @sections, $default_section;
3065     # and calculate the finance charge total, since it won't get done otherwise.
3066     # XXX possibly other totals?
3067     # XXX possibly finance_pkgclass should not be used in this manner?
3068     if ( $conf->exists('finance_pkgclass') ) {
3069       my @finance_charges;
3070       foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3071         if ( grep { $_->section eq $invoice_data{finance_section} }
3072              $cust_bill_pkg->cust_bill_pkg_display ) {
3073           # I think these are always setup fees, but just to be sure...
3074           push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3075         }
3076       }
3077       $invoice_data{finance_amount} = 
3078         sprintf('%.2f', sum( @finance_charges ) || 0);
3079     }
3080   }
3081
3082   # previous invoice balances in the Previous Charges section if there
3083   # is one, otherwise in the main detail section
3084   if ( $self->can('_items_previous') &&
3085        $self->enable_previous &&
3086        ! $conf->exists('previous_balance-summary_only') ) {
3087
3088     warn "$me adding previous balances\n"
3089       if $DEBUG > 1;
3090
3091     foreach my $line_item ( $self->_items_previous ) {
3092
3093       my $detail = {
3094         ext_description => [],
3095       };
3096       $detail->{'ref'} = $line_item->{'pkgnum'};
3097       $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3098       $detail->{'quantity'} = 1;
3099       $detail->{'section'} = $multisection ? $previous_section
3100                                            : $default_section;
3101       $detail->{'description'} = &$escape_function($line_item->{'description'});
3102       if ( exists $line_item->{'ext_description'} ) {
3103         @{$detail->{'ext_description'}} = map {
3104           &$escape_function($_);
3105         } @{$line_item->{'ext_description'}};
3106       }
3107       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3108                             $line_item->{'amount'};
3109       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3110
3111       push @detail_items, $detail;
3112       push @buf, [ $detail->{'description'},
3113                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3114                  ];
3115     }
3116
3117   }
3118
3119   if ( @pr_cust_bill && $self->enable_previous ) {
3120     push @buf, ['','-----------'];
3121     push @buf, [ $self->mt('Total Previous Balance'),
3122                  $money_char. sprintf("%10.2f", $pr_total) ];
3123     push @buf, ['',''];
3124   }
3125  
3126   if ( $conf->exists('svc_phone-did-summary') ) {
3127       warn "$me adding DID summary\n"
3128         if $DEBUG > 1;
3129
3130       my ($didsummary,$minutes) = $self->_did_summary;
3131       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3132       push @detail_items, 
3133        { 'description' => $didsummary_desc,
3134            'ext_description' => [ $didsummary, $minutes ],
3135        };
3136   }
3137
3138   foreach my $section (@sections, @$late_sections) {
3139
3140     warn "$me adding section \n". Dumper($section)
3141       if $DEBUG > 1;
3142
3143     # begin some normalization
3144     $section->{'subtotal'} = $section->{'amount'}
3145       if $multisection
3146          && !exists($section->{subtotal})
3147          && exists($section->{amount});
3148
3149     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3150       if ( $invoice_data{finance_section} &&
3151            $section->{'description'} eq $invoice_data{finance_section} );
3152
3153     $section->{'subtotal'} = $other_money_char.
3154                              sprintf('%.2f', $section->{'subtotal'})
3155       if $multisection;
3156
3157     # continue some normalization
3158     $section->{'amount'}   = $section->{'subtotal'}
3159       if $multisection;
3160
3161
3162     if ( $section->{'description'} ) {
3163       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3164                    [ '', '' ],
3165                  );
3166     }
3167
3168     warn "$me   setting options\n"
3169       if $DEBUG > 1;
3170
3171     my $multilocation = scalar($cust_main->cust_location); #too expensive?
3172     my %options = ();
3173     $options{'section'} = $section if $multisection;
3174     $options{'format'} = $format;
3175     $options{'escape_function'} = $escape_function;
3176     $options{'no_usage'} = 1 unless $unsquelched;
3177     $options{'unsquelched'} = $unsquelched;
3178     $options{'summary_page'} = $summarypage;
3179     $options{'skip_usage'} =
3180       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3181     $options{'multilocation'} = $multilocation;
3182     $options{'multisection'} = $multisection;
3183
3184     warn "$me   searching for line items\n"
3185       if $DEBUG > 1;
3186
3187     foreach my $line_item ( $self->_items_pkg(%options) ) {
3188
3189       warn "$me     adding line item $line_item\n"
3190         if $DEBUG > 1;
3191
3192       my $detail = {
3193         ext_description => [],
3194       };
3195       $detail->{'ref'} = $line_item->{'pkgnum'};
3196       $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3197       $detail->{'quantity'} = $line_item->{'quantity'};
3198       $detail->{'section'} = $section;
3199       $detail->{'description'} = &$escape_function($line_item->{'description'});
3200       if ( exists $line_item->{'ext_description'} ) {
3201         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3202       }
3203       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3204                               $line_item->{'amount'};
3205       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3206                                  $line_item->{'unit_amount'};
3207       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3208
3209       $detail->{'sdate'} = $line_item->{'sdate'};
3210       $detail->{'edate'} = $line_item->{'edate'};
3211       $detail->{'seconds'} = $line_item->{'seconds'};
3212       $detail->{'svc_label'} = $line_item->{'svc_label'};
3213   
3214       push @detail_items, $detail;
3215       push @buf, ( [ $detail->{'description'},
3216                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3217                    ],
3218                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3219                  );
3220     }
3221
3222     if ( $section->{'description'} ) {
3223       push @buf, ( ['','-----------'],
3224                    [ $section->{'description'}. ' sub-total',
3225                       $section->{'subtotal'} # already formatted this 
3226                    ],
3227                    [ '', '' ],
3228                    [ '', '' ],
3229                  );
3230     }
3231   
3232   }
3233
3234   $invoice_data{current_less_finance} =
3235     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3236
3237   # create a major section for previous balance if we have major sections,
3238   # or if previous_section is in summary form
3239   if ( ( $multisection && $self->enable_previous )
3240     || $conf->exists('previous_balance-summary_only') )
3241   {
3242     unshift @sections, $previous_section if $pr_total;
3243   }
3244
3245   warn "$me adding taxes\n"
3246     if $DEBUG > 1;
3247
3248   foreach my $tax ( $self->_items_tax ) {
3249
3250     $taxtotal += $tax->{'amount'};
3251
3252     my $description = &$escape_function( $tax->{'description'} );
3253     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
3254
3255     if ( $multisection ) {
3256
3257       my $money = $old_latex ? '' : $money_char;
3258       push @detail_items, {
3259         ext_description => [],
3260         ref          => '',
3261         quantity     => '',
3262         description  => $description,
3263         amount       => $money. $amount,
3264         product_code => '',
3265         section      => $tax_section,
3266       };
3267
3268     } else {
3269
3270       push @total_items, {
3271         'total_item'   => $description,
3272         'total_amount' => $other_money_char. $amount,
3273       };
3274
3275     }
3276
3277     push @buf,[ $description,
3278                 $money_char. $amount,
3279               ];
3280
3281   }
3282   
3283   if ( $taxtotal ) {
3284     my $total = {};
3285     $total->{'total_item'} = $self->mt('Sub-total');
3286     $total->{'total_amount'} =
3287       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3288
3289     if ( $multisection ) {
3290       $tax_section->{'subtotal'} = $other_money_char.
3291                                    sprintf('%.2f', $taxtotal);
3292       $tax_section->{'pretotal'} = 'New charges sub-total '.
3293                                    $total->{'total_amount'};
3294       push @sections, $tax_section if $taxtotal;
3295     }else{
3296       unshift @total_items, $total;
3297     }
3298   }
3299   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3300
3301   push @buf,['','-----------'];
3302   push @buf,[$self->mt( 
3303               (!$self->enable_previous)
3304                ? 'Total Charges'
3305                : 'Total New Charges'
3306              ),
3307              $money_char. sprintf("%10.2f",$self->charged) ];
3308   push @buf,['',''];
3309
3310   # calculate total, possibly including total owed on previous
3311   # invoices
3312   {
3313     my $total = {};
3314     my $item = 'Total';
3315     $item = $conf->config('previous_balance-exclude_from_total')
3316          || 'Total New Charges'
3317       if $conf->exists('previous_balance-exclude_from_total');
3318     my $amount = $self->charged;
3319     if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3320       $amount += $pr_total;
3321     }
3322
3323     $total->{'total_item'} = &$embolden_function($self->mt($item));
3324     $total->{'total_amount'} =
3325       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
3326     if ( $multisection ) {
3327       if ( $adjust_section->{'sort_weight'} ) {
3328         $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3329           $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
3330       } else {
3331         $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3332           $other_money_char.  sprintf('%.2f', $self->charged );
3333       } 
3334     } else {
3335       push @total_items, $total;
3336     }
3337     push @buf,['','-----------'];
3338     push @buf,[$item,
3339                $money_char.
3340                sprintf( '%10.2f', $amount )
3341               ];
3342     push @buf,['',''];
3343   }
3344
3345   # if we're showing previous invoices, also show previous
3346   # credits and payments 
3347   if ( $self->enable_previous 
3348         and $self->can('_items_credits')
3349         and $self->can('_items_payments') )
3350     {
3351     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3352   
3353     # credits
3354     my $credittotal = 0;
3355     foreach my $credit (
3356       $self->_items_credits( 'template' => $template, 'trim_len' => 60)
3357     ) {
3358
3359       my $total;
3360       $total->{'total_item'} = &$escape_function($credit->{'description'});
3361       $credittotal += $credit->{'amount'};
3362       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3363       $adjusttotal += $credit->{'amount'};
3364       if ( $multisection ) {
3365         my $money = $old_latex ? '' : $money_char;
3366         push @detail_items, {
3367           ext_description => [],
3368           ref          => '',
3369           quantity     => '',
3370           description  => &$escape_function($credit->{'description'}),
3371           amount       => $money. $credit->{'amount'},
3372           product_code => '',
3373           section      => $adjust_section,
3374         };
3375       } else {
3376         push @total_items, $total;
3377       }
3378
3379     }
3380     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3381
3382     #credits (again)
3383     foreach my $credit (
3384       $self->_items_credits( 'template' => $template, 'trim_len' => 32)
3385     ) {
3386       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3387     }
3388
3389     # payments
3390     my $paymenttotal = 0;
3391     foreach my $payment (
3392       $self->_items_payments( 'template' => $template )
3393     ) {
3394       my $total = {};
3395       $total->{'total_item'} = &$escape_function($payment->{'description'});
3396       $paymenttotal += $payment->{'amount'};
3397       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3398       $adjusttotal += $payment->{'amount'};
3399       if ( $multisection ) {
3400         my $money = $old_latex ? '' : $money_char;
3401         push @detail_items, {
3402           ext_description => [],
3403           ref          => '',
3404           quantity     => '',
3405           description  => &$escape_function($payment->{'description'}),
3406           amount       => $money. $payment->{'amount'},
3407           product_code => '',
3408           section      => $adjust_section,
3409         };
3410       }else{
3411         push @total_items, $total;
3412       }
3413       push @buf, [ $payment->{'description'},
3414                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
3415                  ];
3416     }
3417     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3418   
3419     if ( $multisection ) {
3420       $adjust_section->{'subtotal'} = $other_money_char.
3421                                       sprintf('%.2f', $adjusttotal);
3422       push @sections, $adjust_section
3423         unless $adjust_section->{sort_weight};
3424     }
3425
3426     # create Balance Due message
3427     { 
3428       my $total;
3429       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3430       $total->{'total_amount'} =
3431         &$embolden_function(
3432           $other_money_char. sprintf('%.2f', #why? $summarypage 
3433                                              #  ? $self->charged +
3434                                              #    $self->billing_balance
3435                                              #  :
3436                                                  $self->owed + $pr_total
3437                                     )
3438         );
3439       if ( $multisection && !$adjust_section->{sort_weight} ) {
3440         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3441                                          $total->{'total_amount'};
3442       }else{
3443         push @total_items, $total;
3444       }
3445       push @buf,['','-----------'];
3446       push @buf,[$self->balance_due_msg, $money_char. 
3447         sprintf("%10.2f", $balance_due ) ];
3448     }
3449
3450     if ( $conf->exists('previous_balance-show_credit')
3451         and $cust_main->balance < 0 ) {
3452       my $credit_total = {
3453         'total_item'    => &$embolden_function($self->credit_balance_msg),
3454         'total_amount'  => &$embolden_function(
3455           $other_money_char. sprintf('%.2f', -$cust_main->balance)
3456         ),
3457       };
3458       if ( $multisection ) {
3459         $adjust_section->{'posttotal'} .= $newline_token .
3460           $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3461       }
3462       else {
3463         push @total_items, $credit_total;
3464       }
3465       push @buf,['','-----------'];
3466       push @buf,[$self->credit_balance_msg, $money_char. 
3467         sprintf("%10.2f", -$cust_main->balance ) ];
3468     }
3469   }
3470
3471   if ( $multisection ) {
3472     if ($conf->exists('svc_phone_sections')) {
3473       my $total;
3474       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3475       $total->{'total_amount'} =
3476         &$embolden_function(
3477           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3478         );
3479       my $last_section = pop @sections;
3480       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3481                                      $total->{'total_amount'};
3482       push @sections, $last_section;
3483     }
3484     push @sections, @$late_sections
3485       if $unsquelched;
3486   }
3487
3488   # make a discounts-available section, even without multisection
3489   if ( $conf->exists('discount-show_available') 
3490        and my @discounts_avail = $self->_items_discounts_avail ) {
3491     my $discount_section = {
3492       'description' => $self->mt('Discounts Available'),
3493       'subtotal'    => '',
3494       'no_subtotal' => 1,
3495     };
3496
3497     push @sections, $discount_section;
3498     push @detail_items, map { +{
3499         'ref'         => '', #should this be something else?
3500         'section'     => $discount_section,
3501         'description' => &$escape_function( $_->{description} ),
3502         'amount'      => $money_char . &$escape_function( $_->{amount} ),
3503         'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3504     } } @discounts_avail;
3505   }
3506
3507   # debugging hook: call this with 'diag' => 1 to just get a hash of
3508   # the invoice variables
3509   return \%invoice_data if ( $params{'diag'} );
3510
3511   # All sections and items are built; now fill in templates.
3512   my @includelist = ();
3513   push @includelist, 'summary' if $summarypage;
3514   foreach my $include ( @includelist ) {
3515
3516     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3517     my @inc_src;
3518
3519     if ( length( $conf->config($inc_file, $agentnum) ) ) {
3520
3521       @inc_src = $conf->config($inc_file, $agentnum);
3522
3523     } else {
3524
3525       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3526
3527       my $convert_map = $convert_maps{$format}{$include};
3528
3529       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3530                        s/--\@\]/$delimiters{$format}[1]/g;
3531                        $_;
3532                      } 
3533                  &$convert_map( $conf->config($inc_file, $agentnum) );
3534
3535     }
3536
3537     my $inc_tt = new Text::Template (
3538       TYPE       => 'ARRAY',
3539       SOURCE     => [ map "$_\n", @inc_src ],
3540       DELIMITERS => $delimiters{$format},
3541     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3542
3543     unless ( $inc_tt->compile() ) {
3544       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3545       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3546       die $error;
3547     }
3548
3549     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3550
3551     $invoice_data{$include} =~ s/\n+$//
3552       if ($format eq 'latex');
3553   }
3554
3555   $invoice_lines = 0;
3556   my $wasfunc = 0;
3557   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3558     /invoice_lines\((\d*)\)/;
3559     $invoice_lines += $1 || scalar(@buf);
3560     $wasfunc=1;
3561   }
3562   die "no invoice_lines() functions in template?"
3563     if ( $format eq 'template' && !$wasfunc );
3564
3565   if ($format eq 'template') {
3566
3567     if ( $invoice_lines ) {
3568       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3569       $invoice_data{'total_pages'}++
3570         if scalar(@buf) % $invoice_lines;
3571     }
3572
3573     #setup subroutine for the template
3574     $invoice_data{invoice_lines} = sub {
3575       my $lines = shift || scalar(@buf);
3576       map { 
3577         scalar(@buf)
3578           ? shift @buf
3579           : [ '', '' ];
3580       }
3581       ( 1 .. $lines );
3582     };
3583
3584     my $lines;
3585     my @collect;
3586     while (@buf) {
3587       push @collect, split("\n",
3588         $text_template->fill_in( HASH => \%invoice_data )
3589       );
3590       $invoice_data{'page'}++;
3591     }
3592     map "$_\n", @collect;
3593   }else{
3594     # this is where we actually create the invoice
3595     warn "filling in template for invoice ". $self->invnum. "\n"
3596       if $DEBUG;
3597     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3598       if $DEBUG > 1;
3599
3600     $text_template->fill_in(HASH => \%invoice_data);
3601   }
3602 }
3603
3604 # helper routine for generating date ranges
3605 sub _prior_month30s {
3606   my $self = shift;
3607   my @ranges = (
3608    [ 1,       2592000 ], # 0-30 days ago
3609    [ 2592000, 5184000 ], # 30-60 days ago
3610    [ 5184000, 7776000 ], # 60-90 days ago
3611    [ 7776000, 0       ], # 90+   days ago
3612   );
3613
3614   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3615           $_->[1] ? $self->_date - $_->[1] - 1 : '',
3616       ] }
3617   @ranges;
3618 }
3619
3620 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3621
3622 Returns an postscript invoice, as a scalar.
3623
3624 Options can be passed as a hashref (recommended) or as a list of time, template
3625 and then any key/value pairs for any other options.
3626
3627 I<time> an optional value used to control the printing of overdue messages.  The
3628 default is now.  It isn't the date of the invoice; that's the `_date' field.
3629 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3630 L<Time::Local> and L<Date::Parse> for conversion functions.
3631
3632 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3633
3634 =cut
3635
3636 sub print_ps {
3637   my $self = shift;
3638
3639   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3640   my $ps = generate_ps($file);
3641   unlink($logofile);
3642   unlink($barcodefile) if $barcodefile;
3643
3644   $ps;
3645 }
3646
3647 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3648
3649 Returns an PDF invoice, as a scalar.
3650
3651 Options can be passed as a hashref (recommended) or as a list of time, template
3652 and then any key/value pairs for any other options.
3653
3654 I<time> an optional value used to control the printing of overdue messages.  The
3655 default is now.  It isn't the date of the invoice; that's the `_date' field.
3656 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3657 L<Time::Local> and L<Date::Parse> for conversion functions.
3658
3659 I<template>, if specified, is the name of a suffix for alternate invoices.
3660
3661 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3662
3663 =cut
3664
3665 sub print_pdf {
3666   my $self = shift;
3667
3668   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3669   my $pdf = generate_pdf($file);
3670   unlink($logofile);
3671   unlink($barcodefile) if $barcodefile;
3672
3673   $pdf;
3674 }
3675
3676 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3677
3678 Returns an HTML invoice, as a scalar.
3679
3680 I<time> an optional value used to control the printing of overdue messages.  The
3681 default is now.  It isn't the date of the invoice; that's the `_date' field.
3682 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3683 L<Time::Local> and L<Date::Parse> for conversion functions.
3684
3685 I<template>, if specified, is the name of a suffix for alternate invoices.
3686
3687 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3688
3689 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3690 when emailing the invoice as part of a multipart/related MIME email.
3691
3692 =cut
3693
3694 sub print_html {
3695   my $self = shift;
3696   my %params;
3697   if ( ref($_[0]) ) {
3698     %params = %{ shift() }; 
3699   }else{
3700     $params{'time'} = shift;
3701     $params{'template'} = shift;
3702     $params{'cid'} = shift;
3703   }
3704
3705   $params{'format'} = 'html';
3706   
3707   $self->print_generic( %params );
3708 }
3709
3710 # quick subroutine for print_latex
3711 #
3712 # There are ten characters that LaTeX treats as special characters, which
3713 # means that they do not simply typeset themselves: 
3714 #      # $ % & ~ _ ^ \ { }
3715 #
3716 # TeX ignores blanks following an escaped character; if you want a blank (as
3717 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3718
3719 sub _latex_escape {
3720   my $value = shift;
3721   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3722   $value =~ s/([<>])/\$$1\$/g;
3723   $value;
3724 }
3725
3726 sub _html_escape {
3727   my $value = shift;
3728   encode_entities($value);
3729   $value;
3730 }
3731
3732 sub _html_escape_nbsp {
3733   my $value = _html_escape(shift);
3734   $value =~ s/ +/&nbsp;/g;
3735   $value;
3736 }
3737
3738 #utility methods for print_*
3739
3740 sub _translate_old_latex_format {
3741   warn "_translate_old_latex_format called\n"
3742     if $DEBUG; 
3743
3744   my @template = ();
3745   while ( @_ ) {
3746     my $line = shift;
3747   
3748     if ( $line =~ /^%%Detail\s*$/ ) {
3749   
3750       push @template, q![@--!,
3751                       q!  foreach my $_tr_line (@detail_items) {!,
3752                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3753                       q!      $_tr_line->{'description'} .= !, 
3754                       q!        "\\tabularnewline\n~~".!,
3755                       q!        join( "\\tabularnewline\n~~",!,
3756                       q!          @{$_tr_line->{'ext_description'}}!,
3757                       q!        );!,
3758                       q!    }!;
3759
3760       while ( ( my $line_item_line = shift )
3761               !~ /^%%EndDetail\s*$/                            ) {
3762         $line_item_line =~ s/'/\\'/g;    # nice LTS
3763         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3764         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3765         push @template, "    \$OUT .= '$line_item_line';";
3766       }
3767
3768       push @template, '}',
3769                       '--@]';
3770       #' doh, gvim
3771     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3772
3773       push @template, '[@--',
3774                       '  foreach my $_tr_line (@total_items) {';
3775
3776       while ( ( my $total_item_line = shift )
3777               !~ /^%%EndTotalDetails\s*$/                      ) {
3778         $total_item_line =~ s/'/\\'/g;    # nice LTS
3779         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3780         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3781         push @template, "    \$OUT .= '$total_item_line';";
3782       }
3783
3784       push @template, '}',
3785                       '--@]';
3786
3787     } else {
3788       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3789       push @template, $line;  
3790     }
3791   
3792   }
3793
3794   if ($DEBUG) {
3795     warn "$_\n" foreach @template;
3796   }
3797
3798   (@template);
3799 }
3800
3801 sub terms {
3802   my $self = shift;
3803   my $conf = $self->conf;
3804
3805   #check for an invoice-specific override
3806   return $self->invoice_terms if $self->invoice_terms;
3807   
3808   #check for a customer- specific override
3809   my $cust_main = $self->cust_main;
3810   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3811
3812   #use configured default
3813   $conf->config('invoice_default_terms') || '';
3814 }
3815
3816 sub due_date {
3817   my $self = shift;
3818   my $duedate = '';
3819   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3820     $duedate = $self->_date() + ( $1 * 86400 );
3821   }
3822   $duedate;
3823 }
3824
3825 sub due_date2str {
3826   my $self = shift;
3827   $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
3828 }
3829
3830 sub balance_due_msg {
3831   my $self = shift;
3832   my $msg = $self->mt('Balance Due');
3833   return $msg unless $self->terms;
3834   if ( $self->due_date ) {
3835     $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3836       $self->due_date2str($date_format);
3837   } elsif ( $self->terms ) {
3838     $msg .= ' - '. $self->terms;
3839   }
3840   $msg;
3841 }
3842
3843 sub balance_due_date {
3844   my $self = shift;
3845   my $conf = $self->conf;
3846   my $duedate = '';
3847   if (    $conf->exists('invoice_default_terms') 
3848        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3849     $duedate = $self->time2str_local($rdate_format, $self->_date + ($1*86400) );
3850   }
3851   $duedate;
3852 }
3853
3854 sub credit_balance_msg { 
3855   my $self = shift;
3856   $self->mt('Credit Balance Remaining')
3857 }
3858
3859 =item invnum_date_pretty
3860
3861 Returns a string with the invoice number and date, for example:
3862 "Invoice #54 (3/20/2008)"
3863
3864 =cut
3865
3866 sub invnum_date_pretty {
3867   my $self = shift;
3868   $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3869 }
3870
3871 =item _date_pretty
3872
3873 Returns a string with the date, for example: "3/20/2008"
3874
3875 =cut
3876
3877 sub _date_pretty {
3878   my $self = shift;
3879   $self->time2str_local($date_format, $self->_date);
3880 }
3881
3882 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3883
3884 Generate section information for all items appearing on this invoice.
3885 This will only be called for multi-section invoices.
3886
3887 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
3888 related display records (L<FS::cust_bill_pkg_display>) and organize 
3889 them into two groups ("early" and "late" according to whether they come 
3890 before or after the total), then into sections.  A subtotal is calculated 
3891 for each section.
3892
3893 Section descriptions are returned in sort weight order.  Each consists 
3894 of a hash containing:
3895
3896 description: the package category name, escaped
3897 subtotal: the total charges in that section
3898 tax_section: a flag indicating that the section contains only tax charges
3899 summarized: same as tax_section, for some reason
3900 sort_weight: the package category's sort weight
3901
3902 If 'condense' is set on the display record, it also contains everything 
3903 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3904 coderefs to generate parts of the invoice.  This is not advised.
3905
3906 Arguments:
3907
3908 LATE: an arrayref to push the "late" section hashes onto.  The "early"
3909 group is simply returned from the method.
3910
3911 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3912 Turning this on has the following effects:
3913 - Ignores display items with the 'summary' flag.
3914 - Combines all items into the "early" group.
3915 - Creates sections for all non-disabled package categories, even if they 
3916 have no charges on this invoice, as well as a section with no name.
3917
3918 ESCAPE: an escape function to use for section titles.
3919
3920 EXTRA_SECTIONS: an arrayref of additional sections to return after the 
3921 sorted list.  If there are any of these, section subtotals exclude 
3922 usage charges.
3923
3924 FORMAT: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
3925 passed through to C<_condense_section()>.
3926
3927 =cut
3928
3929 use vars qw(%pkg_category_cache);
3930 sub _items_sections {
3931   my $self = shift;
3932   my $late = shift;
3933   my $summarypage = shift;
3934   my $escape = shift;
3935   my $extra_sections = shift;
3936   my $format = shift;
3937
3938   my %subtotal = ();
3939   my %late_subtotal = ();
3940   my %not_tax = ();
3941
3942   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3943   {
3944
3945       my $usage = $cust_bill_pkg->usage;
3946
3947       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3948         next if ( $display->summary && $summarypage );
3949
3950         my $section = $display->section;
3951         my $type    = $display->type;
3952
3953         $not_tax{$section} = 1
3954           unless $cust_bill_pkg->pkgnum == 0;
3955
3956         if ( $display->post_total && !$summarypage ) {
3957           if (! $type || $type eq 'S') {
3958             $late_subtotal{$section} += $cust_bill_pkg->setup
3959               if $cust_bill_pkg->setup != 0
3960               || $cust_bill_pkg->setup_show_zero;
3961           }
3962
3963           if (! $type) {
3964             $late_subtotal{$section} += $cust_bill_pkg->recur
3965               if $cust_bill_pkg->recur != 0
3966               || $cust_bill_pkg->recur_show_zero;
3967           }
3968
3969           if ($type && $type eq 'R') {
3970             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3971               if $cust_bill_pkg->recur != 0
3972               || $cust_bill_pkg->recur_show_zero;
3973           }
3974           
3975           if ($type && $type eq 'U') {
3976             $late_subtotal{$section} += $usage
3977               unless scalar(@$extra_sections);
3978           }
3979
3980         } else {
3981
3982           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3983
3984           if (! $type || $type eq 'S') {
3985             $subtotal{$section} += $cust_bill_pkg->setup
3986               if $cust_bill_pkg->setup != 0
3987               || $cust_bill_pkg->setup_show_zero;
3988           }
3989
3990           if (! $type) {
3991             $subtotal{$section} += $cust_bill_pkg->recur
3992               if $cust_bill_pkg->recur != 0
3993               || $cust_bill_pkg->recur_show_zero;
3994           }
3995
3996           if ($type && $type eq 'R') {
3997             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3998               if $cust_bill_pkg->recur != 0
3999               || $cust_bill_pkg->recur_show_zero;
4000           }
4001           
4002           if ($type && $type eq 'U') {
4003             $subtotal{$section} += $usage
4004               unless scalar(@$extra_sections);
4005           }
4006
4007         }
4008
4009       }
4010
4011   }
4012
4013   %pkg_category_cache = ();
4014
4015   push @$late, map { { 'description' => &{$escape}($_),
4016                        'subtotal'    => $late_subtotal{$_},
4017                        'post_total'  => 1,
4018                        'sort_weight' => ( _pkg_category($_)
4019                                             ? _pkg_category($_)->weight
4020                                             : 0
4021                                        ),
4022                        ((_pkg_category($_) && _pkg_category($_)->condense)
4023                                            ? $self->_condense_section($format)
4024                                            : ()
4025                        ),
4026                    } }
4027                  sort _sectionsort keys %late_subtotal;
4028
4029   my @sections;
4030   if ( $summarypage ) {
4031     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
4032                 map { $_->categoryname } qsearch('pkg_category', {});
4033     push @sections, '' if exists($subtotal{''});
4034   } else {
4035     @sections = keys %subtotal;
4036   }
4037
4038   my @early = map { { 'description' => &{$escape}($_),
4039                       'subtotal'    => $subtotal{$_},
4040                       'summarized'  => $not_tax{$_} ? '' : 'Y',
4041                       'tax_section' => $not_tax{$_} ? '' : 'Y',
4042                       'sort_weight' => ( _pkg_category($_)
4043                                            ? _pkg_category($_)->weight
4044                                            : 0
4045                                        ),
4046                        ((_pkg_category($_) && _pkg_category($_)->condense)
4047                                            ? $self->_condense_section($format)
4048                                            : ()
4049                        ),
4050                     }
4051                   } @sections;
4052   push @early, @$extra_sections if $extra_sections;
4053
4054   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4055
4056 }
4057
4058 #helper subs for above
4059
4060 sub _sectionsort {
4061   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4062 }
4063
4064 sub _pkg_category {
4065   my $categoryname = shift;
4066   $pkg_category_cache{$categoryname} ||=
4067     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4068 }
4069
4070 my %condensed_format = (
4071   'label' => [ qw( Description Qty Amount ) ],
4072   'fields' => [
4073                 sub { shift->{description} },
4074                 sub { shift->{quantity} },
4075                 sub { my($href, %opt) = @_;
4076                       ($opt{dollar} || ''). $href->{amount};
4077                     },
4078               ],
4079   'align'  => [ qw( l r r ) ],
4080   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
4081   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
4082 );
4083
4084 sub _condense_section {
4085   my ( $self, $format ) = ( shift, shift );
4086   ( 'condensed' => 1,
4087     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4088       qw( description_generator
4089           header_generator
4090           total_generator
4091           total_line_generator
4092         )
4093   );
4094 }
4095
4096 sub _condensed_generator_defaults {
4097   my ( $self, $format ) = ( shift, shift );
4098   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4099 }
4100
4101 my %html_align = (
4102   'c' => 'center',
4103   'l' => 'left',
4104   'r' => 'right',
4105 );
4106
4107 sub _condensed_header_generator {
4108   my ( $self, $format ) = ( shift, shift );
4109
4110   my ( $f, $prefix, $suffix, $separator, $column ) =
4111     _condensed_generator_defaults($format);
4112
4113   if ($format eq 'latex') {
4114     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4115     $suffix = "\\\\\n\\hline";
4116     $separator = "&\n";
4117     $column =
4118       sub { my ($d,$a,$s,$w) = @_;
4119             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4120           };
4121   } elsif ( $format eq 'html' ) {
4122     $prefix = '<th></th>';
4123     $suffix = '';
4124     $separator = '';
4125     $column =
4126       sub { my ($d,$a,$s,$w) = @_;
4127             return qq!<th align="$html_align{$a}">$d</th>!;
4128       };
4129   }
4130
4131   sub {
4132     my @args = @_;
4133     my @result = ();
4134
4135     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4136       push @result,
4137         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4138     }
4139
4140     $prefix. join($separator, @result). $suffix;
4141   };
4142
4143 }
4144
4145 sub _condensed_description_generator {
4146   my ( $self, $format ) = ( shift, shift );
4147
4148   my ( $f, $prefix, $suffix, $separator, $column ) =
4149     _condensed_generator_defaults($format);
4150
4151   my $money_char = '$';
4152   if ($format eq 'latex') {
4153     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4154     $suffix = '\\\\';
4155     $separator = " & \n";
4156     $column =
4157       sub { my ($d,$a,$s,$w) = @_;
4158             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4159           };
4160     $money_char = '\\dollar';
4161   }elsif ( $format eq 'html' ) {
4162     $prefix = '"><td align="center"></td>';
4163     $suffix = '';
4164     $separator = '';
4165     $column =
4166       sub { my ($d,$a,$s,$w) = @_;
4167             return qq!<td align="$html_align{$a}">$d</td>!;
4168       };
4169     #$money_char = $conf->config('money_char') || '$';
4170     $money_char = '';  # this is madness
4171   }
4172
4173   sub {
4174     #my @args = @_;
4175     my $href = shift;
4176     my @result = ();
4177
4178     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4179       my $dollar = '';
4180       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4181       push @result,
4182         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4183                     map { $f->{$_}->[$i] } qw(align span width)
4184                   );
4185     }
4186
4187     $prefix. join( $separator, @result ). $suffix;
4188   };
4189
4190 }
4191
4192 sub _condensed_total_generator {
4193   my ( $self, $format ) = ( shift, shift );
4194
4195   my ( $f, $prefix, $suffix, $separator, $column ) =
4196     _condensed_generator_defaults($format);
4197   my $style = '';
4198
4199   if ($format eq 'latex') {
4200     $prefix = "& ";
4201     $suffix = "\\\\\n";
4202     $separator = " & \n";
4203     $column =
4204       sub { my ($d,$a,$s,$w) = @_;
4205             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4206           };
4207   }elsif ( $format eq 'html' ) {
4208     $prefix = '';
4209     $suffix = '';
4210     $separator = '';
4211     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4212     $column =
4213       sub { my ($d,$a,$s,$w) = @_;
4214             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4215       };
4216   }
4217
4218
4219   sub {
4220     my @args = @_;
4221     my @result = ();
4222
4223     #  my $r = &{$f->{fields}->[$i]}(@args);
4224     #  $r .= ' Total' unless $i;
4225
4226     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4227       push @result,
4228         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4229                     map { $f->{$_}->[$i] } qw(align span width)
4230                   );
4231     }
4232
4233     $prefix. join( $separator, @result ). $suffix;
4234   };
4235
4236 }
4237
4238 =item total_line_generator FORMAT
4239
4240 Returns a coderef used for generation of invoice total line items for this
4241 usage_class.  FORMAT is either html or latex
4242
4243 =cut
4244
4245 # should not be used: will have issues with hash element names (description vs
4246 # total_item and amount vs total_amount -- another array of functions?
4247
4248 sub _condensed_total_line_generator {
4249   my ( $self, $format ) = ( shift, shift );
4250
4251   my ( $f, $prefix, $suffix, $separator, $column ) =
4252     _condensed_generator_defaults($format);
4253   my $style = '';
4254
4255   if ($format eq 'latex') {
4256     $prefix = "& ";
4257     $suffix = "\\\\\n";
4258     $separator = " & \n";
4259     $column =
4260       sub { my ($d,$a,$s,$w) = @_;
4261             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4262           };
4263   }elsif ( $format eq 'html' ) {
4264     $prefix = '';
4265     $suffix = '';
4266     $separator = '';
4267     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4268     $column =
4269       sub { my ($d,$a,$s,$w) = @_;
4270             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4271       };
4272   }
4273
4274
4275   sub {
4276     my @args = @_;
4277     my @result = ();
4278
4279     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4280       push @result,
4281         &{$column}( &{$f->{fields}->[$i]}(@args),
4282                     map { $f->{$_}->[$i] } qw(align span width)
4283                   );
4284     }
4285
4286     $prefix. join( $separator, @result ). $suffix;
4287   };
4288
4289 }
4290
4291 #sub _items_extra_usage_sections {
4292 #  my $self = shift;
4293 #  my $escape = shift;
4294 #
4295 #  my %sections = ();
4296 #
4297 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
4298 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4299 #  {
4300 #    next unless $cust_bill_pkg->pkgnum > 0;
4301 #
4302 #    foreach my $section ( keys %usage_class ) {
4303 #
4304 #      my $usage = $cust_bill_pkg->usage($section);
4305 #
4306 #      next unless $usage && $usage > 0;
4307 #
4308 #      $sections{$section} ||= 0;
4309 #      $sections{$section} += $usage;
4310 #
4311 #    }
4312 #
4313 #  }
4314 #
4315 #  map { { 'description' => &{$escape}($_),
4316 #          'subtotal'    => $sections{$_},
4317 #          'summarized'  => '',
4318 #          'tax_section' => '',
4319 #        }
4320 #      }
4321 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4322 #
4323 #}
4324
4325 sub _items_extra_usage_sections {
4326   my $self = shift;
4327   my $conf = $self->conf;
4328   my $escape = shift;
4329   my $format = shift;
4330
4331   my %sections = ();
4332   my %classnums = ();
4333   my %lines = ();
4334
4335   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4336
4337   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4338   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4339     next unless $cust_bill_pkg->pkgnum > 0;
4340
4341     foreach my $classnum ( keys %usage_class ) {
4342       my $section = $usage_class{$classnum}->classname;
4343       $classnums{$section} = $classnum;
4344
4345       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4346         my $amount = $detail->amount;
4347         next unless $amount && $amount > 0;
4348  
4349         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4350         $sections{$section}{amount} += $amount;  #subtotal
4351         $sections{$section}{calls}++;
4352         $sections{$section}{duration} += $detail->duration;
4353
4354         my $desc = $detail->regionname; 
4355         my $description = $desc;
4356         $description = substr($desc, 0, $maxlength). '...'
4357           if $format eq 'latex' && length($desc) > $maxlength;
4358
4359         $lines{$section}{$desc} ||= {
4360           description     => &{$escape}($description),
4361           #pkgpart         => $part_pkg->pkgpart,
4362           pkgnum          => $cust_bill_pkg->pkgnum,
4363           ref             => '',
4364           amount          => 0,
4365           calls           => 0,
4366           duration        => 0,
4367           #unit_amount     => $cust_bill_pkg->unitrecur,
4368           quantity        => $cust_bill_pkg->quantity,
4369           product_code    => 'N/A',
4370           ext_description => [],
4371         };
4372
4373         $lines{$section}{$desc}{amount} += $amount;
4374         $lines{$section}{$desc}{calls}++;
4375         $lines{$section}{$desc}{duration} += $detail->duration;
4376
4377       }
4378     }
4379   }
4380
4381   my %sectionmap = ();
4382   foreach (keys %sections) {
4383     my $usage_class = $usage_class{$classnums{$_}};
4384     $sectionmap{$_} = { 'description' => &{$escape}($_),
4385                         'amount'    => $sections{$_}{amount},    #subtotal
4386                         'calls'       => $sections{$_}{calls},
4387                         'duration'    => $sections{$_}{duration},
4388                         'summarized'  => '',
4389                         'tax_section' => '',
4390                         'sort_weight' => $usage_class->weight,
4391                         ( $usage_class->format
4392                           ? ( map { $_ => $usage_class->$_($format) }
4393                               qw( description_generator header_generator total_generator total_line_generator )
4394                             )
4395                           : ()
4396                         ), 
4397                       };
4398   }
4399
4400   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4401                  values %sectionmap;
4402
4403   my @lines = ();
4404   foreach my $section ( keys %lines ) {
4405     foreach my $line ( keys %{$lines{$section}} ) {
4406       my $l = $lines{$section}{$line};
4407       $l->{section}     = $sectionmap{$section};
4408       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4409       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4410       push @lines, $l;
4411     }
4412   }
4413
4414   return(\@sections, \@lines);
4415
4416 }
4417
4418 sub _did_summary {
4419     my $self = shift;
4420     my $end = $self->_date;
4421
4422     # start at date of previous invoice + 1 second or 0 if no previous invoice
4423     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4424     $start = 0 if !$start;
4425     $start++;
4426
4427     my $cust_main = $self->cust_main;
4428     my @pkgs = $cust_main->all_pkgs;
4429     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4430         = (0,0,0,0,0);
4431     my @seen = ();
4432     foreach my $pkg ( @pkgs ) {
4433         my @h_cust_svc = $pkg->h_cust_svc($end);
4434         foreach my $h_cust_svc ( @h_cust_svc ) {
4435             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4436             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4437
4438             my $inserted = $h_cust_svc->date_inserted;
4439             my $deleted = $h_cust_svc->date_deleted;
4440             my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4441             my $phone_deleted;
4442             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
4443             
4444 # DID either activated or ported in; cannot be both for same DID simultaneously
4445             if ($inserted >= $start && $inserted <= $end && $phone_inserted
4446                 && (!$phone_inserted->lnp_status 
4447                     || $phone_inserted->lnp_status eq ''
4448                     || $phone_inserted->lnp_status eq 'native')) {
4449                 $num_activated++;
4450             }
4451             else { # this one not so clean, should probably move to (h_)svc_phone
4452                  my $phone_portedin = qsearchs( 'h_svc_phone',
4453                       { 'svcnum' => $h_cust_svc->svcnum, 
4454                         'lnp_status' => 'portedin' },  
4455                       FS::h_svc_phone->sql_h_searchs($end),  
4456                     );
4457                  $num_portedin++ if $phone_portedin;
4458             }
4459
4460 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4461             if($deleted >= $start && $deleted <= $end && $phone_deleted
4462                 && (!$phone_deleted->lnp_status 
4463                     || $phone_deleted->lnp_status ne 'portingout')) {
4464                 $num_deactivated++;
4465             } 
4466             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
4467                 && $phone_deleted->lnp_status 
4468                 && $phone_deleted->lnp_status eq 'portingout') {
4469                 $num_portedout++;
4470             }
4471
4472             # increment usage minutes
4473         if ( $phone_inserted ) {
4474             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4475             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4476         }
4477         else {
4478             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4479         }
4480
4481             # don't look at this service again
4482             push @seen, $h_cust_svc->svcnum;
4483         }
4484     }
4485
4486     $minutes = sprintf("%d", $minutes);
4487     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
4488         . "$num_deactivated  Ported-Out: $num_portedout ",
4489             "Total Minutes: $minutes");
4490 }
4491
4492 sub _items_accountcode_cdr {
4493     my $self = shift;
4494     my $escape = shift;
4495     my $format = shift;
4496
4497     my $section = { 'amount'        => 0,
4498                     'calls'         => 0,
4499                     'duration'      => 0,
4500                     'sort_weight'   => '',
4501                     'phonenum'      => '',
4502                     'description'   => 'Usage by Account Code',
4503                     'post_total'    => '',
4504                     'summarized'    => '',
4505                     'header'        => '',
4506                   };
4507     my @lines;
4508     my %accountcodes = ();
4509
4510     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4511         next unless $cust_bill_pkg->pkgnum > 0;
4512
4513         my @header = $cust_bill_pkg->details_header;
4514         next unless scalar(@header);
4515         $section->{'header'} = join(',',@header);
4516
4517         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4518
4519             $section->{'header'} = $detail->formatted('format' => $format)
4520                 if($detail->detail eq $section->{'header'}); 
4521       
4522             my $accountcode = $detail->accountcode;
4523             next unless $accountcode;
4524
4525             my $amount = $detail->amount;
4526             next unless $amount && $amount > 0;
4527
4528             $accountcodes{$accountcode} ||= {
4529                     description => $accountcode,
4530                     pkgnum      => '',
4531                     ref         => '',
4532                     amount      => 0,
4533                     calls       => 0,
4534                     duration    => 0,
4535                     quantity    => '',
4536                     product_code => 'N/A',
4537                     section     => $section,
4538                     ext_description => [ $section->{'header'} ],
4539                     detail_temp => [],
4540             };
4541
4542             $section->{'amount'} += $amount;
4543             $accountcodes{$accountcode}{'amount'} += $amount;
4544             $accountcodes{$accountcode}{calls}++;
4545             $accountcodes{$accountcode}{duration} += $detail->duration;
4546             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4547         }
4548     }
4549
4550     foreach my $l ( values %accountcodes ) {
4551         $l->{amount} = sprintf( "%.2f", $l->{amount} );
4552         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4553         foreach my $sorted_detail ( @sorted_detail ) {
4554             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4555         }
4556         delete $l->{detail_temp};
4557         push @lines, $l;
4558     }
4559
4560     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4561
4562     return ($section,\@sorted_lines);
4563 }
4564
4565 sub _items_svc_phone_sections {
4566   my $self = shift;
4567   my $conf = $self->conf;
4568   my $escape = shift;
4569   my $format = shift;
4570
4571   my %sections = ();
4572   my %classnums = ();
4573   my %lines = ();
4574
4575   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4576
4577   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4578   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4579
4580   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4581     next unless $cust_bill_pkg->pkgnum > 0;
4582
4583     my @header = $cust_bill_pkg->details_header;
4584     next unless scalar(@header);
4585
4586     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4587
4588       my $phonenum = $detail->phonenum;
4589       next unless $phonenum;
4590
4591       my $amount = $detail->amount;
4592       next unless $amount && $amount > 0;
4593
4594       $sections{$phonenum} ||= { 'amount'      => 0,
4595                                  'calls'       => 0,
4596                                  'duration'    => 0,
4597                                  'sort_weight' => -1,
4598                                  'phonenum'    => $phonenum,
4599                                 };
4600       $sections{$phonenum}{amount} += $amount;  #subtotal
4601       $sections{$phonenum}{calls}++;
4602       $sections{$phonenum}{duration} += $detail->duration;
4603
4604       my $desc = $detail->regionname; 
4605       my $description = $desc;
4606       $description = substr($desc, 0, $maxlength). '...'
4607         if $format eq 'latex' && length($desc) > $maxlength;
4608
4609       $lines{$phonenum}{$desc} ||= {
4610         description     => &{$escape}($description),
4611         #pkgpart         => $part_pkg->pkgpart,
4612         pkgnum          => '',
4613         ref             => '',
4614         amount          => 0,
4615         calls           => 0,
4616         duration        => 0,
4617         #unit_amount     => '',
4618         quantity        => '',
4619         product_code    => 'N/A',
4620         ext_description => [],
4621       };
4622
4623       $lines{$phonenum}{$desc}{amount} += $amount;
4624       $lines{$phonenum}{$desc}{calls}++;
4625       $lines{$phonenum}{$desc}{duration} += $detail->duration;
4626
4627       my $line = $usage_class{$detail->classnum}->classname;
4628       $sections{"$phonenum $line"} ||=
4629         { 'amount' => 0,
4630           'calls' => 0,
4631           'duration' => 0,
4632           'sort_weight' => $usage_class{$detail->classnum}->weight,
4633           'phonenum' => $phonenum,
4634           'header'  => [ @header ],
4635         };
4636       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
4637       $sections{"$phonenum $line"}{calls}++;
4638       $sections{"$phonenum $line"}{duration} += $detail->duration;
4639
4640       $lines{"$phonenum $line"}{$desc} ||= {
4641         description     => &{$escape}($description),
4642         #pkgpart         => $part_pkg->pkgpart,
4643         pkgnum          => '',
4644         ref             => '',
4645         amount          => 0,
4646         calls           => 0,
4647         duration        => 0,
4648         #unit_amount     => '',
4649         quantity        => '',
4650         product_code    => 'N/A',
4651         ext_description => [],
4652       };
4653
4654       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4655       $lines{"$phonenum $line"}{$desc}{calls}++;
4656       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4657       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4658            $detail->formatted('format' => $format);
4659
4660     }
4661   }
4662
4663   my %sectionmap = ();
4664   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4665   foreach ( keys %sections ) {
4666     my @header = @{ $sections{$_}{header} || [] };
4667     my $usage_simple =
4668       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4669     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4670     my $usage_class = $summary ? $simple : $usage_simple;
4671     my $ending = $summary ? ' usage charges' : '';
4672     my %gen_opt = ();
4673     unless ($summary) {
4674       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4675     }
4676     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4677                         'amount'    => $sections{$_}{amount},    #subtotal
4678                         'calls'       => $sections{$_}{calls},
4679                         'duration'    => $sections{$_}{duration},
4680                         'summarized'  => '',
4681                         'tax_section' => '',
4682                         'phonenum'    => $sections{$_}{phonenum},
4683                         'sort_weight' => $sections{$_}{sort_weight},
4684                         'post_total'  => $summary, #inspire pagebreak
4685                         (
4686                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4687                             qw( description_generator
4688                                 header_generator
4689                                 total_generator
4690                                 total_line_generator
4691                               )
4692                           )
4693                         ), 
4694                       };
4695   }
4696
4697   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4698                         $a->{sort_weight} <=> $b->{sort_weight}
4699                       }
4700                  values %sectionmap;
4701
4702   my @lines = ();
4703   foreach my $section ( keys %lines ) {
4704     foreach my $line ( keys %{$lines{$section}} ) {
4705       my $l = $lines{$section}{$line};
4706       $l->{section}     = $sectionmap{$section};
4707       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4708       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4709       push @lines, $l;
4710     }
4711   }
4712   
4713   if($conf->exists('phone_usage_class_summary')) { 
4714       # this only works with Latex
4715       my @newlines;
4716       my @newsections;
4717
4718       # after this, we'll have only two sections per DID:
4719       # Calls Summary and Calls Detail
4720       foreach my $section ( @sections ) {
4721         if($section->{'post_total'}) {
4722             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4723             $section->{'total_line_generator'} = sub { '' };
4724             $section->{'total_generator'} = sub { '' };
4725             $section->{'header_generator'} = sub { '' };
4726             $section->{'description_generator'} = '';
4727             push @newsections, $section;
4728             my %calls_detail = %$section;
4729             $calls_detail{'post_total'} = '';
4730             $calls_detail{'sort_weight'} = '';
4731             $calls_detail{'description_generator'} = sub { '' };
4732             $calls_detail{'header_generator'} = sub {
4733                 return ' & Date/Time & Called Number & Duration & Price'
4734                     if $format eq 'latex';
4735                 '';
4736             };
4737             $calls_detail{'description'} = 'Calls Detail: '
4738                                                     . $section->{'phonenum'};
4739             push @newsections, \%calls_detail;  
4740         }
4741       }
4742
4743       # after this, each usage class is collapsed/summarized into a single
4744       # line under the Calls Summary section
4745       foreach my $newsection ( @newsections ) {
4746         if($newsection->{'post_total'}) { # this means Calls Summary
4747             foreach my $section ( @sections ) {
4748                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
4749                                 && !$section->{'post_total'});
4750                 my $newdesc = $section->{'description'};
4751                 my $tn = $section->{'phonenum'};
4752                 $newdesc =~ s/$tn//g;
4753                 my $line = {  ext_description => [],
4754                               pkgnum => '',
4755                               ref => '',
4756                               quantity => '',
4757                               calls => $section->{'calls'},
4758                               section => $newsection,
4759                               duration => $section->{'duration'},
4760                               description => $newdesc,
4761                               amount => sprintf("%.2f",$section->{'amount'}),
4762                               product_code => 'N/A',
4763                             };
4764                 push @newlines, $line;
4765             }
4766         }
4767       }
4768
4769       # after this, Calls Details is populated with all CDRs
4770       foreach my $newsection ( @newsections ) {
4771         if(!$newsection->{'post_total'}) { # this means Calls Details
4772             foreach my $line ( @lines ) {
4773                 next unless (scalar(@{$line->{'ext_description'}}) &&
4774                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4775                             );
4776                 my @extdesc = @{$line->{'ext_description'}};
4777                 my @newextdesc;
4778                 foreach my $extdesc ( @extdesc ) {
4779                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4780                     push @newextdesc, $extdesc;
4781                 }
4782                 $line->{'ext_description'} = \@newextdesc;
4783                 $line->{'section'} = $newsection;
4784                 push @newlines, $line;
4785             }
4786         }
4787       }
4788
4789       return(\@newsections, \@newlines);
4790   }
4791
4792   return(\@sections, \@lines);
4793
4794 }
4795
4796 sub _items { # seems to be unused
4797   my $self = shift;
4798
4799   #my @display = scalar(@_)
4800   #              ? @_
4801   #              : qw( _items_previous _items_pkg );
4802   #              #: qw( _items_pkg );
4803   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4804   my @display = qw( _items_previous _items_pkg );
4805
4806   my @b = ();
4807   foreach my $display ( @display ) {
4808     push @b, $self->$display(@_);
4809   }
4810   @b;
4811 }
4812
4813 sub _items_previous {
4814   my $self = shift;
4815   my $conf = $self->conf;
4816   my $cust_main = $self->cust_main;
4817   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4818   my @b = ();
4819   foreach ( @pr_cust_bill ) {
4820     my $date = $conf->exists('invoice_show_prior_due_date')
4821                ? 'due '. $_->due_date2str($date_format)
4822                : $self->time2str_local($date_format, $_->_date);
4823     push @b, {
4824       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4825       #'pkgpart'     => 'N/A',
4826       'pkgnum'      => 'N/A',
4827       'amount'      => sprintf("%.2f", $_->owed),
4828     };
4829   }
4830   @b;
4831
4832   #{
4833   #    'description'     => 'Previous Balance',
4834   #    #'pkgpart'         => 'N/A',
4835   #    'pkgnum'          => 'N/A',
4836   #    'amount'          => sprintf("%10.2f", $pr_total ),
4837   #    'ext_description' => [ map {
4838   #                                 "Invoice ". $_->invnum.
4839   #                                 " (". time2str("%x",$_->_date). ") ".
4840   #                                 sprintf("%10.2f", $_->owed)
4841   #                         } @pr_cust_bill ],
4842
4843   #};
4844 }
4845
4846 =item _items_pkg [ OPTIONS ]
4847
4848 Return line item hashes for each package item on this invoice. Nearly 
4849 equivalent to 
4850
4851 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4852
4853 The only OPTIONS accepted is 'section', which may point to a hashref 
4854 with a key named 'condensed', which may have a true value.  If it 
4855 does, this method tries to merge identical items into items with 
4856 'quantity' equal to the number of items (not the sum of their 
4857 separate quantities, for some reason).
4858
4859 =cut
4860
4861 sub _items_pkg {
4862   my $self = shift;
4863   my %options = @_;
4864
4865   warn "$me _items_pkg searching for all package line items\n"
4866     if $DEBUG > 1;
4867
4868   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4869
4870   warn "$me _items_pkg filtering line items\n"
4871     if $DEBUG > 1;
4872   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4873
4874   if ($options{section} && $options{section}->{condensed}) {
4875
4876     warn "$me _items_pkg condensing section\n"
4877       if $DEBUG > 1;
4878
4879     my %itemshash = ();
4880     local $Storable::canonical = 1;
4881     foreach ( @items ) {
4882       my $item = { %$_ };
4883       delete $item->{ref};
4884       delete $item->{ext_description};
4885       my $key = freeze($item);
4886       $itemshash{$key} ||= 0;
4887       $itemshash{$key} ++; # += $item->{quantity};
4888     }
4889     @items = sort { $a->{description} cmp $b->{description} }
4890              map { my $i = thaw($_);
4891                    $i->{quantity} = $itemshash{$_};
4892                    $i->{amount} =
4893                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4894                    $i;
4895                  }
4896              keys %itemshash;
4897   }
4898
4899   warn "$me _items_pkg returning ". scalar(@items). " items\n"
4900     if $DEBUG > 1;
4901
4902   @items;
4903 }
4904
4905 sub _taxsort {
4906   return 0 unless $a->itemdesc cmp $b->itemdesc;
4907   return -1 if $b->itemdesc eq 'Tax';
4908   return 1 if $a->itemdesc eq 'Tax';
4909   return -1 if $b->itemdesc eq 'Other surcharges';
4910   return 1 if $a->itemdesc eq 'Other surcharges';
4911   $a->itemdesc cmp $b->itemdesc;
4912 }
4913
4914 sub _items_tax {
4915   my $self = shift;
4916   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4917   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4918 }
4919
4920 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4921
4922 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4923 list of hashrefs describing the line items they generate on the invoice.
4924
4925 OPTIONS may include:
4926
4927 format: the invoice format.
4928
4929 escape_function: the function used to escape strings.
4930
4931 DEPRECATED? (expensive, mostly unused?)
4932 format_function: the function used to format CDRs.
4933
4934 section: a hashref containing 'description'; if this is present, 
4935 cust_bill_pkg_display records not belonging to this section are 
4936 ignored.
4937
4938 multisection: a flag indicating that this is a multisection invoice,
4939 which does something complicated.
4940
4941 multilocation: a flag to display the location label for the package.
4942
4943 Returns a list of hashrefs, each of which may contain:
4944
4945 pkgnum, description, amount, unit_amount, quantity, _is_setup, and 
4946 ext_description, which is an arrayref of detail lines to show below 
4947 the package line.
4948
4949 =cut
4950
4951 sub _items_cust_bill_pkg {
4952   my $self = shift;
4953   my $conf = $self->conf;
4954   my $cust_bill_pkgs = shift;
4955   my %opt = @_;
4956
4957   my $format = $opt{format} || '';
4958   my $escape_function = $opt{escape_function} || sub { shift };
4959   my $format_function = $opt{format_function} || '';
4960   my $no_usage = $opt{no_usage} || '';
4961   my $unsquelched = $opt{unsquelched} || ''; #unused
4962   my $section = $opt{section}->{description} if $opt{section};
4963   my $summary_page = $opt{summary_page} || ''; #unused
4964   my $multilocation = $opt{multilocation} || '';
4965   my $multisection = $opt{multisection} || '';
4966   my $discount_show_always = 0;
4967
4968   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4969
4970   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4971
4972   my @b = ();
4973   my ($s, $r, $u) = ( undef, undef, undef );
4974   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4975   {
4976
4977     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4978       if ( $_ && !$cust_bill_pkg->hidden ) {
4979         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4980         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4981         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4982         push @b, { %$_ }
4983           if $_->{amount} != 0
4984           || $discount_show_always
4985           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4986           || (   $_->{_is_setup} && $_->{setup_show_zero} )
4987         ;
4988         $_ = undef;
4989       }
4990     }
4991
4992     my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4993
4994     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4995          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4996       if $DEBUG > 1;
4997
4998     foreach my $display ( grep { defined($section)
4999                                  ? $_->section eq $section
5000                                  : 1
5001                                }
5002                           #grep { !$_->summary || !$summary_page } # bunk!
5003                           grep { !$_->summary || $multisection }
5004                           @cust_bill_pkg_display
5005                         )
5006     {
5007
5008       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
5009            $display->billpkgdisplaynum. "\n"
5010         if $DEBUG > 1;
5011
5012       my $type = $display->type;
5013
5014       my $desc = $cust_bill_pkg->desc;
5015       $desc = substr($desc, 0, $maxlength). '...'
5016         if $format eq 'latex' && length($desc) > $maxlength;
5017
5018       my %details_opt = ( 'format'          => $format,
5019                           'escape_function' => $escape_function,
5020                           'format_function' => $format_function,
5021                           'no_usage'        => $opt{'no_usage'},
5022                         );
5023
5024       if ( $cust_bill_pkg->pkgnum > 0 ) {
5025
5026         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
5027           if $DEBUG > 1;
5028  
5029         my $cust_pkg = $cust_bill_pkg->cust_pkg;
5030
5031         # which pkgpart to show for display purposes?
5032         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
5033
5034         # start/end dates for invoice formats that do nonstandard 
5035         # things with them
5036         my %item_dates = ();
5037         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
5038           unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
5039
5040         if (    (!$type || $type eq 'S')
5041              && (    $cust_bill_pkg->setup != 0
5042                   || $cust_bill_pkg->setup_show_zero
5043                 )
5044            )
5045          {
5046
5047           warn "$me _items_cust_bill_pkg adding setup\n"
5048             if $DEBUG > 1;
5049
5050           my $description = $desc;
5051           $description .= ' Setup'
5052             if $cust_bill_pkg->recur != 0
5053             || $discount_show_always
5054             || $cust_bill_pkg->recur_show_zero;
5055
5056           my @d = ();
5057           my $svc_label;
5058           unless ( $cust_pkg->part_pkg->hide_svc_detail
5059                 || $cust_bill_pkg->hidden )
5060           {
5061
5062             my @svc_labels = map &{$escape_function}($_),
5063                         $cust_pkg->h_labels_short($self->_date, undef, 'I');
5064             push @d, @svc_labels
5065               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5066             $svc_label = $svc_labels[0];
5067
5068             if ( $multilocation ) {
5069               my $loc = $cust_pkg->location_label;
5070               $loc = substr($loc, 0, $maxlength). '...'
5071                 if $format eq 'latex' && length($loc) > $maxlength;
5072               push @d, &{$escape_function}($loc);
5073             }
5074
5075           } #unless hiding service details
5076
5077           push @d, $cust_bill_pkg->details(%details_opt)
5078             if $cust_bill_pkg->recur == 0;
5079
5080           if ( $cust_bill_pkg->hidden ) {
5081             $s->{amount}      += $cust_bill_pkg->setup;
5082             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5083             push @{ $s->{ext_description} }, @d;
5084           } else {
5085             $s = {
5086               _is_setup       => 1,
5087               description     => $description,
5088               pkgpart         => $pkgpart,
5089               pkgnum          => $cust_bill_pkg->pkgnum,
5090               amount          => $cust_bill_pkg->setup,
5091               setup_show_zero => $cust_bill_pkg->setup_show_zero,
5092               unit_amount     => $cust_bill_pkg->unitsetup,
5093               quantity        => $cust_bill_pkg->quantity,
5094               ext_description => \@d,
5095               svc_label       => ($svc_label || ''),
5096             };
5097           };
5098
5099         }
5100
5101         if (    ( !$type || $type eq 'R' || $type eq 'U' )
5102              && (
5103                      $cust_bill_pkg->recur != 0
5104                   || $cust_bill_pkg->setup == 0
5105                   || $discount_show_always
5106                   || $cust_bill_pkg->recur_show_zero
5107                 )
5108            )
5109         {
5110
5111           warn "$me _items_cust_bill_pkg adding recur/usage\n"
5112             if $DEBUG > 1;
5113
5114           my $is_summary = $display->summary;
5115           my $description = ($is_summary && $type && $type eq 'U')
5116                             ? "Usage charges" : $desc;
5117
5118           my $part_pkg = $cust_pkg->part_pkg;
5119
5120           #pry be a bit more efficient to look some of this conf stuff up
5121           # outside the loop
5122           unless (
5123             $conf->exists('disable_line_item_date_ranges')
5124               || $part_pkg->option('disable_line_item_date_ranges',1)
5125               || ! $cust_bill_pkg->sdate
5126               || ! $cust_bill_pkg->edate
5127           ) {
5128             my $time_period;
5129             my $date_style = '';                                               
5130             $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monthly',
5131                                          $cust_main->agentnum                  
5132                                        )                                       
5133               if $part_pkg && $part_pkg->freq !~ /^1m?$/;                      
5134             $date_style ||= $conf->config( 'cust_bill-line_item-date_style',   
5135                                             $cust_main->agentnum
5136                                           );
5137             if ( defined($date_style) && $date_style eq 'month_of' ) {
5138               $time_period = $self->time2str_local('The month of %B', $cust_bill_pkg->sdate);
5139             } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5140               my $desc = $conf->config( 'cust_bill-line_item-date_description',
5141                                          $cust_main->agentnum
5142                                       );
5143               $desc .= ' ' unless $desc =~ /\s$/;
5144               $time_period = $desc. $self->time2str_local('%B', $cust_bill_pkg->sdate);
5145             } else {
5146               $time_period =      $self->time2str_local($date_format, $cust_bill_pkg->sdate).
5147                            " - ". $self->time2str_local($date_format, $cust_bill_pkg->edate);
5148             }
5149             $description .= " ($time_period)";
5150           }
5151
5152           my @d = ();
5153           my @seconds = (); # for display of usage info
5154           my $svc_label = '';
5155
5156           #at least until cust_bill_pkg has "past" ranges in addition to
5157           #the "future" sdate/edate ones... see #3032
5158           my @dates = ( $self->_date );
5159           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5160           push @dates, $prev->sdate if $prev;
5161           push @dates, undef if !$prev;
5162
5163           unless ( $cust_pkg->part_pkg->hide_svc_detail
5164                 || $cust_bill_pkg->itemdesc
5165                 || $cust_bill_pkg->hidden
5166                 || $is_summary && $type && $type eq 'U' )
5167           {
5168
5169             warn "$me _items_cust_bill_pkg adding service details\n"
5170               if $DEBUG > 1;
5171
5172             my @svc_labels = map &{$escape_function}($_),
5173                         $cust_pkg->h_labels_short(@dates, 'I');
5174             push @d, @svc_labels
5175               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5176             $svc_label = $svc_labels[0];
5177
5178             warn "$me _items_cust_bill_pkg done adding service details\n"
5179               if $DEBUG > 1;
5180
5181             if ( $multilocation ) {
5182               my $loc = $cust_pkg->location_label;
5183               $loc = substr($loc, 0, $maxlength). '...'
5184                 if $format eq 'latex' && length($loc) > $maxlength;
5185               push @d, &{$escape_function}($loc);
5186             }
5187
5188             # Display of seconds_since_sqlradacct:
5189             # On the invoice, when processing @detail_items, look for a field
5190             # named 'seconds'.  This will contain total seconds for each 
5191             # service, in the same order as @ext_description.  For services 
5192             # that don't support this it will show undef.
5193             if ( $conf->exists('svc_acct-usage_seconds') 
5194                  and ! $cust_bill_pkg->pkgpart_override ) {
5195               foreach my $cust_svc ( 
5196                   $cust_pkg->h_cust_svc(@dates, 'I') 
5197                 ) {
5198
5199                 # eval because not having any part_export_usage exports 
5200                 # is a fatal error, last_bill/_date because that's how 
5201                 # sqlradius_hour billing does it
5202                 my $sec = eval {
5203                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5204                 };
5205                 push @seconds, $sec;
5206               }
5207             } #if svc_acct-usage_seconds
5208
5209           }
5210
5211           unless ( $is_summary ) {
5212             warn "$me _items_cust_bill_pkg adding details\n"
5213               if $DEBUG > 1;
5214
5215             #instead of omitting details entirely in this case (unwanted side
5216             # effects), just omit CDRs
5217             $details_opt{'no_usage'} = 1
5218               if $type && $type eq 'R';
5219
5220             push @d, $cust_bill_pkg->details(%details_opt);
5221           }
5222
5223           warn "$me _items_cust_bill_pkg calculating amount\n"
5224             if $DEBUG > 1;
5225   
5226           my $amount = 0;
5227           if (!$type) {
5228             $amount = $cust_bill_pkg->recur;
5229           } elsif ($type eq 'R') {
5230             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5231           } elsif ($type eq 'U') {
5232             $amount = $cust_bill_pkg->usage;
5233           }
5234   
5235           if ( !$type || $type eq 'R' ) {
5236
5237             warn "$me _items_cust_bill_pkg adding recur\n"
5238               if $DEBUG > 1;
5239
5240             if ( $cust_bill_pkg->hidden ) {
5241               $r->{amount}      += $amount;
5242               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5243               push @{ $r->{ext_description} }, @d;
5244             } else {
5245               $r = {
5246                 description     => $description,
5247                 pkgpart         => $pkgpart,
5248                 pkgnum          => $cust_bill_pkg->pkgnum,
5249                 amount          => $amount,
5250                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5251                 unit_amount     => $cust_bill_pkg->unitrecur,
5252                 quantity        => $cust_bill_pkg->quantity,
5253                 %item_dates,
5254                 ext_description => \@d,
5255                 svc_label       => ($svc_label || ''),
5256               };
5257               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5258             }
5259
5260           } else {  # $type eq 'U'
5261
5262             warn "$me _items_cust_bill_pkg adding usage\n"
5263               if $DEBUG > 1;
5264
5265             if ( $cust_bill_pkg->hidden ) {
5266               $u->{amount}      += $amount;
5267               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5268               push @{ $u->{ext_description} }, @d;
5269             } else {
5270               $u = {
5271                 description     => $description,
5272                 pkgpart         => $pkgpart,
5273                 pkgnum          => $cust_bill_pkg->pkgnum,
5274                 amount          => $amount,
5275                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5276                 unit_amount     => $cust_bill_pkg->unitrecur,
5277                 quantity        => $cust_bill_pkg->quantity,
5278                 %item_dates,
5279                 ext_description => \@d,
5280               };
5281             }
5282           }
5283
5284         } # recurring or usage with recurring charge
5285
5286       } else { #pkgnum tax or one-shot line item (??)
5287
5288         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5289           if $DEBUG > 1;
5290
5291         if ( $cust_bill_pkg->setup != 0 ) {
5292           push @b, {
5293             'description' => $desc,
5294             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
5295           };
5296         }
5297         if ( $cust_bill_pkg->recur != 0 ) {
5298           push @b, {
5299             'description' => "$desc (".
5300                              $self->time2str_local($date_format, $cust_bill_pkg->sdate). ' - '.
5301                              $self->time2str_local($date_format, $cust_bill_pkg->edate). ')',
5302             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
5303           };
5304         }
5305
5306       }
5307
5308     }
5309
5310     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5311                                 && $conf->exists('discount-show-always'));
5312
5313   }
5314
5315   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5316     if ( $_  ) {
5317       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
5318       $_->{amount}      =~ s/^\-0\.00$/0.00/;
5319       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5320       push @b, { %$_ }
5321         if $_->{amount} != 0
5322         || $discount_show_always
5323         || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5324         || (   $_->{_is_setup} && $_->{setup_show_zero} )
5325     }
5326   }
5327
5328   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5329     if $DEBUG > 1;
5330
5331   @b;
5332
5333 }
5334
5335 sub _items_credits {
5336   my( $self, %opt ) = @_;
5337   my $trim_len = $opt{'trim_len'} || 60;
5338
5339   my @b;
5340   #credits
5341   my @objects;
5342   if ( $self->conf->exists('previous_balance-payments_since') ) {
5343     if ( $opt{'template'} eq 'statement' ) {
5344       # then the current bill is a "statement" (i.e. an invoice sent as
5345       # a payment receipt)
5346       # and in that case we want to see payments on or after THIS invoice
5347       @objects = qsearch('cust_credit', {
5348           'custnum' => $self->custnum,
5349           '_date'   => {op => '>=', value => $self->_date},
5350       });
5351     } else {
5352       my $date = 0;
5353       $date = $self->previous_bill->_date if $self->previous_bill;
5354       @objects = qsearch('cust_credit', {
5355           'custnum' => $self->custnum,
5356           '_date'   => {op => '>=', value => $date},
5357       });
5358     }
5359   } else {
5360     @objects = $self->cust_credited;
5361   }
5362
5363   foreach my $obj ( @objects ) {
5364     my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
5365
5366     my $reason = substr($cust_credit->reason, 0, $trim_len);
5367     $reason .= '...' if length($reason) < length($cust_credit->reason);
5368     $reason = " ($reason) " if $reason;
5369
5370     push @b, {
5371       #'description' => 'Credit ref\#'. $_->crednum.
5372       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
5373       #                 $reason,
5374       'description' => $self->mt('Credit applied').' '.
5375                        $self->time2str_local($date_format,$obj->_date). $reason,
5376       'amount'      => sprintf("%.2f",$obj->amount),
5377     };
5378   }
5379
5380   @b;
5381
5382 }
5383
5384 sub _items_payments {
5385   my $self = shift;
5386   my %opt = @_;
5387
5388   my @b;
5389   my $detailed = $self->conf->exists('invoice_payment_details');
5390   my @objects;
5391   if ( $self->conf->exists('previous_balance-payments_since') ) {
5392     # then show payments dated on/after the previous bill...
5393     if ( $opt{'template'} eq 'statement' ) {
5394       # then the current bill is a "statement" (i.e. an invoice sent as
5395       # a payment receipt)
5396       # and in that case we want to see payments on or after THIS invoice
5397       @objects = qsearch('cust_pay', {
5398           'custnum' => $self->custnum,
5399           '_date'   => {op => '>=', value => $self->_date},
5400       });
5401     } else {
5402       # the normal case: payments on or after the previous invoice
5403       my $date = 0;
5404       $date = $self->previous_bill->_date if $self->previous_bill;
5405       @objects = qsearch('cust_pay', {
5406         'custnum' => $self->custnum,
5407         '_date'   => {op => '>=', value => $date},
5408       });
5409       # and before the current bill...
5410       @objects = grep { $_->_date < $self->_date } @objects;
5411     }
5412   } else {
5413     @objects = $self->cust_bill_pay;
5414   }
5415
5416   foreach my $obj (@objects) {
5417     my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
5418     my $desc = $self->mt('Payment received').' '.
5419                $self->time2str_local($date_format, $cust_pay->_date );
5420     $desc .= $self->mt(' via ') .
5421              $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
5422       if $detailed;
5423
5424     push @b, {
5425       'description' => $desc,
5426       'amount'      => sprintf("%.2f", $obj->amount )
5427     };
5428   }
5429
5430   @b;
5431
5432 }
5433
5434 =item _items_discounts_avail
5435
5436 Returns an array of line item hashrefs representing available term discounts
5437 for this invoice.  This makes the same assumptions that apply to term 
5438 discounts in general: that the package is billed monthly, at a flat rate, 
5439 with no usage charges.  A prorated first month will be handled, as will 
5440 a setup fee if the discount is allowed to apply to setup fees.
5441
5442 =cut
5443
5444 sub _items_discounts_avail {
5445   my $self = shift;
5446   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5447
5448   my %plans = $self->discount_plans;
5449
5450   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5451
5452   map {
5453     my $months = $_;
5454     my $plan = $plans{$months};
5455
5456     my $term_total = sprintf('%.2f', $plan->discounted_total);
5457     my $percent = sprintf('%.0f', 
5458                           100 * (1 - $term_total / $plan->base_total) );
5459     my $permonth = sprintf('%.2f', $term_total / $months);
5460     my $detail = $self->mt('discount on item'). ' '.
5461                  join(', ', map { "#$_" } $plan->pkgnums)
5462       if $list_pkgnums;
5463
5464     # discounts for non-integer months don't work anyway
5465     $months = sprintf("%d", $months);
5466
5467     +{
5468       description => $self->mt('Save [_1]% by paying for [_2] months',
5469                                 $percent, $months),
5470       amount      => $self->mt('[_1] ([_2] per month)', 
5471                                 $term_total, $money_char.$permonth),
5472       ext_description => ($detail || ''),
5473     }
5474   } #map
5475   sort { $b <=> $a } keys %plans;
5476
5477 }
5478
5479 =item call_details [ OPTION => VALUE ... ]
5480
5481 Returns an array of CSV strings representing the call details for this invoice
5482 The only option available is the boolean prepend_billed_number
5483
5484 =cut
5485
5486 sub call_details {
5487   my ($self, %opt) = @_;
5488
5489   my $format_function = sub { shift };
5490
5491   if ($opt{prepend_billed_number}) {
5492     $format_function = sub {
5493       my $detail = shift;
5494       my $row = shift;
5495
5496       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5497       
5498     };
5499   }
5500
5501   my @details = map { $_->details( 'format_function' => $format_function,
5502                                    'escape_function' => sub{ return() },
5503                                  )
5504                     }
5505                   grep { $_->pkgnum }
5506                   $self->cust_bill_pkg;
5507   my $header = $details[0];
5508   ( $header, grep { $_ ne $header } @details );
5509 }
5510
5511
5512 =back
5513
5514 =head1 SUBROUTINES
5515
5516 =over 4
5517
5518 =item process_reprint
5519
5520 =cut
5521
5522 sub process_reprint {
5523   process_re_X('print', @_);
5524 }
5525
5526 =item process_reemail
5527
5528 =cut
5529
5530 sub process_reemail {
5531   process_re_X('email', @_);
5532 }
5533
5534 =item process_refax
5535
5536 =cut
5537
5538 sub process_refax {
5539   process_re_X('fax', @_);
5540 }
5541
5542 =item process_reftp
5543
5544 =cut
5545
5546 sub process_reftp {
5547   process_re_X('ftp', @_);
5548 }
5549
5550 =item respool
5551
5552 =cut
5553
5554 sub process_respool {
5555   process_re_X('spool', @_);
5556 }
5557
5558 use Storable qw(thaw);
5559 use Data::Dumper;
5560 use MIME::Base64;
5561 sub process_re_X {
5562   my( $method, $job ) = ( shift, shift );
5563   warn "$me process_re_X $method for job $job\n" if $DEBUG;
5564
5565   my $param = thaw(decode_base64(shift));
5566   warn Dumper($param) if $DEBUG;
5567
5568   re_X(
5569     $method,
5570     $job,
5571     %$param,
5572   );
5573
5574 }
5575
5576 sub re_X {
5577   my($method, $job, %param ) = @_;
5578   if ( $DEBUG ) {
5579     warn "re_X $method for job $job with param:\n".
5580          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
5581   }
5582
5583   #some false laziness w/search/cust_bill.html
5584   my $distinct = '';
5585   my $orderby = 'ORDER BY cust_bill._date';
5586
5587   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5588
5589   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5590      
5591   my @cust_bill = qsearch( {
5592     #'select'    => "cust_bill.*",
5593     'table'     => 'cust_bill',
5594     'addl_from' => $addl_from,
5595     'hashref'   => {},
5596     'extra_sql' => $extra_sql,
5597     'order_by'  => $orderby,
5598     'debug' => 1,
5599   } );
5600
5601   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5602
5603   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5604     if $DEBUG;
5605
5606   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5607   foreach my $cust_bill ( @cust_bill ) {
5608     $cust_bill->$method();
5609
5610     if ( $job ) { #progressbar foo
5611       $num++;
5612       if ( time - $min_sec > $last ) {
5613         my $error = $job->update_statustext(
5614           int( 100 * $num / scalar(@cust_bill) )
5615         );
5616         die $error if $error;
5617         $last = time;
5618       }
5619     }
5620
5621   }
5622
5623 }
5624
5625 =back
5626
5627 =head1 CLASS METHODS
5628
5629 =over 4
5630
5631 =item owed_sql
5632
5633 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5634
5635 =cut
5636
5637 sub owed_sql {
5638   my ($class, $start, $end) = @_;
5639   'charged - '. 
5640     $class->paid_sql($start, $end). ' - '. 
5641     $class->credited_sql($start, $end);
5642 }
5643
5644 =item net_sql
5645
5646 Returns an SQL fragment to retreive the net amount (charged minus credited).
5647
5648 =cut
5649
5650 sub net_sql {
5651   my ($class, $start, $end) = @_;
5652   'charged - '. $class->credited_sql($start, $end);
5653 }
5654
5655 =item paid_sql
5656
5657 Returns an SQL fragment to retreive the amount paid against this invoice.
5658
5659 =cut
5660
5661 sub paid_sql {
5662   my ($class, $start, $end) = @_;
5663   $start &&= "AND cust_bill_pay._date <= $start";
5664   $end   &&= "AND cust_bill_pay._date > $end";
5665   $start = '' unless defined($start);
5666   $end   = '' unless defined($end);
5667   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5668        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
5669 }
5670
5671 =item credited_sql
5672
5673 Returns an SQL fragment to retreive the amount credited against this invoice.
5674
5675 =cut
5676
5677 sub credited_sql {
5678   my ($class, $start, $end) = @_;
5679   $start &&= "AND cust_credit_bill._date <= $start";
5680   $end   &&= "AND cust_credit_bill._date >  $end";
5681   $start = '' unless defined($start);
5682   $end   = '' unless defined($end);
5683   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5684        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
5685 }
5686
5687 =item due_date_sql
5688
5689 Returns an SQL fragment to retrieve the due date of an invoice.
5690 Currently only supported on PostgreSQL.
5691
5692 =cut
5693
5694 sub due_date_sql {
5695   my $conf = new FS::Conf;
5696 'COALESCE(
5697   SUBSTRING(
5698     COALESCE(
5699       cust_bill.invoice_terms,
5700       cust_main.invoice_terms,
5701       \''.($conf->config('invoice_default_terms') || '').'\'
5702     ), E\'Net (\\\\d+)\'
5703   )::INTEGER, 0
5704 ) * 86400 + cust_bill._date'
5705 }
5706
5707 =item search_sql_where HASHREF
5708
5709 Class method which returns an SQL WHERE fragment to search for parameters
5710 specified in HASHREF.  Valid parameters are
5711
5712 =over 4
5713
5714 =item _date
5715
5716 List reference of start date, end date, as UNIX timestamps.
5717
5718 =item invnum_min
5719
5720 =item invnum_max
5721
5722 =item agentnum
5723
5724 =item charged
5725
5726 List reference of charged limits (exclusive).
5727
5728 =item owed
5729
5730 List reference of charged limits (exclusive).
5731
5732 =item open
5733
5734 flag, return open invoices only
5735
5736 =item net
5737
5738 flag, return net invoices only
5739
5740 =item days
5741
5742 =item newest_percust
5743
5744 =back
5745
5746 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5747
5748 =cut
5749
5750 sub search_sql_where {
5751   my($class, $param) = @_;
5752   if ( $DEBUG ) {
5753     warn "$me search_sql_where called with params: \n".
5754          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
5755   }
5756
5757   my @search = ();
5758
5759   #agentnum
5760   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5761     push @search, "cust_main.agentnum = $1";
5762   }
5763
5764   #refnum
5765   if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5766     push @search, "cust_main.refnum = $1";
5767   }
5768
5769   #custnum
5770   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5771     push @search, "cust_bill.custnum = $1";
5772   }
5773
5774   #customer classnum
5775   if ( $param->{'cust_classnum'} ) {
5776     my $classnums = $param->{'cust_classnum'};
5777     $classnums = [ $classnums ] if !ref($classnums);
5778     $classnums = [ grep /^\d+$/, @$classnums ];
5779     push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
5780       if @$classnums;
5781   }
5782
5783   #_date
5784   if ( $param->{_date} ) {
5785     my($beginning, $ending) = @{$param->{_date}};
5786
5787     push @search, "cust_bill._date >= $beginning",
5788                   "cust_bill._date <  $ending";
5789   }
5790
5791   #invnum
5792   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5793     push @search, "cust_bill.invnum >= $1";
5794   }
5795   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5796     push @search, "cust_bill.invnum <= $1";
5797   }
5798
5799   #charged
5800   if ( $param->{charged} ) {
5801     my @charged = ref($param->{charged})
5802                     ? @{ $param->{charged} }
5803                     : ($param->{charged});
5804
5805     push @search, map { s/^charged/cust_bill.charged/; $_; }
5806                       @charged;
5807   }
5808
5809   my $owed_sql = FS::cust_bill->owed_sql;
5810
5811   #owed
5812   if ( $param->{owed} ) {
5813     my @owed = ref($param->{owed})
5814                  ? @{ $param->{owed} }
5815                  : ($param->{owed});
5816     push @search, map { s/^owed/$owed_sql/; $_; }
5817                       @owed;
5818   }
5819
5820   #open/net flags
5821   push @search, "0 != $owed_sql"
5822     if $param->{'open'};
5823   push @search, '0 != '. FS::cust_bill->net_sql
5824     if $param->{'net'};
5825
5826   #days
5827   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5828     if $param->{'days'};
5829
5830   #newest_percust
5831   if ( $param->{'newest_percust'} ) {
5832
5833     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5834     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5835
5836     my @newest_where = map { my $x = $_;
5837                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
5838                              $x;
5839                            }
5840                            grep ! /^cust_main./, @search;
5841     my $newest_where = scalar(@newest_where)
5842                          ? ' AND '. join(' AND ', @newest_where)
5843                          : '';
5844
5845
5846     push @search, "cust_bill._date = (
5847       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5848         WHERE newest_cust_bill.custnum = cust_bill.custnum
5849           $newest_where
5850     )";
5851
5852   }
5853
5854   #promised_date - also has an option to accept nulls
5855   if ( $param->{promised_date} ) {
5856     my($beginning, $ending, $null) = @{$param->{promised_date}};
5857
5858     push @search, "(( cust_bill.promised_date >= $beginning AND ".
5859                     "cust_bill.promised_date <  $ending )" .
5860                     ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5861   }
5862
5863   #agent virtualization
5864   my $curuser = $FS::CurrentUser::CurrentUser;
5865   if ( $curuser->username eq 'fs_queue'
5866        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5867     my $username = $1;
5868     my $newuser = qsearchs('access_user', {
5869       'username' => $username,
5870       'disabled' => '',
5871     } );
5872     if ( $newuser ) {
5873       $curuser = $newuser;
5874     } else {
5875       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5876     }
5877   }
5878   push @search, $curuser->agentnums_sql;
5879
5880   join(' AND ', @search );
5881
5882 }
5883
5884 =back
5885
5886 =head1 BUGS
5887
5888 The delete method.
5889
5890 =head1 SEE ALSO
5891
5892 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5893 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
5894 documentation.
5895
5896 =cut
5897
5898 1;
5899