localization hook for payinfo descriptions, #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   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1749   my $file = "$spooldir/$tracctnum.csv";
1750   
1751   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1752
1753   open(CSV, ">$file") or die "can't open $file: $!";
1754   print CSV $header;
1755
1756   print CSV $detail;
1757
1758   close CSV;
1759
1760   my $net;
1761   if ( $opt{protocol} eq 'ftp' ) {
1762     eval "use Net::FTP;";
1763     die $@ if $@;
1764     $net = Net::FTP->new($opt{server}) or die @$;
1765   } else {
1766     die "unknown protocol: $opt{protocol}";
1767   }
1768
1769   $net->login( $opt{username}, $opt{password} )
1770     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1771
1772   $net->binary or die "can't set binary mode";
1773
1774   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1775
1776   $net->put($file) or die "can't put $file: $!";
1777
1778   $net->quit;
1779
1780   unlink $file;
1781
1782 }
1783
1784 =item spool_csv
1785
1786 Spools CSV invoice data.
1787
1788 Options are:
1789
1790 =over 4
1791
1792 =item format - 'default' or 'billco'
1793
1794 =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>).
1795
1796 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1797
1798 =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.
1799
1800 =back
1801
1802 =cut
1803
1804 sub spool_csv {
1805   my($self, %opt) = @_;
1806
1807   my $cust_main = $self->cust_main;
1808
1809   if ( $opt{'dest'} ) {
1810     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1811                              $cust_main->invoicing_list;
1812     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1813                      || ! keys %invoicing_list;
1814   }
1815
1816   if ( $opt{'balanceover'} ) {
1817     return 'N/A'
1818       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1819   }
1820
1821   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1822   mkdir $spooldir, 0700 unless -d $spooldir;
1823
1824   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1825
1826   my $file =
1827     "$spooldir/".
1828     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1829     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1830     '.csv';
1831   
1832   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1833
1834   open(CSV, ">>$file") or die "can't open $file: $!";
1835   flock(CSV, LOCK_EX);
1836   seek(CSV, 0, 2);
1837
1838   print CSV $header;
1839
1840   if ( lc($opt{'format'}) eq 'billco' ) {
1841
1842     flock(CSV, LOCK_UN);
1843     close CSV;
1844
1845     $file =
1846       "$spooldir/".
1847       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1848       '-detail.csv';
1849
1850     open(CSV,">>$file") or die "can't open $file: $!";
1851     flock(CSV, LOCK_EX);
1852     seek(CSV, 0, 2);
1853   }
1854
1855   print CSV $detail;
1856
1857   flock(CSV, LOCK_UN);
1858   close CSV;
1859
1860   return '';
1861
1862 }
1863
1864 =item print_csv OPTION => VALUE, ...
1865
1866 Returns CSV data for this invoice.
1867
1868 Options are:
1869
1870 format - 'default' or 'billco'
1871
1872 Returns a list consisting of two scalars.  The first is a single line of CSV
1873 header information for this invoice.  The second is one or more lines of CSV
1874 detail information for this invoice.
1875
1876 If I<format> is not specified or "default", the fields of the CSV file are as
1877 follows:
1878
1879 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1880
1881 =over 4
1882
1883 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1884
1885 B<record_type> is C<cust_bill> for the initial header line only.  The
1886 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1887 fields are filled in.
1888
1889 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1890 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1891 are filled in.
1892
1893 =item invnum - invoice number
1894
1895 =item custnum - customer number
1896
1897 =item _date - invoice date
1898
1899 =item charged - total invoice amount
1900
1901 =item first - customer first name
1902
1903 =item last - customer first name
1904
1905 =item company - company name
1906
1907 =item address1 - address line 1
1908
1909 =item address2 - address line 1
1910
1911 =item city
1912
1913 =item state
1914
1915 =item zip
1916
1917 =item country
1918
1919 =item pkg - line item description
1920
1921 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1922
1923 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1924
1925 =item sdate - start date for recurring fee
1926
1927 =item edate - end date for recurring fee
1928
1929 =back
1930
1931 If I<format> is "billco", the fields of the header CSV file are as follows:
1932
1933   +-------------------------------------------------------------------+
1934   |                        FORMAT HEADER FILE                         |
1935   |-------------------------------------------------------------------|
1936   | Field | Description                   | Name       | Type | Width |
1937   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1938   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1939   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1940   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1941   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1942   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1943   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1944   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1945   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1946   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1947   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1948   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1949   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1950   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1951   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1952   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1953   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1954   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1955   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1956   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1957   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1958   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1959   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1960   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1961   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1962   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1963   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1964   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1965   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1966   +-------+-------------------------------+------------+------+-------+
1967
1968 If I<format> is "billco", the fields of the detail CSV file are as follows:
1969
1970                                   FORMAT FOR DETAIL FILE
1971         |                            |           |      |
1972   Field | Description                | Name      | Type | Width
1973   1     | N/A-Leave Empty            | RC        | CHAR |     2
1974   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1975   3     | Account Number             | TRACCTNUM | CHAR |    15
1976   4     | Invoice Number             | TRINVOICE | CHAR |    15
1977   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1978   6     | Transaction Detail         | DETAILS   | CHAR |   100
1979   7     | Amount                     | AMT       | NUM* |     9
1980   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1981   9     | Grouping Code              | GROUP     | CHAR |     2
1982   10    | User Defined               | ACCT CODE | CHAR |    15
1983
1984 =cut
1985
1986 sub print_csv {
1987   my($self, %opt) = @_;
1988   
1989   eval "use Text::CSV_XS";
1990   die $@ if $@;
1991
1992   my $cust_main = $self->cust_main;
1993
1994   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1995
1996   if ( lc($opt{'format'}) eq 'billco' ) {
1997
1998     my $taxtotal = 0;
1999     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
2000
2001     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
2002
2003     my( $previous_balance, @unused ) = $self->previous; #previous balance
2004
2005     my $pmt_cr_applied = 0;
2006     $pmt_cr_applied += $_->{'amount'}
2007       foreach ( $self->_items_payments, $self->_items_credits ) ;
2008
2009     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2010
2011     $csv->combine(
2012       '',                         #  1 | N/A-Leave Empty               CHAR   2
2013       '',                         #  2 | N/A-Leave Empty               CHAR  15
2014       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
2015       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
2016       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
2017       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
2018       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
2019       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
2020       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
2021       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
2022       '',                         # 10 | Ancillary Billing Information CHAR  30
2023       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
2024       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
2025
2026       # XXX ?
2027       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
2028
2029       # XXX ?
2030       $duedate,                   # 14 | Bill Due Date                 CHAR  10
2031
2032       $previous_balance,          # 15 | Previous Balance              NUM*   9
2033       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
2034       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
2035       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
2036       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
2037       '',                         # 20 | 30 Day Aging                  NUM*   9
2038       '',                         # 21 | 60 Day Aging                  NUM*   9
2039       '',                         # 22 | 90 Day Aging                  NUM*   9
2040       'N',                        # 23 | Y/N                           CHAR   1
2041       '',                         # 24 | Remittance automation         CHAR 100
2042       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
2043       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
2044       '0',                        # 27 | Federal Tax***                NUM*   9
2045       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
2046       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
2047     );
2048
2049   } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2050   
2051     my ($previous_balance) = $self->previous; 
2052     $previous_balance = sprintf('%.2f', $previous_balance);
2053     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2054     my @items = map {
2055                       $_->{pkgnum},
2056                       $_->{description},
2057                       $_->{amount}
2058                     }
2059                   $self->_items_pkg, #_items_nontax?  no sections or anything
2060                                      # with this format
2061                   $self->_items_tax;
2062
2063     $csv->combine(
2064       $cust_main->agentnum,
2065       $cust_main->agent->agent,
2066       $self->custnum,
2067       $cust_main->first,
2068       $cust_main->last,
2069       $cust_main->company,
2070       $cust_main->address1,
2071       $cust_main->address2,
2072       $cust_main->city,
2073       $cust_main->state,
2074       $cust_main->zip,
2075
2076       # invoice fields
2077       time2str("%x", $self->_date),
2078       $self->invnum,
2079       $self->charged,
2080       $totaldue,
2081       $previous_balance,
2082       $self->due_date2str("%x"),
2083
2084       @items,
2085     );
2086
2087   } else {
2088   
2089     $csv->combine(
2090       'cust_bill',
2091       $self->invnum,
2092       $self->custnum,
2093       time2str("%x", $self->_date),
2094       sprintf("%.2f", $self->charged),
2095       ( map { $cust_main->getfield($_) }
2096           qw( first last company address1 address2 city state zip country ) ),
2097       map { '' } (1..5),
2098     ) or die "can't create csv";
2099   }
2100
2101   my $header = $csv->string. "\n";
2102
2103   my $detail = '';
2104   if ( lc($opt{'format'}) eq 'billco' ) {
2105
2106     my $lineseq = 0;
2107     foreach my $item ( $self->_items_pkg ) {
2108
2109       $csv->combine(
2110         '',                     #  1 | N/A-Leave Empty            CHAR   2
2111         '',                     #  2 | N/A-Leave Empty            CHAR  15
2112         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
2113         $self->invnum,          #  4 | Invoice Number             CHAR  15
2114         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
2115         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
2116         $item->{'amount'},      #  7 | Amount                     NUM*   9
2117         '',                     #  8 | Line Format Control**      CHAR   2
2118         '',                     #  9 | Grouping Code              CHAR   2
2119         '',                     # 10 | User Defined               CHAR  15
2120       );
2121
2122       $detail .= $csv->string. "\n";
2123
2124     }
2125
2126   } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2127
2128     #do nothing
2129
2130   } else {
2131
2132     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2133
2134       my($pkg, $setup, $recur, $sdate, $edate);
2135       if ( $cust_bill_pkg->pkgnum ) {
2136       
2137         ($pkg, $setup, $recur, $sdate, $edate) = (
2138           $cust_bill_pkg->part_pkg->pkg,
2139           ( $cust_bill_pkg->setup != 0
2140             ? sprintf("%.2f", $cust_bill_pkg->setup )
2141             : '' ),
2142           ( $cust_bill_pkg->recur != 0
2143             ? sprintf("%.2f", $cust_bill_pkg->recur )
2144             : '' ),
2145           ( $cust_bill_pkg->sdate 
2146             ? time2str("%x", $cust_bill_pkg->sdate)
2147             : '' ),
2148           ($cust_bill_pkg->edate 
2149             ?time2str("%x", $cust_bill_pkg->edate)
2150             : '' ),
2151         );
2152   
2153       } else { #pkgnum tax
2154         next unless $cust_bill_pkg->setup != 0;
2155         $pkg = $cust_bill_pkg->desc;
2156         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2157         ( $sdate, $edate ) = ( '', '' );
2158       }
2159   
2160       $csv->combine(
2161         'cust_bill_pkg',
2162         $self->invnum,
2163         ( map { '' } (1..11) ),
2164         ($pkg, $setup, $recur, $sdate, $edate)
2165       ) or die "can't create csv";
2166
2167       $detail .= $csv->string. "\n";
2168
2169     }
2170
2171   }
2172
2173   ( $header, $detail );
2174
2175 }
2176
2177 =item comp
2178
2179 Pays this invoice with a compliemntary payment.  If there is an error,
2180 returns the error, otherwise returns false.
2181
2182 =cut
2183
2184 sub comp {
2185   my $self = shift;
2186   my $cust_pay = new FS::cust_pay ( {
2187     'invnum'   => $self->invnum,
2188     'paid'     => $self->owed,
2189     '_date'    => '',
2190     'payby'    => 'COMP',
2191     'payinfo'  => $self->cust_main->payinfo,
2192     'paybatch' => '',
2193   } );
2194   $cust_pay->insert;
2195 }
2196
2197 =item realtime_card
2198
2199 Attempts to pay this invoice with a credit card payment via a
2200 Business::OnlinePayment realtime gateway.  See
2201 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2202 for supported processors.
2203
2204 =cut
2205
2206 sub realtime_card {
2207   my $self = shift;
2208   $self->realtime_bop( 'CC', @_ );
2209 }
2210
2211 =item realtime_ach
2212
2213 Attempts to pay this invoice with an electronic check (ACH) payment via a
2214 Business::OnlinePayment realtime gateway.  See
2215 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2216 for supported processors.
2217
2218 =cut
2219
2220 sub realtime_ach {
2221   my $self = shift;
2222   $self->realtime_bop( 'ECHECK', @_ );
2223 }
2224
2225 =item realtime_lec
2226
2227 Attempts to pay this invoice with phone bill (LEC) payment via a
2228 Business::OnlinePayment realtime gateway.  See
2229 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2230 for supported processors.
2231
2232 =cut
2233
2234 sub realtime_lec {
2235   my $self = shift;
2236   $self->realtime_bop( 'LEC', @_ );
2237 }
2238
2239 sub realtime_bop {
2240   my( $self, $method ) = (shift,shift);
2241   my $conf = $self->conf;
2242   my %opt = @_;
2243
2244   my $cust_main = $self->cust_main;
2245   my $balance = $cust_main->balance;
2246   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2247   $amount = sprintf("%.2f", $amount);
2248   return "not run (balance $balance)" unless $amount > 0;
2249
2250   my $description = 'Internet Services';
2251   if ( $conf->exists('business-onlinepayment-description') ) {
2252     my $dtempl = $conf->config('business-onlinepayment-description');
2253
2254     my $agent_obj = $cust_main->agent
2255       or die "can't retreive agent for $cust_main (agentnum ".
2256              $cust_main->agentnum. ")";
2257     my $agent = $agent_obj->agent;
2258     my $pkgs = join(', ',
2259       map { $_->part_pkg->pkg }
2260         grep { $_->pkgnum } $self->cust_bill_pkg
2261     );
2262     $description = eval qq("$dtempl");
2263   }
2264
2265   $cust_main->realtime_bop($method, $amount,
2266     'description' => $description,
2267     'invnum'      => $self->invnum,
2268 #this didn't do what we want, it just calls apply_payments_and_credits
2269 #    'apply'       => 1,
2270     'apply_to_invoice' => 1,
2271     %opt,
2272  #what we want:
2273  #this changes application behavior: auto payments
2274                         #triggered against a specific invoice are now applied
2275                         #to that invoice instead of oldest open.
2276                         #seem okay to me...
2277   );
2278
2279 }
2280
2281 =item batch_card OPTION => VALUE...
2282
2283 Adds a payment for this invoice to the pending credit card batch (see
2284 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2285 runs the payment using a realtime gateway.
2286
2287 =cut
2288
2289 sub batch_card {
2290   my ($self, %options) = @_;
2291   my $cust_main = $self->cust_main;
2292
2293   $options{invnum} = $self->invnum;
2294   
2295   $cust_main->batch_card(%options);
2296 }
2297
2298 sub _agent_template {
2299   my $self = shift;
2300   $self->cust_main->agent_template;
2301 }
2302
2303 sub _agent_invoice_from {
2304   my $self = shift;
2305   $self->cust_main->agent_invoice_from;
2306 }
2307
2308 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2309
2310 Returns an text invoice, as a list of lines.
2311
2312 Options can be passed as a hashref (recommended) or as a list of time, template
2313 and then any key/value pairs for any other options.
2314
2315 I<time>, if specified, is used to control the printing of overdue messages.  The
2316 default is now.  It isn't the date of the invoice; that's the `_date' field.
2317 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2318 L<Time::Local> and L<Date::Parse> for conversion functions.
2319
2320 I<template>, if specified, is the name of a suffix for alternate invoices.
2321
2322 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2323
2324 =cut
2325
2326 sub print_text {
2327   my $self = shift;
2328   my( $today, $template, %opt );
2329   if ( ref($_[0]) ) {
2330     %opt = %{ shift() };
2331     $today = delete($opt{'time'}) || '';
2332     $template = delete($opt{template}) || '';
2333   } else {
2334     ( $today, $template, %opt ) = @_;
2335   }
2336
2337   my %params = ( 'format' => 'template' );
2338   $params{'time'} = $today if $today;
2339   $params{'template'} = $template if $template;
2340   $params{$_} = $opt{$_} 
2341     foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2342
2343   $self->print_generic( %params );
2344 }
2345
2346 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2347
2348 Internal method - returns a filename of a filled-in LaTeX template for this
2349 invoice (Note: add ".tex" to get the actual filename), and a filename of
2350 an associated logo (with the .eps extension included).
2351
2352 See print_ps and print_pdf for methods that return PostScript and PDF output.
2353
2354 Options can be passed as a hashref (recommended) or as a list of time, template
2355 and then any key/value pairs for any other options.
2356
2357 I<time>, if specified, is used to control the printing of overdue messages.  The
2358 default is now.  It isn't the date of the invoice; that's the `_date' field.
2359 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2360 L<Time::Local> and L<Date::Parse> for conversion functions.
2361
2362 I<template>, if specified, is the name of a suffix for alternate invoices.
2363
2364 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2365
2366 =cut
2367
2368 sub print_latex {
2369   my $self = shift;
2370   my $conf = $self->conf;
2371   my( $today, $template, %opt );
2372   if ( ref($_[0]) ) {
2373     %opt = %{ shift() };
2374     $today = delete($opt{'time'}) || '';
2375     $template = delete($opt{template}) || '';
2376   } else {
2377     ( $today, $template, %opt ) = @_;
2378   }
2379
2380   my %params = ( 'format' => 'latex' );
2381   $params{'time'} = $today if $today;
2382   $params{'template'} = $template if $template;
2383   $params{$_} = $opt{$_} 
2384     foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2385
2386   $template ||= $self->_agent_template;
2387
2388   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2389   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2390                            DIR      => $dir,
2391                            SUFFIX   => '.eps',
2392                            UNLINK   => 0,
2393                          ) or die "can't open temp file: $!\n";
2394
2395   my $agentnum = $self->cust_main->agentnum;
2396
2397   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2398     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2399       or die "can't write temp file: $!\n";
2400   } else {
2401     print $lh $conf->config_binary('logo.eps', $agentnum)
2402       or die "can't write temp file: $!\n";
2403   }
2404   close $lh;
2405   $params{'logo_file'} = $lh->filename;
2406
2407   if($conf->exists('invoice-barcode')){
2408       my $png_file = $self->invoice_barcode($dir);
2409       my $eps_file = $png_file;
2410       $eps_file =~ s/\.png$/.eps/g;
2411       $png_file =~ /(barcode.*png)/;
2412       $png_file = $1;
2413       $eps_file =~ /(barcode.*eps)/;
2414       $eps_file = $1;
2415
2416       my $curr_dir = cwd();
2417       chdir($dir); 
2418       # after painfuly long experimentation, it was determined that sam2p won't
2419       # accept : and other chars in the path, no matter how hard I tried to
2420       # escape them, hence the chdir (and chdir back, just to be safe)
2421       system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2422         or die "sam2p failed: $!\n";
2423       unlink($png_file);
2424       chdir($curr_dir);
2425
2426       $params{'barcode_file'} = $eps_file;
2427   }
2428
2429   my @filled_in = $self->print_generic( %params );
2430   
2431   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2432                            DIR      => $dir,
2433                            SUFFIX   => '.tex',
2434                            UNLINK   => 0,
2435                          ) or die "can't open temp file: $!\n";
2436   binmode($fh, ':utf8'); # language support
2437   print $fh join('', @filled_in );
2438   close $fh;
2439
2440   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2441   return ($1, $params{'logo_file'}, $params{'barcode_file'});
2442
2443 }
2444
2445 =item invoice_barcode DIR_OR_FALSE
2446
2447 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2448 it is taken as the temp directory where the PNG file will be generated and the
2449 PNG file name is returned. Otherwise, the PNG image itself is returned.
2450
2451 =cut
2452
2453 sub invoice_barcode {
2454     my ($self, $dir) = (shift,shift);
2455     
2456     my $gdbar = new GD::Barcode('Code39',$self->invnum);
2457         die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2458     my $gd = $gdbar->plot(Height => 30);
2459
2460     if($dir) {
2461         my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2462                            DIR      => $dir,
2463                            SUFFIX   => '.png',
2464                            UNLINK   => 0,
2465                          ) or die "can't open temp file: $!\n";
2466         print $bh $gd->png or die "cannot write barcode to file: $!\n";
2467         my $png_file = $bh->filename;
2468         close $bh;
2469         return $png_file;
2470     }
2471     return $gd->png;
2472 }
2473
2474 =item print_generic OPTION => VALUE ...
2475
2476 Internal method - returns a filled-in template for this invoice as a scalar.
2477
2478 See print_ps and print_pdf for methods that return PostScript and PDF output.
2479
2480 Non optional options include 
2481   format - latex, html, template
2482
2483 Optional options include
2484
2485 template - a value used as a suffix for a configuration template
2486
2487 time - a value used to control the printing of overdue messages.  The
2488 default is now.  It isn't the date of the invoice; that's the `_date' field.
2489 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2490 L<Time::Local> and L<Date::Parse> for conversion functions.
2491
2492 cid - 
2493
2494 unsquelch_cdr - overrides any per customer cdr squelching when true
2495
2496 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2497
2498 locale - override customer's locale
2499
2500 =cut
2501
2502 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
2503 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2504 # yes: fixed width/plain text printing will be borked
2505 sub print_generic {
2506   my( $self, %params ) = @_;
2507   my $conf = $self->conf;
2508   my $today = $params{today} ? $params{today} : time;
2509   warn "$me print_generic called on $self with suffix $params{template}\n"
2510     if $DEBUG;
2511
2512   my $format = $params{format};
2513   die "Unknown format: $format"
2514     unless $format =~ /^(latex|html|template)$/;
2515
2516   my $cust_main = $self->cust_main;
2517   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2518     unless $cust_main->payname
2519         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2520
2521   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
2522                      'html'     => [ '<%=', '%>' ],
2523                      'template' => [ '{', '}' ],
2524                    );
2525
2526   warn "$me print_generic creating template\n"
2527     if $DEBUG > 1;
2528
2529   #create the template
2530   my $template = $params{template} ? $params{template} : $self->_agent_template;
2531   my $templatefile = "invoice_$format";
2532   $templatefile .= "_$template"
2533     if length($template) && $conf->exists($templatefile."_$template");
2534   my @invoice_template = map "$_\n", $conf->config($templatefile)
2535     or die "cannot load config data $templatefile";
2536
2537   my $old_latex = '';
2538   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2539     #change this to a die when the old code is removed
2540     warn "old-style invoice template $templatefile; ".
2541          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2542     $old_latex = 'true';
2543     @invoice_template = _translate_old_latex_format(@invoice_template);
2544   } 
2545
2546   warn "$me print_generic creating T:T object\n"
2547     if $DEBUG > 1;
2548
2549   my $text_template = new Text::Template(
2550     TYPE => 'ARRAY',
2551     SOURCE => \@invoice_template,
2552     DELIMITERS => $delimiters{$format},
2553   );
2554
2555   warn "$me print_generic compiling T:T object\n"
2556     if $DEBUG > 1;
2557
2558   $text_template->compile()
2559     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2560
2561
2562   # additional substitution could possibly cause breakage in existing templates
2563   my %convert_maps = ( 
2564     'latex' => {
2565                  'notes'         => sub { map "$_", @_ },
2566                  'footer'        => sub { map "$_", @_ },
2567                  'smallfooter'   => sub { map "$_", @_ },
2568                  'returnaddress' => sub { map "$_", @_ },
2569                  'coupon'        => sub { map "$_", @_ },
2570                  'summary'       => sub { map "$_", @_ },
2571                },
2572     'html'  => {
2573                  'notes' =>
2574                    sub {
2575                      map { 
2576                        s/%%(.*)$/<!-- $1 -->/g;
2577                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2578                        s/\\begin\{enumerate\}/<ol>/g;
2579                        s/\\item /  <li>/g;
2580                        s/\\end\{enumerate\}/<\/ol>/g;
2581                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2582                        s/\\\\\*/<br>/g;
2583                        s/\\dollar ?/\$/g;
2584                        s/\\#/#/g;
2585                        s/~/&nbsp;/g;
2586                        $_;
2587                      }  @_
2588                    },
2589                  'footer' =>
2590                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2591                  'smallfooter' =>
2592                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2593                  'returnaddress' =>
2594                    sub {
2595                      map { 
2596                        s/~/&nbsp;/g;
2597                        s/\\\\\*?\s*$/<BR>/;
2598                        s/\\hyphenation\{[\w\s\-]+}//;
2599                        s/\\([&])/$1/g;
2600                        $_;
2601                      }  @_
2602                    },
2603                  'coupon'        => sub { "" },
2604                  'summary'       => sub { "" },
2605                },
2606     'template' => {
2607                  'notes' =>
2608                    sub {
2609                      map { 
2610                        s/%%.*$//g;
2611                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2612                        s/\\begin\{enumerate\}//g;
2613                        s/\\item /  * /g;
2614                        s/\\end\{enumerate\}//g;
2615                        s/\\textbf\{(.*)\}/$1/g;
2616                        s/\\\\\*/ /;
2617                        s/\\dollar ?/\$/g;
2618                        $_;
2619                      }  @_
2620                    },
2621                  'footer' =>
2622                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2623                  'smallfooter' =>
2624                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2625                  'returnaddress' =>
2626                    sub {
2627                      map { 
2628                        s/~/ /g;
2629                        s/\\\\\*?\s*$/\n/;             # dubious
2630                        s/\\hyphenation\{[\w\s\-]+}//;
2631                        $_;
2632                      }  @_
2633                    },
2634                  'coupon'        => sub { "" },
2635                  'summary'       => sub { "" },
2636                },
2637   );
2638
2639
2640   # hashes for differing output formats
2641   my %nbsps = ( 'latex'    => '~',
2642                 'html'     => '',    # '&nbps;' would be nice
2643                 'template' => '',    # not used
2644               );
2645   my $nbsp = $nbsps{$format};
2646
2647   my %escape_functions = ( 'latex'    => \&_latex_escape,
2648                            'html'     => \&_html_escape_nbsp,#\&encode_entities,
2649                            'template' => sub { shift },
2650                          );
2651   my $escape_function = $escape_functions{$format};
2652   my $escape_function_nonbsp = ($format eq 'html')
2653                                  ? \&_html_escape : $escape_function;
2654
2655   my %date_formats = ( 'latex'    => $date_format_long,
2656                        'html'     => $date_format_long,
2657                        'template' => '%s',
2658                      );
2659   $date_formats{'html'} =~ s/ /&nbsp;/g;
2660
2661   my $date_format = $date_formats{$format};
2662
2663   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
2664                                                },
2665                              'html'     => sub { return '<b>'. shift(). '</b>'
2666                                                },
2667                              'template' => sub { shift },
2668                            );
2669   my $embolden_function = $embolden_functions{$format};
2670
2671   my %newline_tokens = (  'latex'     => '\\\\',
2672                           'html'      => '<br>',
2673                           'template'  => "\n",
2674                         );
2675   my $newline_token = $newline_tokens{$format};
2676
2677   warn "$me generating template variables\n"
2678     if $DEBUG > 1;
2679
2680   # generate template variables
2681   my $returnaddress;
2682   if (
2683          defined( $conf->config_orbase( "invoice_${format}returnaddress",
2684                                         $template
2685                                       )
2686                 )
2687        && length( $conf->config_orbase( "invoice_${format}returnaddress",
2688                                         $template
2689                                       )
2690                 )
2691   ) {
2692
2693     $returnaddress = join("\n",
2694       $conf->config_orbase("invoice_${format}returnaddress", $template)
2695     );
2696
2697   } elsif ( grep /\S/,
2698             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2699
2700     my $convert_map = $convert_maps{$format}{'returnaddress'};
2701     $returnaddress =
2702       join( "\n",
2703             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2704                                                  $template
2705                                                )
2706                          )
2707           );
2708   } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2709
2710     my $convert_map = $convert_maps{$format}{'returnaddress'};
2711     $returnaddress = join( "\n", &$convert_map(
2712                                    map { s/( {2,})/'~' x length($1)/eg;
2713                                          s/$/\\\\\*/;
2714                                          $_
2715                                        }
2716                                      ( $conf->config('company_name', $self->cust_main->agentnum),
2717                                        $conf->config('company_address', $self->cust_main->agentnum),
2718                                      )
2719                                  )
2720                      );
2721
2722   } else {
2723
2724     my $warning = "Couldn't find a return address; ".
2725                   "do you need to set the company_address configuration value?";
2726     warn "$warning\n";
2727     $returnaddress = $nbsp;
2728     #$returnaddress = $warning;
2729
2730   }
2731
2732   warn "$me generating invoice data\n"
2733     if $DEBUG > 1;
2734
2735   my $agentnum = $self->cust_main->agentnum;
2736
2737   my %invoice_data = (
2738
2739     #invoice from info
2740     'company_name'    => scalar( $conf->config('company_name', $agentnum) ),
2741     'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2742     'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2743     'returnaddress'   => $returnaddress,
2744     'agent'           => &$escape_function($cust_main->agent->agent),
2745
2746     #invoice info
2747     'invnum'          => $self->invnum,
2748     '_date'           => $self->_date,
2749     'date'            => time2str($date_format, $self->_date),
2750     'today'           => time2str($date_format_long, $today),
2751     'terms'           => $self->terms,
2752     'template'        => $template, #params{'template'},
2753     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
2754     'current_charges' => sprintf("%.2f", $self->charged),
2755     'duedate'         => $self->due_date2str($rdate_format), #date_format?
2756
2757     #customer info
2758     'custnum'         => $cust_main->display_custnum,
2759     'agent_custid'    => &$escape_function($cust_main->agent_custid),
2760     ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2761       payname company address1 address2 city state zip fax
2762     )),
2763
2764     #global config
2765     'ship_enable'     => $conf->exists('invoice-ship_address'),
2766     'unitprices'      => $conf->exists('invoice-unitprice'),
2767     'smallernotes'    => $conf->exists('invoice-smallernotes'),
2768     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
2769     'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2770    
2771     #layout info -- would be fancy to calc some of this and bury the template
2772     #               here in the code
2773     'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2774     'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2775     'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
2776     'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2777     'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2778     'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2779     'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2780     'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2781     'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2782     'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2783
2784     # better hang on to conf_dir for a while (for old templates)
2785     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2786
2787     #these are only used when doing paged plaintext
2788     'page'            => 1,
2789     'total_pages'     => 1,
2790
2791   );
2792  
2793   #localization
2794   my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2795   $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2796   my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2797   # eval to avoid death for unimplemented languages
2798   my $dh = eval { Date::Language->new($info{'name'}) } ||
2799            Date::Language->new(); # fall back to English
2800   # prototype here to silence warnings
2801   $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2802   # eventually use this date handle everywhere in here, too
2803
2804   my $min_sdate = 999999999999;
2805   my $max_edate = 0;
2806   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2807     next unless $cust_bill_pkg->pkgnum > 0;
2808     $min_sdate = $cust_bill_pkg->sdate
2809       if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2810     $max_edate = $cust_bill_pkg->edate
2811       if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2812   }
2813
2814   $invoice_data{'bill_period'} = '';
2815   $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate) 
2816     . " to " . time2str('%e %h', $max_edate)
2817     if ($max_edate != 0 && $min_sdate != 999999999999);
2818
2819   $invoice_data{finance_section} = '';
2820   if ( $conf->config('finance_pkgclass') ) {
2821     my $pkg_class =
2822       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2823     $invoice_data{finance_section} = $pkg_class->categoryname;
2824   } 
2825   $invoice_data{finance_amount} = '0.00';
2826   $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2827
2828   my $countrydefault = $conf->config('countrydefault') || 'US';
2829   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2830   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2831     my $method = $prefix.$_;
2832     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2833   }
2834   $invoice_data{'ship_country'} = ''
2835     if ( $invoice_data{'ship_country'} eq $countrydefault );
2836   
2837   $invoice_data{'cid'} = $params{'cid'}
2838     if $params{'cid'};
2839
2840   if ( $cust_main->country eq $countrydefault ) {
2841     $invoice_data{'country'} = '';
2842   } else {
2843     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2844   }
2845
2846   my @address = ();
2847   $invoice_data{'address'} = \@address;
2848   push @address,
2849     $cust_main->payname.
2850       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2851         ? " (P.O. #". $cust_main->payinfo. ")"
2852         : ''
2853       )
2854   ;
2855   push @address, $cust_main->company
2856     if $cust_main->company;
2857   push @address, $cust_main->address1;
2858   push @address, $cust_main->address2
2859     if $cust_main->address2;
2860   push @address,
2861     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
2862   push @address, $invoice_data{'country'}
2863     if $invoice_data{'country'};
2864   push @address, ''
2865     while (scalar(@address) < 5);
2866
2867   $invoice_data{'logo_file'} = $params{'logo_file'}
2868     if $params{'logo_file'};
2869   $invoice_data{'barcode_file'} = $params{'barcode_file'}
2870     if $params{'barcode_file'};
2871   $invoice_data{'barcode_img'} = $params{'barcode_img'}
2872     if $params{'barcode_img'};
2873   $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2874     if $params{'barcode_cid'};
2875
2876   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2877 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2878   #my $balance_due = $self->owed + $pr_total - $cr_total;
2879   my $balance_due = $self->owed + $pr_total;
2880
2881   # the customer's current balance as shown on the invoice before this one
2882   $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2883
2884   # the change in balance from that invoice to this one
2885   $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2886
2887   # the sum of amount owed on all previous invoices
2888   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2889
2890   # the sum of amount owed on all invoices
2891   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2892
2893   # info from customer's last invoice before this one, for some 
2894   # summary formats
2895   $invoice_data{'last_bill'} = {};
2896   if ( $self->previous_bill ) {
2897     $invoice_data{'last_bill'} = {
2898       '_date'     => $self->previous_bill->_date, #unformatted
2899       # all we need for now
2900     };
2901   }
2902
2903   my $summarypage = '';
2904   if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2905     $summarypage = 1;
2906   }
2907   $invoice_data{'summarypage'} = $summarypage;
2908
2909   warn "$me substituting variables in notes, footer, smallfooter\n"
2910     if $DEBUG > 1;
2911
2912   my @include = (qw( notes footer smallfooter ));
2913   push @include, 'coupon' unless $params{'no_coupon'};
2914   foreach my $include (@include) {
2915
2916     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2917     my @inc_src;
2918
2919     if ( $conf->exists($inc_file, $agentnum)
2920          && length( $conf->config($inc_file, $agentnum) ) ) {
2921
2922       @inc_src = $conf->config($inc_file, $agentnum);
2923
2924     } else {
2925
2926       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2927
2928       my $convert_map = $convert_maps{$format}{$include};
2929
2930       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2931                        s/--\@\]/$delimiters{$format}[1]/g;
2932                        $_;
2933                      } 
2934                  &$convert_map( $conf->config($inc_file, $agentnum) );
2935
2936     }
2937
2938     my $inc_tt = new Text::Template (
2939       TYPE       => 'ARRAY',
2940       SOURCE     => [ map "$_\n", @inc_src ],
2941       DELIMITERS => $delimiters{$format},
2942     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2943
2944     unless ( $inc_tt->compile() ) {
2945       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2946       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2947       die $error;
2948     }
2949
2950     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2951
2952     $invoice_data{$include} =~ s/\n+$//
2953       if ($format eq 'latex');
2954   }
2955
2956   # let invoices use either of these as needed
2957   $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL') 
2958     ? $cust_main->payinfo : '';
2959   $invoice_data{'po_line'} = 
2960     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2961       ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2962       : $nbsp;
2963
2964   my %money_chars = ( 'latex'    => '',
2965                       'html'     => $conf->config('money_char') || '$',
2966                       'template' => '',
2967                     );
2968   my $money_char = $money_chars{$format};
2969
2970   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2971                             'html'     => $conf->config('money_char') || '$',
2972                             'template' => '',
2973                           );
2974   my $other_money_char = $other_money_chars{$format};
2975   $invoice_data{'dollar'} = $other_money_char;
2976
2977   my @detail_items = ();
2978   my @total_items = ();
2979   my @buf = ();
2980   my @sections = ();
2981
2982   $invoice_data{'detail_items'} = \@detail_items;
2983   $invoice_data{'total_items'} = \@total_items;
2984   $invoice_data{'buf'} = \@buf;
2985   $invoice_data{'sections'} = \@sections;
2986
2987   warn "$me generating sections\n"
2988     if $DEBUG > 1;
2989
2990   my $previous_section = { 'description' => $self->mt('Previous Charges'),
2991                            'subtotal'    => $other_money_char.
2992                                             sprintf('%.2f', $pr_total),
2993                            'summarized'  => '', #why? $summarypage ? 'Y' : '',
2994                          };
2995   $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
2996     join(' / ', map { $cust_main->balance_date_range(@$_) }
2997                 $self->_prior_month30s
2998         )
2999     if $conf->exists('invoice_include_aging');
3000
3001   my $taxtotal = 0;
3002   my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
3003                       'subtotal'    => $taxtotal,   # adjusted below
3004                       'tax_section' => 1,
3005                     };
3006   my $tax_weight = _pkg_category($tax_section->{description})
3007                         ? _pkg_category($tax_section->{description})->weight
3008                         : 0;
3009   $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
3010   $tax_section->{'sort_weight'} = $tax_weight;
3011
3012
3013   my $adjusttotal = 0;
3014   my $adjust_section = {
3015     'description'    => $self->mt('Credits, Payments, and Adjustments'),
3016     'adjust_section' => 1,
3017     'subtotal'       => 0,   # adjusted below
3018   };
3019   my $adjust_weight = _pkg_category($adjust_section->{description})
3020                         ? _pkg_category($adjust_section->{description})->weight
3021                         : 0;
3022   $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
3023   $adjust_section->{'sort_weight'} = $adjust_weight;
3024
3025   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
3026   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
3027   $invoice_data{'multisection'} = $multisection;
3028   my $late_sections = [];
3029   my $extra_sections = [];
3030   my $extra_lines = ();
3031
3032   my $default_section = { 'description' => '',
3033                           'subtotal'    => '', 
3034                           'no_subtotal' => 1,
3035                         };
3036
3037   if ( $multisection ) {
3038     ($extra_sections, $extra_lines) =
3039       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3040       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3041
3042     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3043
3044     push @detail_items, @$extra_lines if $extra_lines;
3045     push @sections,
3046       $self->_items_sections( $late_sections,      # this could stand a refactor
3047                               $summarypage,
3048                               $escape_function_nonbsp,
3049                               $extra_sections,
3050                               $format,             #bah
3051                             );
3052     if ($conf->exists('svc_phone_sections')) {
3053       my ($phone_sections, $phone_lines) =
3054         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3055       push @{$late_sections}, @$phone_sections;
3056       push @detail_items, @$phone_lines;
3057     }
3058     if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3059       my ($accountcode_section, $accountcode_lines) =
3060         $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3061       if ( scalar(@$accountcode_lines) ) {
3062           push @{$late_sections}, $accountcode_section;
3063           push @detail_items, @$accountcode_lines;
3064       }
3065     }
3066   } else {# not multisection
3067     # make a default section
3068     push @sections, $default_section;
3069     # and calculate the finance charge total, since it won't get done otherwise.
3070     # XXX possibly other totals?
3071     # XXX possibly finance_pkgclass should not be used in this manner?
3072     if ( $conf->exists('finance_pkgclass') ) {
3073       my @finance_charges;
3074       foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3075         if ( grep { $_->section eq $invoice_data{finance_section} }
3076              $cust_bill_pkg->cust_bill_pkg_display ) {
3077           # I think these are always setup fees, but just to be sure...
3078           push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3079         }
3080       }
3081       $invoice_data{finance_amount} = 
3082         sprintf('%.2f', sum( @finance_charges ) || 0);
3083     }
3084   }
3085
3086   # previous invoice balances in the Previous Charges section if there
3087   # is one, otherwise in the main detail section
3088   if ( $self->can('_items_previous') &&
3089        $self->enable_previous &&
3090        ! $conf->exists('previous_balance-summary_only') ) {
3091
3092     warn "$me adding previous balances\n"
3093       if $DEBUG > 1;
3094
3095     foreach my $line_item ( $self->_items_previous ) {
3096
3097       my $detail = {
3098         ext_description => [],
3099       };
3100       $detail->{'ref'} = $line_item->{'pkgnum'};
3101       $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3102       $detail->{'quantity'} = 1;
3103       $detail->{'section'} = $multisection ? $previous_section
3104                                            : $default_section;
3105       $detail->{'description'} = &$escape_function($line_item->{'description'});
3106       if ( exists $line_item->{'ext_description'} ) {
3107         @{$detail->{'ext_description'}} = map {
3108           &$escape_function($_);
3109         } @{$line_item->{'ext_description'}};
3110       }
3111       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3112                             $line_item->{'amount'};
3113       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3114
3115       push @detail_items, $detail;
3116       push @buf, [ $detail->{'description'},
3117                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3118                  ];
3119     }
3120
3121   }
3122
3123   if ( @pr_cust_bill && $self->enable_previous ) {
3124     push @buf, ['','-----------'];
3125     push @buf, [ $self->mt('Total Previous Balance'),
3126                  $money_char. sprintf("%10.2f", $pr_total) ];
3127     push @buf, ['',''];
3128   }
3129  
3130   if ( $conf->exists('svc_phone-did-summary') ) {
3131       warn "$me adding DID summary\n"
3132         if $DEBUG > 1;
3133
3134       my ($didsummary,$minutes) = $self->_did_summary;
3135       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3136       push @detail_items, 
3137        { 'description' => $didsummary_desc,
3138            'ext_description' => [ $didsummary, $minutes ],
3139        };
3140   }
3141
3142   foreach my $section (@sections, @$late_sections) {
3143
3144     warn "$me adding section \n". Dumper($section)
3145       if $DEBUG > 1;
3146
3147     # begin some normalization
3148     $section->{'subtotal'} = $section->{'amount'}
3149       if $multisection
3150          && !exists($section->{subtotal})
3151          && exists($section->{amount});
3152
3153     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3154       if ( $invoice_data{finance_section} &&
3155            $section->{'description'} eq $invoice_data{finance_section} );
3156
3157     $section->{'subtotal'} = $other_money_char.
3158                              sprintf('%.2f', $section->{'subtotal'})
3159       if $multisection;
3160
3161     # continue some normalization
3162     $section->{'amount'}   = $section->{'subtotal'}
3163       if $multisection;
3164
3165
3166     if ( $section->{'description'} ) {
3167       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3168                    [ '', '' ],
3169                  );
3170     }
3171
3172     warn "$me   setting options\n"
3173       if $DEBUG > 1;
3174
3175     my $multilocation = scalar($cust_main->cust_location); #too expensive?
3176     my %options = ();
3177     $options{'section'} = $section if $multisection;
3178     $options{'format'} = $format;
3179     $options{'escape_function'} = $escape_function;
3180     $options{'no_usage'} = 1 unless $unsquelched;
3181     $options{'unsquelched'} = $unsquelched;
3182     $options{'summary_page'} = $summarypage;
3183     $options{'skip_usage'} =
3184       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3185     $options{'multilocation'} = $multilocation;
3186     $options{'multisection'} = $multisection;
3187
3188     warn "$me   searching for line items\n"
3189       if $DEBUG > 1;
3190
3191     foreach my $line_item ( $self->_items_pkg(%options) ) {
3192
3193       warn "$me     adding line item $line_item\n"
3194         if $DEBUG > 1;
3195
3196       my $detail = {
3197         ext_description => [],
3198       };
3199       $detail->{'ref'} = $line_item->{'pkgnum'};
3200       $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3201       $detail->{'quantity'} = $line_item->{'quantity'};
3202       $detail->{'section'} = $section;
3203       $detail->{'description'} = &$escape_function($line_item->{'description'});
3204       if ( exists $line_item->{'ext_description'} ) {
3205         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3206       }
3207       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3208                               $line_item->{'amount'};
3209       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3210                                  $line_item->{'unit_amount'};
3211       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3212
3213       $detail->{'sdate'} = $line_item->{'sdate'};
3214       $detail->{'edate'} = $line_item->{'edate'};
3215       $detail->{'seconds'} = $line_item->{'seconds'};
3216       $detail->{'svc_label'} = $line_item->{'svc_label'};
3217   
3218       push @detail_items, $detail;
3219       push @buf, ( [ $detail->{'description'},
3220                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3221                    ],
3222                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3223                  );
3224     }
3225
3226     if ( $section->{'description'} ) {
3227       push @buf, ( ['','-----------'],
3228                    [ $section->{'description'}. ' sub-total',
3229                       $section->{'subtotal'} # already formatted this 
3230                    ],
3231                    [ '', '' ],
3232                    [ '', '' ],
3233                  );
3234     }
3235   
3236   }
3237
3238   $invoice_data{current_less_finance} =
3239     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3240
3241   # create a major section for previous balance if we have major sections,
3242   # or if previous_section is in summary form
3243   if ( ( $multisection && $self->enable_previous )
3244     || $conf->exists('previous_balance-summary_only') )
3245   {
3246     unshift @sections, $previous_section if $pr_total;
3247   }
3248
3249   warn "$me adding taxes\n"
3250     if $DEBUG > 1;
3251
3252   foreach my $tax ( $self->_items_tax ) {
3253
3254     $taxtotal += $tax->{'amount'};
3255
3256     my $description = &$escape_function( $tax->{'description'} );
3257     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
3258
3259     if ( $multisection ) {
3260
3261       my $money = $old_latex ? '' : $money_char;
3262       push @detail_items, {
3263         ext_description => [],
3264         ref          => '',
3265         quantity     => '',
3266         description  => $description,
3267         amount       => $money. $amount,
3268         product_code => '',
3269         section      => $tax_section,
3270       };
3271
3272     } else {
3273
3274       push @total_items, {
3275         'total_item'   => $description,
3276         'total_amount' => $other_money_char. $amount,
3277       };
3278
3279     }
3280
3281     push @buf,[ $description,
3282                 $money_char. $amount,
3283               ];
3284
3285   }
3286   
3287   if ( $taxtotal ) {
3288     my $total = {};
3289     $total->{'total_item'} = $self->mt('Sub-total');
3290     $total->{'total_amount'} =
3291       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3292
3293     if ( $multisection ) {
3294       $tax_section->{'subtotal'} = $other_money_char.
3295                                    sprintf('%.2f', $taxtotal);
3296       $tax_section->{'pretotal'} = 'New charges sub-total '.
3297                                    $total->{'total_amount'};
3298       push @sections, $tax_section if $taxtotal;
3299     }else{
3300       unshift @total_items, $total;
3301     }
3302   }
3303   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3304
3305   push @buf,['','-----------'];
3306   push @buf,[$self->mt( 
3307               (!$self->enable_previous)
3308                ? 'Total Charges'
3309                : 'Total New Charges'
3310              ),
3311              $money_char. sprintf("%10.2f",$self->charged) ];
3312   push @buf,['',''];
3313
3314   # calculate total, possibly including total owed on previous
3315   # invoices
3316   {
3317     my $total = {};
3318     my $item = 'Total';
3319     $item = $conf->config('previous_balance-exclude_from_total')
3320          || 'Total New Charges'
3321       if $conf->exists('previous_balance-exclude_from_total');
3322     my $amount = $self->charged;
3323     if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3324       $amount += $pr_total;
3325     }
3326
3327     $total->{'total_item'} = &$embolden_function($self->mt($item));
3328     $total->{'total_amount'} =
3329       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
3330     if ( $multisection ) {
3331       if ( $adjust_section->{'sort_weight'} ) {
3332         $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3333           $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
3334       } else {
3335         $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3336           $other_money_char.  sprintf('%.2f', $self->charged );
3337       } 
3338     } else {
3339       push @total_items, $total;
3340     }
3341     push @buf,['','-----------'];
3342     push @buf,[$item,
3343                $money_char.
3344                sprintf( '%10.2f', $amount )
3345               ];
3346     push @buf,['',''];
3347   }
3348
3349   # if we're showing previous invoices, also show previous
3350   # credits and payments 
3351   if ( $self->enable_previous 
3352         and $self->can('_items_credits')
3353         and $self->can('_items_payments') )
3354     {
3355     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3356   
3357     # credits
3358     my $credittotal = 0;
3359     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3360
3361       my $total;
3362       $total->{'total_item'} = &$escape_function($credit->{'description'});
3363       $credittotal += $credit->{'amount'};
3364       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3365       $adjusttotal += $credit->{'amount'};
3366       if ( $multisection ) {
3367         my $money = $old_latex ? '' : $money_char;
3368         push @detail_items, {
3369           ext_description => [],
3370           ref          => '',
3371           quantity     => '',
3372           description  => &$escape_function($credit->{'description'}),
3373           amount       => $money. $credit->{'amount'},
3374           product_code => '',
3375           section      => $adjust_section,
3376         };
3377       } else {
3378         push @total_items, $total;
3379       }
3380
3381     }
3382     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3383
3384     #credits (again)
3385     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3386       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3387     }
3388
3389     # payments
3390     my $paymenttotal = 0;
3391     foreach my $payment ( $self->_items_payments ) {
3392       my $total = {};
3393       $total->{'total_item'} = &$escape_function($payment->{'description'});
3394       $paymenttotal += $payment->{'amount'};
3395       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3396       $adjusttotal += $payment->{'amount'};
3397       if ( $multisection ) {
3398         my $money = $old_latex ? '' : $money_char;
3399         push @detail_items, {
3400           ext_description => [],
3401           ref          => '',
3402           quantity     => '',
3403           description  => &$escape_function($payment->{'description'}),
3404           amount       => $money. $payment->{'amount'},
3405           product_code => '',
3406           section      => $adjust_section,
3407         };
3408       }else{
3409         push @total_items, $total;
3410       }
3411       push @buf, [ $payment->{'description'},
3412                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
3413                  ];
3414     }
3415     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3416   
3417     if ( $multisection ) {
3418       $adjust_section->{'subtotal'} = $other_money_char.
3419                                       sprintf('%.2f', $adjusttotal);
3420       push @sections, $adjust_section
3421         unless $adjust_section->{sort_weight};
3422     }
3423
3424     # create Balance Due message
3425     { 
3426       my $total;
3427       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3428       $total->{'total_amount'} =
3429         &$embolden_function(
3430           $other_money_char. sprintf('%.2f', #why? $summarypage 
3431                                              #  ? $self->charged +
3432                                              #    $self->billing_balance
3433                                              #  :
3434                                                  $self->owed + $pr_total
3435                                     )
3436         );
3437       if ( $multisection && !$adjust_section->{sort_weight} ) {
3438         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3439                                          $total->{'total_amount'};
3440       }else{
3441         push @total_items, $total;
3442       }
3443       push @buf,['','-----------'];
3444       push @buf,[$self->balance_due_msg, $money_char. 
3445         sprintf("%10.2f", $balance_due ) ];
3446     }
3447
3448     if ( $conf->exists('previous_balance-show_credit')
3449         and $cust_main->balance < 0 ) {
3450       my $credit_total = {
3451         'total_item'    => &$embolden_function($self->credit_balance_msg),
3452         'total_amount'  => &$embolden_function(
3453           $other_money_char. sprintf('%.2f', -$cust_main->balance)
3454         ),
3455       };
3456       if ( $multisection ) {
3457         $adjust_section->{'posttotal'} .= $newline_token .
3458           $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3459       }
3460       else {
3461         push @total_items, $credit_total;
3462       }
3463       push @buf,['','-----------'];
3464       push @buf,[$self->credit_balance_msg, $money_char. 
3465         sprintf("%10.2f", -$cust_main->balance ) ];
3466     }
3467   }
3468
3469   if ( $multisection ) {
3470     if ($conf->exists('svc_phone_sections')) {
3471       my $total;
3472       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3473       $total->{'total_amount'} =
3474         &$embolden_function(
3475           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3476         );
3477       my $last_section = pop @sections;
3478       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3479                                      $total->{'total_amount'};
3480       push @sections, $last_section;
3481     }
3482     push @sections, @$late_sections
3483       if $unsquelched;
3484   }
3485
3486   # make a discounts-available section, even without multisection
3487   if ( $conf->exists('discount-show_available') 
3488        and my @discounts_avail = $self->_items_discounts_avail ) {
3489     my $discount_section = {
3490       'description' => $self->mt('Discounts Available'),
3491       'subtotal'    => '',
3492       'no_subtotal' => 1,
3493     };
3494
3495     push @sections, $discount_section;
3496     push @detail_items, map { +{
3497         'ref'         => '', #should this be something else?
3498         'section'     => $discount_section,
3499         'description' => &$escape_function( $_->{description} ),
3500         'amount'      => $money_char . &$escape_function( $_->{amount} ),
3501         'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3502     } } @discounts_avail;
3503   }
3504
3505   # debugging hook: call this with 'diag' => 1 to just get a hash of
3506   # the invoice variables
3507   return \%invoice_data if ( $params{'diag'} );
3508
3509   # All sections and items are built; now fill in templates.
3510   my @includelist = ();
3511   push @includelist, 'summary' if $summarypage;
3512   foreach my $include ( @includelist ) {
3513
3514     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3515     my @inc_src;
3516
3517     if ( length( $conf->config($inc_file, $agentnum) ) ) {
3518
3519       @inc_src = $conf->config($inc_file, $agentnum);
3520
3521     } else {
3522
3523       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3524
3525       my $convert_map = $convert_maps{$format}{$include};
3526
3527       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3528                        s/--\@\]/$delimiters{$format}[1]/g;
3529                        $_;
3530                      } 
3531                  &$convert_map( $conf->config($inc_file, $agentnum) );
3532
3533     }
3534
3535     my $inc_tt = new Text::Template (
3536       TYPE       => 'ARRAY',
3537       SOURCE     => [ map "$_\n", @inc_src ],
3538       DELIMITERS => $delimiters{$format},
3539     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3540
3541     unless ( $inc_tt->compile() ) {
3542       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3543       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3544       die $error;
3545     }
3546
3547     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3548
3549     $invoice_data{$include} =~ s/\n+$//
3550       if ($format eq 'latex');
3551   }
3552
3553   $invoice_lines = 0;
3554   my $wasfunc = 0;
3555   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3556     /invoice_lines\((\d*)\)/;
3557     $invoice_lines += $1 || scalar(@buf);
3558     $wasfunc=1;
3559   }
3560   die "no invoice_lines() functions in template?"
3561     if ( $format eq 'template' && !$wasfunc );
3562
3563   if ($format eq 'template') {
3564
3565     if ( $invoice_lines ) {
3566       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3567       $invoice_data{'total_pages'}++
3568         if scalar(@buf) % $invoice_lines;
3569     }
3570
3571     #setup subroutine for the template
3572     $invoice_data{invoice_lines} = sub {
3573       my $lines = shift || scalar(@buf);
3574       map { 
3575         scalar(@buf)
3576           ? shift @buf
3577           : [ '', '' ];
3578       }
3579       ( 1 .. $lines );
3580     };
3581
3582     my $lines;
3583     my @collect;
3584     while (@buf) {
3585       push @collect, split("\n",
3586         $text_template->fill_in( HASH => \%invoice_data )
3587       );
3588       $invoice_data{'page'}++;
3589     }
3590     map "$_\n", @collect;
3591   }else{
3592     # this is where we actually create the invoice
3593     warn "filling in template for invoice ". $self->invnum. "\n"
3594       if $DEBUG;
3595     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3596       if $DEBUG > 1;
3597
3598     $text_template->fill_in(HASH => \%invoice_data);
3599   }
3600 }
3601
3602 # helper routine for generating date ranges
3603 sub _prior_month30s {
3604   my $self = shift;
3605   my @ranges = (
3606    [ 1,       2592000 ], # 0-30 days ago
3607    [ 2592000, 5184000 ], # 30-60 days ago
3608    [ 5184000, 7776000 ], # 60-90 days ago
3609    [ 7776000, 0       ], # 90+   days ago
3610   );
3611
3612   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3613           $_->[1] ? $self->_date - $_->[1] - 1 : '',
3614       ] }
3615   @ranges;
3616 }
3617
3618 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3619
3620 Returns an postscript invoice, as a scalar.
3621
3622 Options can be passed as a hashref (recommended) or as a list of time, template
3623 and then any key/value pairs for any other options.
3624
3625 I<time> an optional value used to control the printing of overdue messages.  The
3626 default is now.  It isn't the date of the invoice; that's the `_date' field.
3627 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3628 L<Time::Local> and L<Date::Parse> for conversion functions.
3629
3630 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3631
3632 =cut
3633
3634 sub print_ps {
3635   my $self = shift;
3636
3637   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3638   my $ps = generate_ps($file);
3639   unlink($logofile);
3640   unlink($barcodefile) if $barcodefile;
3641
3642   $ps;
3643 }
3644
3645 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3646
3647 Returns an PDF invoice, as a scalar.
3648
3649 Options can be passed as a hashref (recommended) or as a list of time, template
3650 and then any key/value pairs for any other options.
3651
3652 I<time> an optional value used to control the printing of overdue messages.  The
3653 default is now.  It isn't the date of the invoice; that's the `_date' field.
3654 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3655 L<Time::Local> and L<Date::Parse> for conversion functions.
3656
3657 I<template>, if specified, is the name of a suffix for alternate invoices.
3658
3659 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3660
3661 =cut
3662
3663 sub print_pdf {
3664   my $self = shift;
3665
3666   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3667   my $pdf = generate_pdf($file);
3668   unlink($logofile);
3669   unlink($barcodefile) if $barcodefile;
3670
3671   $pdf;
3672 }
3673
3674 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3675
3676 Returns an HTML invoice, as a scalar.
3677
3678 I<time> an optional value used to control the printing of overdue messages.  The
3679 default is now.  It isn't the date of the invoice; that's the `_date' field.
3680 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3681 L<Time::Local> and L<Date::Parse> for conversion functions.
3682
3683 I<template>, if specified, is the name of a suffix for alternate invoices.
3684
3685 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3686
3687 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3688 when emailing the invoice as part of a multipart/related MIME email.
3689
3690 =cut
3691
3692 sub print_html {
3693   my $self = shift;
3694   my %params;
3695   if ( ref($_[0]) ) {
3696     %params = %{ shift() }; 
3697   }else{
3698     $params{'time'} = shift;
3699     $params{'template'} = shift;
3700     $params{'cid'} = shift;
3701   }
3702
3703   $params{'format'} = 'html';
3704   
3705   $self->print_generic( %params );
3706 }
3707
3708 # quick subroutine for print_latex
3709 #
3710 # There are ten characters that LaTeX treats as special characters, which
3711 # means that they do not simply typeset themselves: 
3712 #      # $ % & ~ _ ^ \ { }
3713 #
3714 # TeX ignores blanks following an escaped character; if you want a blank (as
3715 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3716
3717 sub _latex_escape {
3718   my $value = shift;
3719   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3720   $value =~ s/([<>])/\$$1\$/g;
3721   $value;
3722 }
3723
3724 sub _html_escape {
3725   my $value = shift;
3726   encode_entities($value);
3727   $value;
3728 }
3729
3730 sub _html_escape_nbsp {
3731   my $value = _html_escape(shift);
3732   $value =~ s/ +/&nbsp;/g;
3733   $value;
3734 }
3735
3736 #utility methods for print_*
3737
3738 sub _translate_old_latex_format {
3739   warn "_translate_old_latex_format called\n"
3740     if $DEBUG; 
3741
3742   my @template = ();
3743   while ( @_ ) {
3744     my $line = shift;
3745   
3746     if ( $line =~ /^%%Detail\s*$/ ) {
3747   
3748       push @template, q![@--!,
3749                       q!  foreach my $_tr_line (@detail_items) {!,
3750                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3751                       q!      $_tr_line->{'description'} .= !, 
3752                       q!        "\\tabularnewline\n~~".!,
3753                       q!        join( "\\tabularnewline\n~~",!,
3754                       q!          @{$_tr_line->{'ext_description'}}!,
3755                       q!        );!,
3756                       q!    }!;
3757
3758       while ( ( my $line_item_line = shift )
3759               !~ /^%%EndDetail\s*$/                            ) {
3760         $line_item_line =~ s/'/\\'/g;    # nice LTS
3761         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3762         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3763         push @template, "    \$OUT .= '$line_item_line';";
3764       }
3765
3766       push @template, '}',
3767                       '--@]';
3768       #' doh, gvim
3769     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3770
3771       push @template, '[@--',
3772                       '  foreach my $_tr_line (@total_items) {';
3773
3774       while ( ( my $total_item_line = shift )
3775               !~ /^%%EndTotalDetails\s*$/                      ) {
3776         $total_item_line =~ s/'/\\'/g;    # nice LTS
3777         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3778         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3779         push @template, "    \$OUT .= '$total_item_line';";
3780       }
3781
3782       push @template, '}',
3783                       '--@]';
3784
3785     } else {
3786       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3787       push @template, $line;  
3788     }
3789   
3790   }
3791
3792   if ($DEBUG) {
3793     warn "$_\n" foreach @template;
3794   }
3795
3796   (@template);
3797 }
3798
3799 sub terms {
3800   my $self = shift;
3801   my $conf = $self->conf;
3802
3803   #check for an invoice-specific override
3804   return $self->invoice_terms if $self->invoice_terms;
3805   
3806   #check for a customer- specific override
3807   my $cust_main = $self->cust_main;
3808   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3809
3810   #use configured default
3811   $conf->config('invoice_default_terms') || '';
3812 }
3813
3814 sub due_date {
3815   my $self = shift;
3816   my $duedate = '';
3817   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3818     $duedate = $self->_date() + ( $1 * 86400 );
3819   }
3820   $duedate;
3821 }
3822
3823 sub due_date2str {
3824   my $self = shift;
3825   $self->due_date ? time2str(shift, $self->due_date) : '';
3826 }
3827
3828 sub balance_due_msg {
3829   my $self = shift;
3830   my $msg = $self->mt('Balance Due');
3831   return $msg unless $self->terms;
3832   if ( $self->due_date ) {
3833     $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3834       $self->due_date2str($date_format);
3835   } elsif ( $self->terms ) {
3836     $msg .= ' - '. $self->terms;
3837   }
3838   $msg;
3839 }
3840
3841 sub balance_due_date {
3842   my $self = shift;
3843   my $conf = $self->conf;
3844   my $duedate = '';
3845   if (    $conf->exists('invoice_default_terms') 
3846        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3847     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3848   }
3849   $duedate;
3850 }
3851
3852 sub credit_balance_msg { 
3853   my $self = shift;
3854   $self->mt('Credit Balance Remaining')
3855 }
3856
3857 =item invnum_date_pretty
3858
3859 Returns a string with the invoice number and date, for example:
3860 "Invoice #54 (3/20/2008)"
3861
3862 =cut
3863
3864 sub invnum_date_pretty {
3865   my $self = shift;
3866   $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3867 }
3868
3869 =item _date_pretty
3870
3871 Returns a string with the date, for example: "3/20/2008"
3872
3873 =cut
3874
3875 sub _date_pretty {
3876   my $self = shift;
3877   time2str($date_format, $self->_date);
3878 }
3879
3880 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3881
3882 Generate section information for all items appearing on this invoice.
3883 This will only be called for multi-section invoices.
3884
3885 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
3886 related display records (L<FS::cust_bill_pkg_display>) and organize 
3887 them into two groups ("early" and "late" according to whether they come 
3888 before or after the total), then into sections.  A subtotal is calculated 
3889 for each section.
3890
3891 Section descriptions are returned in sort weight order.  Each consists 
3892 of a hash containing:
3893
3894 description: the package category name, escaped
3895 subtotal: the total charges in that section
3896 tax_section: a flag indicating that the section contains only tax charges
3897 summarized: same as tax_section, for some reason
3898 sort_weight: the package category's sort weight
3899
3900 If 'condense' is set on the display record, it also contains everything 
3901 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3902 coderefs to generate parts of the invoice.  This is not advised.
3903
3904 Arguments:
3905
3906 LATE: an arrayref to push the "late" section hashes onto.  The "early"
3907 group is simply returned from the method.
3908
3909 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3910 Turning this on has the following effects:
3911 - Ignores display items with the 'summary' flag.
3912 - Combines all items into the "early" group.
3913 - Creates sections for all non-disabled package categories, even if they 
3914 have no charges on this invoice, as well as a section with no name.
3915
3916 ESCAPE: an escape function to use for section titles.
3917
3918 EXTRA_SECTIONS: an arrayref of additional sections to return after the 
3919 sorted list.  If there are any of these, section subtotals exclude 
3920 usage charges.
3921
3922 FORMAT: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
3923 passed through to C<_condense_section()>.
3924
3925 =cut
3926
3927 use vars qw(%pkg_category_cache);
3928 sub _items_sections {
3929   my $self = shift;
3930   my $late = shift;
3931   my $summarypage = shift;
3932   my $escape = shift;
3933   my $extra_sections = shift;
3934   my $format = shift;
3935
3936   my %subtotal = ();
3937   my %late_subtotal = ();
3938   my %not_tax = ();
3939
3940   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3941   {
3942
3943       my $usage = $cust_bill_pkg->usage;
3944
3945       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3946         next if ( $display->summary && $summarypage );
3947
3948         my $section = $display->section;
3949         my $type    = $display->type;
3950
3951         $not_tax{$section} = 1
3952           unless $cust_bill_pkg->pkgnum == 0;
3953
3954         if ( $display->post_total && !$summarypage ) {
3955           if (! $type || $type eq 'S') {
3956             $late_subtotal{$section} += $cust_bill_pkg->setup
3957               if $cust_bill_pkg->setup != 0
3958               || $cust_bill_pkg->setup_show_zero;
3959           }
3960
3961           if (! $type) {
3962             $late_subtotal{$section} += $cust_bill_pkg->recur
3963               if $cust_bill_pkg->recur != 0
3964               || $cust_bill_pkg->recur_show_zero;
3965           }
3966
3967           if ($type && $type eq 'R') {
3968             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3969               if $cust_bill_pkg->recur != 0
3970               || $cust_bill_pkg->recur_show_zero;
3971           }
3972           
3973           if ($type && $type eq 'U') {
3974             $late_subtotal{$section} += $usage
3975               unless scalar(@$extra_sections);
3976           }
3977
3978         } else {
3979
3980           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3981
3982           if (! $type || $type eq 'S') {
3983             $subtotal{$section} += $cust_bill_pkg->setup
3984               if $cust_bill_pkg->setup != 0
3985               || $cust_bill_pkg->setup_show_zero;
3986           }
3987
3988           if (! $type) {
3989             $subtotal{$section} += $cust_bill_pkg->recur
3990               if $cust_bill_pkg->recur != 0
3991               || $cust_bill_pkg->recur_show_zero;
3992           }
3993
3994           if ($type && $type eq 'R') {
3995             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3996               if $cust_bill_pkg->recur != 0
3997               || $cust_bill_pkg->recur_show_zero;
3998           }
3999           
4000           if ($type && $type eq 'U') {
4001             $subtotal{$section} += $usage
4002               unless scalar(@$extra_sections);
4003           }
4004
4005         }
4006
4007       }
4008
4009   }
4010
4011   %pkg_category_cache = ();
4012
4013   push @$late, map { { 'description' => &{$escape}($_),
4014                        'subtotal'    => $late_subtotal{$_},
4015                        'post_total'  => 1,
4016                        'sort_weight' => ( _pkg_category($_)
4017                                             ? _pkg_category($_)->weight
4018                                             : 0
4019                                        ),
4020                        ((_pkg_category($_) && _pkg_category($_)->condense)
4021                                            ? $self->_condense_section($format)
4022                                            : ()
4023                        ),
4024                    } }
4025                  sort _sectionsort keys %late_subtotal;
4026
4027   my @sections;
4028   if ( $summarypage ) {
4029     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
4030                 map { $_->categoryname } qsearch('pkg_category', {});
4031     push @sections, '' if exists($subtotal{''});
4032   } else {
4033     @sections = keys %subtotal;
4034   }
4035
4036   my @early = map { { 'description' => &{$escape}($_),
4037                       'subtotal'    => $subtotal{$_},
4038                       'summarized'  => $not_tax{$_} ? '' : 'Y',
4039                       'tax_section' => $not_tax{$_} ? '' : 'Y',
4040                       'sort_weight' => ( _pkg_category($_)
4041                                            ? _pkg_category($_)->weight
4042                                            : 0
4043                                        ),
4044                        ((_pkg_category($_) && _pkg_category($_)->condense)
4045                                            ? $self->_condense_section($format)
4046                                            : ()
4047                        ),
4048                     }
4049                   } @sections;
4050   push @early, @$extra_sections if $extra_sections;
4051
4052   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4053
4054 }
4055
4056 #helper subs for above
4057
4058 sub _sectionsort {
4059   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4060 }
4061
4062 sub _pkg_category {
4063   my $categoryname = shift;
4064   $pkg_category_cache{$categoryname} ||=
4065     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4066 }
4067
4068 my %condensed_format = (
4069   'label' => [ qw( Description Qty Amount ) ],
4070   'fields' => [
4071                 sub { shift->{description} },
4072                 sub { shift->{quantity} },
4073                 sub { my($href, %opt) = @_;
4074                       ($opt{dollar} || ''). $href->{amount};
4075                     },
4076               ],
4077   'align'  => [ qw( l r r ) ],
4078   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
4079   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
4080 );
4081
4082 sub _condense_section {
4083   my ( $self, $format ) = ( shift, shift );
4084   ( 'condensed' => 1,
4085     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4086       qw( description_generator
4087           header_generator
4088           total_generator
4089           total_line_generator
4090         )
4091   );
4092 }
4093
4094 sub _condensed_generator_defaults {
4095   my ( $self, $format ) = ( shift, shift );
4096   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4097 }
4098
4099 my %html_align = (
4100   'c' => 'center',
4101   'l' => 'left',
4102   'r' => 'right',
4103 );
4104
4105 sub _condensed_header_generator {
4106   my ( $self, $format ) = ( shift, shift );
4107
4108   my ( $f, $prefix, $suffix, $separator, $column ) =
4109     _condensed_generator_defaults($format);
4110
4111   if ($format eq 'latex') {
4112     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4113     $suffix = "\\\\\n\\hline";
4114     $separator = "&\n";
4115     $column =
4116       sub { my ($d,$a,$s,$w) = @_;
4117             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4118           };
4119   } elsif ( $format eq 'html' ) {
4120     $prefix = '<th></th>';
4121     $suffix = '';
4122     $separator = '';
4123     $column =
4124       sub { my ($d,$a,$s,$w) = @_;
4125             return qq!<th align="$html_align{$a}">$d</th>!;
4126       };
4127   }
4128
4129   sub {
4130     my @args = @_;
4131     my @result = ();
4132
4133     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4134       push @result,
4135         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4136     }
4137
4138     $prefix. join($separator, @result). $suffix;
4139   };
4140
4141 }
4142
4143 sub _condensed_description_generator {
4144   my ( $self, $format ) = ( shift, shift );
4145
4146   my ( $f, $prefix, $suffix, $separator, $column ) =
4147     _condensed_generator_defaults($format);
4148
4149   my $money_char = '$';
4150   if ($format eq 'latex') {
4151     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4152     $suffix = '\\\\';
4153     $separator = " & \n";
4154     $column =
4155       sub { my ($d,$a,$s,$w) = @_;
4156             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4157           };
4158     $money_char = '\\dollar';
4159   }elsif ( $format eq 'html' ) {
4160     $prefix = '"><td align="center"></td>';
4161     $suffix = '';
4162     $separator = '';
4163     $column =
4164       sub { my ($d,$a,$s,$w) = @_;
4165             return qq!<td align="$html_align{$a}">$d</td>!;
4166       };
4167     #$money_char = $conf->config('money_char') || '$';
4168     $money_char = '';  # this is madness
4169   }
4170
4171   sub {
4172     #my @args = @_;
4173     my $href = shift;
4174     my @result = ();
4175
4176     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4177       my $dollar = '';
4178       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4179       push @result,
4180         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4181                     map { $f->{$_}->[$i] } qw(align span width)
4182                   );
4183     }
4184
4185     $prefix. join( $separator, @result ). $suffix;
4186   };
4187
4188 }
4189
4190 sub _condensed_total_generator {
4191   my ( $self, $format ) = ( shift, shift );
4192
4193   my ( $f, $prefix, $suffix, $separator, $column ) =
4194     _condensed_generator_defaults($format);
4195   my $style = '';
4196
4197   if ($format eq 'latex') {
4198     $prefix = "& ";
4199     $suffix = "\\\\\n";
4200     $separator = " & \n";
4201     $column =
4202       sub { my ($d,$a,$s,$w) = @_;
4203             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4204           };
4205   }elsif ( $format eq 'html' ) {
4206     $prefix = '';
4207     $suffix = '';
4208     $separator = '';
4209     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4210     $column =
4211       sub { my ($d,$a,$s,$w) = @_;
4212             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4213       };
4214   }
4215
4216
4217   sub {
4218     my @args = @_;
4219     my @result = ();
4220
4221     #  my $r = &{$f->{fields}->[$i]}(@args);
4222     #  $r .= ' Total' unless $i;
4223
4224     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4225       push @result,
4226         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4227                     map { $f->{$_}->[$i] } qw(align span width)
4228                   );
4229     }
4230
4231     $prefix. join( $separator, @result ). $suffix;
4232   };
4233
4234 }
4235
4236 =item total_line_generator FORMAT
4237
4238 Returns a coderef used for generation of invoice total line items for this
4239 usage_class.  FORMAT is either html or latex
4240
4241 =cut
4242
4243 # should not be used: will have issues with hash element names (description vs
4244 # total_item and amount vs total_amount -- another array of functions?
4245
4246 sub _condensed_total_line_generator {
4247   my ( $self, $format ) = ( shift, shift );
4248
4249   my ( $f, $prefix, $suffix, $separator, $column ) =
4250     _condensed_generator_defaults($format);
4251   my $style = '';
4252
4253   if ($format eq 'latex') {
4254     $prefix = "& ";
4255     $suffix = "\\\\\n";
4256     $separator = " & \n";
4257     $column =
4258       sub { my ($d,$a,$s,$w) = @_;
4259             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4260           };
4261   }elsif ( $format eq 'html' ) {
4262     $prefix = '';
4263     $suffix = '';
4264     $separator = '';
4265     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4266     $column =
4267       sub { my ($d,$a,$s,$w) = @_;
4268             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4269       };
4270   }
4271
4272
4273   sub {
4274     my @args = @_;
4275     my @result = ();
4276
4277     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4278       push @result,
4279         &{$column}( &{$f->{fields}->[$i]}(@args),
4280                     map { $f->{$_}->[$i] } qw(align span width)
4281                   );
4282     }
4283
4284     $prefix. join( $separator, @result ). $suffix;
4285   };
4286
4287 }
4288
4289 #sub _items_extra_usage_sections {
4290 #  my $self = shift;
4291 #  my $escape = shift;
4292 #
4293 #  my %sections = ();
4294 #
4295 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
4296 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4297 #  {
4298 #    next unless $cust_bill_pkg->pkgnum > 0;
4299 #
4300 #    foreach my $section ( keys %usage_class ) {
4301 #
4302 #      my $usage = $cust_bill_pkg->usage($section);
4303 #
4304 #      next unless $usage && $usage > 0;
4305 #
4306 #      $sections{$section} ||= 0;
4307 #      $sections{$section} += $usage;
4308 #
4309 #    }
4310 #
4311 #  }
4312 #
4313 #  map { { 'description' => &{$escape}($_),
4314 #          'subtotal'    => $sections{$_},
4315 #          'summarized'  => '',
4316 #          'tax_section' => '',
4317 #        }
4318 #      }
4319 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4320 #
4321 #}
4322
4323 sub _items_extra_usage_sections {
4324   my $self = shift;
4325   my $conf = $self->conf;
4326   my $escape = shift;
4327   my $format = shift;
4328
4329   my %sections = ();
4330   my %classnums = ();
4331   my %lines = ();
4332
4333   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4334
4335   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4336   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4337     next unless $cust_bill_pkg->pkgnum > 0;
4338
4339     foreach my $classnum ( keys %usage_class ) {
4340       my $section = $usage_class{$classnum}->classname;
4341       $classnums{$section} = $classnum;
4342
4343       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4344         my $amount = $detail->amount;
4345         next unless $amount && $amount > 0;
4346  
4347         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4348         $sections{$section}{amount} += $amount;  #subtotal
4349         $sections{$section}{calls}++;
4350         $sections{$section}{duration} += $detail->duration;
4351
4352         my $desc = $detail->regionname; 
4353         my $description = $desc;
4354         $description = substr($desc, 0, $maxlength). '...'
4355           if $format eq 'latex' && length($desc) > $maxlength;
4356
4357         $lines{$section}{$desc} ||= {
4358           description     => &{$escape}($description),
4359           #pkgpart         => $part_pkg->pkgpart,
4360           pkgnum          => $cust_bill_pkg->pkgnum,
4361           ref             => '',
4362           amount          => 0,
4363           calls           => 0,
4364           duration        => 0,
4365           #unit_amount     => $cust_bill_pkg->unitrecur,
4366           quantity        => $cust_bill_pkg->quantity,
4367           product_code    => 'N/A',
4368           ext_description => [],
4369         };
4370
4371         $lines{$section}{$desc}{amount} += $amount;
4372         $lines{$section}{$desc}{calls}++;
4373         $lines{$section}{$desc}{duration} += $detail->duration;
4374
4375       }
4376     }
4377   }
4378
4379   my %sectionmap = ();
4380   foreach (keys %sections) {
4381     my $usage_class = $usage_class{$classnums{$_}};
4382     $sectionmap{$_} = { 'description' => &{$escape}($_),
4383                         'amount'    => $sections{$_}{amount},    #subtotal
4384                         'calls'       => $sections{$_}{calls},
4385                         'duration'    => $sections{$_}{duration},
4386                         'summarized'  => '',
4387                         'tax_section' => '',
4388                         'sort_weight' => $usage_class->weight,
4389                         ( $usage_class->format
4390                           ? ( map { $_ => $usage_class->$_($format) }
4391                               qw( description_generator header_generator total_generator total_line_generator )
4392                             )
4393                           : ()
4394                         ), 
4395                       };
4396   }
4397
4398   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4399                  values %sectionmap;
4400
4401   my @lines = ();
4402   foreach my $section ( keys %lines ) {
4403     foreach my $line ( keys %{$lines{$section}} ) {
4404       my $l = $lines{$section}{$line};
4405       $l->{section}     = $sectionmap{$section};
4406       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4407       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4408       push @lines, $l;
4409     }
4410   }
4411
4412   return(\@sections, \@lines);
4413
4414 }
4415
4416 sub _did_summary {
4417     my $self = shift;
4418     my $end = $self->_date;
4419
4420     # start at date of previous invoice + 1 second or 0 if no previous invoice
4421     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4422     $start = 0 if !$start;
4423     $start++;
4424
4425     my $cust_main = $self->cust_main;
4426     my @pkgs = $cust_main->all_pkgs;
4427     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4428         = (0,0,0,0,0);
4429     my @seen = ();
4430     foreach my $pkg ( @pkgs ) {
4431         my @h_cust_svc = $pkg->h_cust_svc($end);
4432         foreach my $h_cust_svc ( @h_cust_svc ) {
4433             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4434             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4435
4436             my $inserted = $h_cust_svc->date_inserted;
4437             my $deleted = $h_cust_svc->date_deleted;
4438             my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4439             my $phone_deleted;
4440             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
4441             
4442 # DID either activated or ported in; cannot be both for same DID simultaneously
4443             if ($inserted >= $start && $inserted <= $end && $phone_inserted
4444                 && (!$phone_inserted->lnp_status 
4445                     || $phone_inserted->lnp_status eq ''
4446                     || $phone_inserted->lnp_status eq 'native')) {
4447                 $num_activated++;
4448             }
4449             else { # this one not so clean, should probably move to (h_)svc_phone
4450                  my $phone_portedin = qsearchs( 'h_svc_phone',
4451                       { 'svcnum' => $h_cust_svc->svcnum, 
4452                         'lnp_status' => 'portedin' },  
4453                       FS::h_svc_phone->sql_h_searchs($end),  
4454                     );
4455                  $num_portedin++ if $phone_portedin;
4456             }
4457
4458 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4459             if($deleted >= $start && $deleted <= $end && $phone_deleted
4460                 && (!$phone_deleted->lnp_status 
4461                     || $phone_deleted->lnp_status ne 'portingout')) {
4462                 $num_deactivated++;
4463             } 
4464             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
4465                 && $phone_deleted->lnp_status 
4466                 && $phone_deleted->lnp_status eq 'portingout') {
4467                 $num_portedout++;
4468             }
4469
4470             # increment usage minutes
4471         if ( $phone_inserted ) {
4472             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4473             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4474         }
4475         else {
4476             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4477         }
4478
4479             # don't look at this service again
4480             push @seen, $h_cust_svc->svcnum;
4481         }
4482     }
4483
4484     $minutes = sprintf("%d", $minutes);
4485     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
4486         . "$num_deactivated  Ported-Out: $num_portedout ",
4487             "Total Minutes: $minutes");
4488 }
4489
4490 sub _items_accountcode_cdr {
4491     my $self = shift;
4492     my $escape = shift;
4493     my $format = shift;
4494
4495     my $section = { 'amount'        => 0,
4496                     'calls'         => 0,
4497                     'duration'      => 0,
4498                     'sort_weight'   => '',
4499                     'phonenum'      => '',
4500                     'description'   => 'Usage by Account Code',
4501                     'post_total'    => '',
4502                     'summarized'    => '',
4503                     'header'        => '',
4504                   };
4505     my @lines;
4506     my %accountcodes = ();
4507
4508     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4509         next unless $cust_bill_pkg->pkgnum > 0;
4510
4511         my @header = $cust_bill_pkg->details_header;
4512         next unless scalar(@header);
4513         $section->{'header'} = join(',',@header);
4514
4515         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4516
4517             $section->{'header'} = $detail->formatted('format' => $format)
4518                 if($detail->detail eq $section->{'header'}); 
4519       
4520             my $accountcode = $detail->accountcode;
4521             next unless $accountcode;
4522
4523             my $amount = $detail->amount;
4524             next unless $amount && $amount > 0;
4525
4526             $accountcodes{$accountcode} ||= {
4527                     description => $accountcode,
4528                     pkgnum      => '',
4529                     ref         => '',
4530                     amount      => 0,
4531                     calls       => 0,
4532                     duration    => 0,
4533                     quantity    => '',
4534                     product_code => 'N/A',
4535                     section     => $section,
4536                     ext_description => [ $section->{'header'} ],
4537                     detail_temp => [],
4538             };
4539
4540             $section->{'amount'} += $amount;
4541             $accountcodes{$accountcode}{'amount'} += $amount;
4542             $accountcodes{$accountcode}{calls}++;
4543             $accountcodes{$accountcode}{duration} += $detail->duration;
4544             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4545         }
4546     }
4547
4548     foreach my $l ( values %accountcodes ) {
4549         $l->{amount} = sprintf( "%.2f", $l->{amount} );
4550         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4551         foreach my $sorted_detail ( @sorted_detail ) {
4552             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4553         }
4554         delete $l->{detail_temp};
4555         push @lines, $l;
4556     }
4557
4558     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4559
4560     return ($section,\@sorted_lines);
4561 }
4562
4563 sub _items_svc_phone_sections {
4564   my $self = shift;
4565   my $conf = $self->conf;
4566   my $escape = shift;
4567   my $format = shift;
4568
4569   my %sections = ();
4570   my %classnums = ();
4571   my %lines = ();
4572
4573   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4574
4575   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4576   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4577
4578   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4579     next unless $cust_bill_pkg->pkgnum > 0;
4580
4581     my @header = $cust_bill_pkg->details_header;
4582     next unless scalar(@header);
4583
4584     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4585
4586       my $phonenum = $detail->phonenum;
4587       next unless $phonenum;
4588
4589       my $amount = $detail->amount;
4590       next unless $amount && $amount > 0;
4591
4592       $sections{$phonenum} ||= { 'amount'      => 0,
4593                                  'calls'       => 0,
4594                                  'duration'    => 0,
4595                                  'sort_weight' => -1,
4596                                  'phonenum'    => $phonenum,
4597                                 };
4598       $sections{$phonenum}{amount} += $amount;  #subtotal
4599       $sections{$phonenum}{calls}++;
4600       $sections{$phonenum}{duration} += $detail->duration;
4601
4602       my $desc = $detail->regionname; 
4603       my $description = $desc;
4604       $description = substr($desc, 0, $maxlength). '...'
4605         if $format eq 'latex' && length($desc) > $maxlength;
4606
4607       $lines{$phonenum}{$desc} ||= {
4608         description     => &{$escape}($description),
4609         #pkgpart         => $part_pkg->pkgpart,
4610         pkgnum          => '',
4611         ref             => '',
4612         amount          => 0,
4613         calls           => 0,
4614         duration        => 0,
4615         #unit_amount     => '',
4616         quantity        => '',
4617         product_code    => 'N/A',
4618         ext_description => [],
4619       };
4620
4621       $lines{$phonenum}{$desc}{amount} += $amount;
4622       $lines{$phonenum}{$desc}{calls}++;
4623       $lines{$phonenum}{$desc}{duration} += $detail->duration;
4624
4625       my $line = $usage_class{$detail->classnum}->classname;
4626       $sections{"$phonenum $line"} ||=
4627         { 'amount' => 0,
4628           'calls' => 0,
4629           'duration' => 0,
4630           'sort_weight' => $usage_class{$detail->classnum}->weight,
4631           'phonenum' => $phonenum,
4632           'header'  => [ @header ],
4633         };
4634       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
4635       $sections{"$phonenum $line"}{calls}++;
4636       $sections{"$phonenum $line"}{duration} += $detail->duration;
4637
4638       $lines{"$phonenum $line"}{$desc} ||= {
4639         description     => &{$escape}($description),
4640         #pkgpart         => $part_pkg->pkgpart,
4641         pkgnum          => '',
4642         ref             => '',
4643         amount          => 0,
4644         calls           => 0,
4645         duration        => 0,
4646         #unit_amount     => '',
4647         quantity        => '',
4648         product_code    => 'N/A',
4649         ext_description => [],
4650       };
4651
4652       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4653       $lines{"$phonenum $line"}{$desc}{calls}++;
4654       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4655       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4656            $detail->formatted('format' => $format);
4657
4658     }
4659   }
4660
4661   my %sectionmap = ();
4662   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4663   foreach ( keys %sections ) {
4664     my @header = @{ $sections{$_}{header} || [] };
4665     my $usage_simple =
4666       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4667     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4668     my $usage_class = $summary ? $simple : $usage_simple;
4669     my $ending = $summary ? ' usage charges' : '';
4670     my %gen_opt = ();
4671     unless ($summary) {
4672       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4673     }
4674     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4675                         'amount'    => $sections{$_}{amount},    #subtotal
4676                         'calls'       => $sections{$_}{calls},
4677                         'duration'    => $sections{$_}{duration},
4678                         'summarized'  => '',
4679                         'tax_section' => '',
4680                         'phonenum'    => $sections{$_}{phonenum},
4681                         'sort_weight' => $sections{$_}{sort_weight},
4682                         'post_total'  => $summary, #inspire pagebreak
4683                         (
4684                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4685                             qw( description_generator
4686                                 header_generator
4687                                 total_generator
4688                                 total_line_generator
4689                               )
4690                           )
4691                         ), 
4692                       };
4693   }
4694
4695   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4696                         $a->{sort_weight} <=> $b->{sort_weight}
4697                       }
4698                  values %sectionmap;
4699
4700   my @lines = ();
4701   foreach my $section ( keys %lines ) {
4702     foreach my $line ( keys %{$lines{$section}} ) {
4703       my $l = $lines{$section}{$line};
4704       $l->{section}     = $sectionmap{$section};
4705       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4706       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4707       push @lines, $l;
4708     }
4709   }
4710   
4711   if($conf->exists('phone_usage_class_summary')) { 
4712       # this only works with Latex
4713       my @newlines;
4714       my @newsections;
4715
4716       # after this, we'll have only two sections per DID:
4717       # Calls Summary and Calls Detail
4718       foreach my $section ( @sections ) {
4719         if($section->{'post_total'}) {
4720             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4721             $section->{'total_line_generator'} = sub { '' };
4722             $section->{'total_generator'} = sub { '' };
4723             $section->{'header_generator'} = sub { '' };
4724             $section->{'description_generator'} = '';
4725             push @newsections, $section;
4726             my %calls_detail = %$section;
4727             $calls_detail{'post_total'} = '';
4728             $calls_detail{'sort_weight'} = '';
4729             $calls_detail{'description_generator'} = sub { '' };
4730             $calls_detail{'header_generator'} = sub {
4731                 return ' & Date/Time & Called Number & Duration & Price'
4732                     if $format eq 'latex';
4733                 '';
4734             };
4735             $calls_detail{'description'} = 'Calls Detail: '
4736                                                     . $section->{'phonenum'};
4737             push @newsections, \%calls_detail;  
4738         }
4739       }
4740
4741       # after this, each usage class is collapsed/summarized into a single
4742       # line under the Calls Summary section
4743       foreach my $newsection ( @newsections ) {
4744         if($newsection->{'post_total'}) { # this means Calls Summary
4745             foreach my $section ( @sections ) {
4746                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
4747                                 && !$section->{'post_total'});
4748                 my $newdesc = $section->{'description'};
4749                 my $tn = $section->{'phonenum'};
4750                 $newdesc =~ s/$tn//g;
4751                 my $line = {  ext_description => [],
4752                               pkgnum => '',
4753                               ref => '',
4754                               quantity => '',
4755                               calls => $section->{'calls'},
4756                               section => $newsection,
4757                               duration => $section->{'duration'},
4758                               description => $newdesc,
4759                               amount => sprintf("%.2f",$section->{'amount'}),
4760                               product_code => 'N/A',
4761                             };
4762                 push @newlines, $line;
4763             }
4764         }
4765       }
4766
4767       # after this, Calls Details is populated with all CDRs
4768       foreach my $newsection ( @newsections ) {
4769         if(!$newsection->{'post_total'}) { # this means Calls Details
4770             foreach my $line ( @lines ) {
4771                 next unless (scalar(@{$line->{'ext_description'}}) &&
4772                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4773                             );
4774                 my @extdesc = @{$line->{'ext_description'}};
4775                 my @newextdesc;
4776                 foreach my $extdesc ( @extdesc ) {
4777                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4778                     push @newextdesc, $extdesc;
4779                 }
4780                 $line->{'ext_description'} = \@newextdesc;
4781                 $line->{'section'} = $newsection;
4782                 push @newlines, $line;
4783             }
4784         }
4785       }
4786
4787       return(\@newsections, \@newlines);
4788   }
4789
4790   return(\@sections, \@lines);
4791
4792 }
4793
4794 sub _items { # seems to be unused
4795   my $self = shift;
4796
4797   #my @display = scalar(@_)
4798   #              ? @_
4799   #              : qw( _items_previous _items_pkg );
4800   #              #: qw( _items_pkg );
4801   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4802   my @display = qw( _items_previous _items_pkg );
4803
4804   my @b = ();
4805   foreach my $display ( @display ) {
4806     push @b, $self->$display(@_);
4807   }
4808   @b;
4809 }
4810
4811 sub _items_previous {
4812   my $self = shift;
4813   my $conf = $self->conf;
4814   my $cust_main = $self->cust_main;
4815   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4816   my @b = ();
4817   foreach ( @pr_cust_bill ) {
4818     my $date = $conf->exists('invoice_show_prior_due_date')
4819                ? 'due '. $_->due_date2str($date_format)
4820                : time2str($date_format, $_->_date);
4821     push @b, {
4822       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4823       #'pkgpart'     => 'N/A',
4824       'pkgnum'      => 'N/A',
4825       'amount'      => sprintf("%.2f", $_->owed),
4826     };
4827   }
4828   @b;
4829
4830   #{
4831   #    'description'     => 'Previous Balance',
4832   #    #'pkgpart'         => 'N/A',
4833   #    'pkgnum'          => 'N/A',
4834   #    'amount'          => sprintf("%10.2f", $pr_total ),
4835   #    'ext_description' => [ map {
4836   #                                 "Invoice ". $_->invnum.
4837   #                                 " (". time2str("%x",$_->_date). ") ".
4838   #                                 sprintf("%10.2f", $_->owed)
4839   #                         } @pr_cust_bill ],
4840
4841   #};
4842 }
4843
4844 =item _items_pkg [ OPTIONS ]
4845
4846 Return line item hashes for each package item on this invoice. Nearly 
4847 equivalent to 
4848
4849 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4850
4851 The only OPTIONS accepted is 'section', which may point to a hashref 
4852 with a key named 'condensed', which may have a true value.  If it 
4853 does, this method tries to merge identical items into items with 
4854 'quantity' equal to the number of items (not the sum of their 
4855 separate quantities, for some reason).
4856
4857 =cut
4858
4859 sub _items_pkg {
4860   my $self = shift;
4861   my %options = @_;
4862
4863   warn "$me _items_pkg searching for all package line items\n"
4864     if $DEBUG > 1;
4865
4866   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4867
4868   warn "$me _items_pkg filtering line items\n"
4869     if $DEBUG > 1;
4870   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4871
4872   if ($options{section} && $options{section}->{condensed}) {
4873
4874     warn "$me _items_pkg condensing section\n"
4875       if $DEBUG > 1;
4876
4877     my %itemshash = ();
4878     local $Storable::canonical = 1;
4879     foreach ( @items ) {
4880       my $item = { %$_ };
4881       delete $item->{ref};
4882       delete $item->{ext_description};
4883       my $key = freeze($item);
4884       $itemshash{$key} ||= 0;
4885       $itemshash{$key} ++; # += $item->{quantity};
4886     }
4887     @items = sort { $a->{description} cmp $b->{description} }
4888              map { my $i = thaw($_);
4889                    $i->{quantity} = $itemshash{$_};
4890                    $i->{amount} =
4891                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4892                    $i;
4893                  }
4894              keys %itemshash;
4895   }
4896
4897   warn "$me _items_pkg returning ". scalar(@items). " items\n"
4898     if $DEBUG > 1;
4899
4900   @items;
4901 }
4902
4903 sub _taxsort {
4904   return 0 unless $a->itemdesc cmp $b->itemdesc;
4905   return -1 if $b->itemdesc eq 'Tax';
4906   return 1 if $a->itemdesc eq 'Tax';
4907   return -1 if $b->itemdesc eq 'Other surcharges';
4908   return 1 if $a->itemdesc eq 'Other surcharges';
4909   $a->itemdesc cmp $b->itemdesc;
4910 }
4911
4912 sub _items_tax {
4913   my $self = shift;
4914   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4915   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4916 }
4917
4918 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4919
4920 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4921 list of hashrefs describing the line items they generate on the invoice.
4922
4923 OPTIONS may include:
4924
4925 format: the invoice format.
4926
4927 escape_function: the function used to escape strings.
4928
4929 DEPRECATED? (expensive, mostly unused?)
4930 format_function: the function used to format CDRs.
4931
4932 section: a hashref containing 'description'; if this is present, 
4933 cust_bill_pkg_display records not belonging to this section are 
4934 ignored.
4935
4936 multisection: a flag indicating that this is a multisection invoice,
4937 which does something complicated.
4938
4939 multilocation: a flag to display the location label for the package.
4940
4941 Returns a list of hashrefs, each of which may contain:
4942
4943 pkgnum, description, amount, unit_amount, quantity, _is_setup, and 
4944 ext_description, which is an arrayref of detail lines to show below 
4945 the package line.
4946
4947 =cut
4948
4949 sub _items_cust_bill_pkg {
4950   my $self = shift;
4951   my $conf = $self->conf;
4952   my $cust_bill_pkgs = shift;
4953   my %opt = @_;
4954
4955   my $format = $opt{format} || '';
4956   my $escape_function = $opt{escape_function} || sub { shift };
4957   my $format_function = $opt{format_function} || '';
4958   my $no_usage = $opt{no_usage} || '';
4959   my $unsquelched = $opt{unsquelched} || ''; #unused
4960   my $section = $opt{section}->{description} if $opt{section};
4961   my $summary_page = $opt{summary_page} || ''; #unused
4962   my $multilocation = $opt{multilocation} || '';
4963   my $multisection = $opt{multisection} || '';
4964   my $discount_show_always = 0;
4965
4966   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4967
4968   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4969
4970   my @b = ();
4971   my ($s, $r, $u) = ( undef, undef, undef );
4972   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4973   {
4974
4975     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4976       if ( $_ && !$cust_bill_pkg->hidden ) {
4977         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4978         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4979         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4980         push @b, { %$_ }
4981           if $_->{amount} != 0
4982           || $discount_show_always
4983           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4984           || (   $_->{_is_setup} && $_->{setup_show_zero} )
4985         ;
4986         $_ = undef;
4987       }
4988     }
4989
4990     my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4991
4992     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4993          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4994       if $DEBUG > 1;
4995
4996     foreach my $display ( grep { defined($section)
4997                                  ? $_->section eq $section
4998                                  : 1
4999                                }
5000                           #grep { !$_->summary || !$summary_page } # bunk!
5001                           grep { !$_->summary || $multisection }
5002                           @cust_bill_pkg_display
5003                         )
5004     {
5005
5006       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
5007            $display->billpkgdisplaynum. "\n"
5008         if $DEBUG > 1;
5009
5010       my $type = $display->type;
5011
5012       my $desc = $cust_bill_pkg->desc;
5013       $desc = substr($desc, 0, $maxlength). '...'
5014         if $format eq 'latex' && length($desc) > $maxlength;
5015
5016       my %details_opt = ( 'format'          => $format,
5017                           'escape_function' => $escape_function,
5018                           'format_function' => $format_function,
5019                           'no_usage'        => $opt{'no_usage'},
5020                         );
5021
5022       if ( $cust_bill_pkg->pkgnum > 0 ) {
5023
5024         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
5025           if $DEBUG > 1;
5026  
5027         my $cust_pkg = $cust_bill_pkg->cust_pkg;
5028
5029         # which pkgpart to show for display purposes?
5030         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
5031
5032         # start/end dates for invoice formats that do nonstandard 
5033         # things with them
5034         my %item_dates = ();
5035         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
5036           unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
5037
5038         if (    (!$type || $type eq 'S')
5039              && (    $cust_bill_pkg->setup != 0
5040                   || $cust_bill_pkg->setup_show_zero
5041                 )
5042            )
5043          {
5044
5045           warn "$me _items_cust_bill_pkg adding setup\n"
5046             if $DEBUG > 1;
5047
5048           my $description = $desc;
5049           $description .= ' Setup'
5050             if $cust_bill_pkg->recur != 0
5051             || $discount_show_always
5052             || $cust_bill_pkg->recur_show_zero;
5053
5054           my @d = ();
5055           my $svc_label;
5056           unless ( $cust_pkg->part_pkg->hide_svc_detail
5057                 || $cust_bill_pkg->hidden )
5058           {
5059
5060             my @svc_labels = map &{$escape_function}($_),
5061                         $cust_pkg->h_labels_short($self->_date, undef, 'I');
5062             push @d, @svc_labels
5063               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5064             $svc_label = $svc_labels[0];
5065
5066             if ( $multilocation ) {
5067               my $loc = $cust_pkg->location_label;
5068               $loc = substr($loc, 0, $maxlength). '...'
5069                 if $format eq 'latex' && length($loc) > $maxlength;
5070               push @d, &{$escape_function}($loc);
5071             }
5072
5073           } #unless hiding service details
5074
5075           push @d, $cust_bill_pkg->details(%details_opt)
5076             if $cust_bill_pkg->recur == 0;
5077
5078           if ( $cust_bill_pkg->hidden ) {
5079             $s->{amount}      += $cust_bill_pkg->setup;
5080             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5081             push @{ $s->{ext_description} }, @d;
5082           } else {
5083             $s = {
5084               _is_setup       => 1,
5085               description     => $description,
5086               pkgpart         => $pkgpart,
5087               pkgnum          => $cust_bill_pkg->pkgnum,
5088               amount          => $cust_bill_pkg->setup,
5089               setup_show_zero => $cust_bill_pkg->setup_show_zero,
5090               unit_amount     => $cust_bill_pkg->unitsetup,
5091               quantity        => $cust_bill_pkg->quantity,
5092               ext_description => \@d,
5093               svc_label       => ($svc_label || ''),
5094             };
5095           };
5096
5097         }
5098
5099         if (    ( !$type || $type eq 'R' || $type eq 'U' )
5100              && (
5101                      $cust_bill_pkg->recur != 0
5102                   || $cust_bill_pkg->setup == 0
5103                   || $discount_show_always
5104                   || $cust_bill_pkg->recur_show_zero
5105                 )
5106            )
5107         {
5108
5109           warn "$me _items_cust_bill_pkg adding recur/usage\n"
5110             if $DEBUG > 1;
5111
5112           my $is_summary = $display->summary;
5113           my $description = ($is_summary && $type && $type eq 'U')
5114                             ? "Usage charges" : $desc;
5115
5116           my $part_pkg = $cust_pkg->part_pkg;
5117
5118           #pry be a bit more efficient to look some of this conf stuff up
5119           # outside the loop
5120           unless (
5121             $conf->exists('disable_line_item_date_ranges')
5122               || $part_pkg->option('disable_line_item_date_ranges',1)
5123               || ! $cust_bill_pkg->sdate
5124               || ! $cust_bill_pkg->edate
5125           ) {
5126             my $time_period;
5127             my $date_style = '';                                               
5128             $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monthly',
5129                                          $cust_main->agentnum                  
5130                                        )                                       
5131               if $part_pkg && $part_pkg->freq !~ /^1m?$/;                      
5132             $date_style ||= $conf->config( 'cust_bill-line_item-date_style',   
5133                                             $cust_main->agentnum
5134                                           );
5135             if ( defined($date_style) && $date_style eq 'month_of' ) {
5136               $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5137             } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5138               my $desc = $conf->config( 'cust_bill-line_item-date_description',
5139                                          $cust_main->agentnum
5140                                       );
5141               $desc .= ' ' unless $desc =~ /\s$/;
5142               $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5143             } else {
5144               $time_period =      time2str($date_format, $cust_bill_pkg->sdate).
5145                            " - ". time2str($date_format, $cust_bill_pkg->edate);
5146             }
5147             $description .= " ($time_period)";
5148           }
5149
5150           my @d = ();
5151           my @seconds = (); # for display of usage info
5152           my $svc_label = '';
5153
5154           #at least until cust_bill_pkg has "past" ranges in addition to
5155           #the "future" sdate/edate ones... see #3032
5156           my @dates = ( $self->_date );
5157           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5158           push @dates, $prev->sdate if $prev;
5159           push @dates, undef if !$prev;
5160
5161           unless ( $cust_pkg->part_pkg->hide_svc_detail
5162                 || $cust_bill_pkg->itemdesc
5163                 || $cust_bill_pkg->hidden
5164                 || $is_summary && $type && $type eq 'U' )
5165           {
5166
5167             warn "$me _items_cust_bill_pkg adding service details\n"
5168               if $DEBUG > 1;
5169
5170             my @svc_labels = map &{$escape_function}($_),
5171                         $cust_pkg->h_labels_short(@dates, 'I');
5172             push @d, @svc_labels
5173               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5174             $svc_label = $svc_labels[0];
5175
5176             warn "$me _items_cust_bill_pkg done adding service details\n"
5177               if $DEBUG > 1;
5178
5179             if ( $multilocation ) {
5180               my $loc = $cust_pkg->location_label;
5181               $loc = substr($loc, 0, $maxlength). '...'
5182                 if $format eq 'latex' && length($loc) > $maxlength;
5183               push @d, &{$escape_function}($loc);
5184             }
5185
5186             # Display of seconds_since_sqlradacct:
5187             # On the invoice, when processing @detail_items, look for a field
5188             # named 'seconds'.  This will contain total seconds for each 
5189             # service, in the same order as @ext_description.  For services 
5190             # that don't support this it will show undef.
5191             if ( $conf->exists('svc_acct-usage_seconds') 
5192                  and ! $cust_bill_pkg->pkgpart_override ) {
5193               foreach my $cust_svc ( 
5194                   $cust_pkg->h_cust_svc(@dates, 'I') 
5195                 ) {
5196
5197                 # eval because not having any part_export_usage exports 
5198                 # is a fatal error, last_bill/_date because that's how 
5199                 # sqlradius_hour billing does it
5200                 my $sec = eval {
5201                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5202                 };
5203                 push @seconds, $sec;
5204               }
5205             } #if svc_acct-usage_seconds
5206
5207           }
5208
5209           unless ( $is_summary ) {
5210             warn "$me _items_cust_bill_pkg adding details\n"
5211               if $DEBUG > 1;
5212
5213             #instead of omitting details entirely in this case (unwanted side
5214             # effects), just omit CDRs
5215             $details_opt{'no_usage'} = 1
5216               if $type && $type eq 'R';
5217
5218             push @d, $cust_bill_pkg->details(%details_opt);
5219           }
5220
5221           warn "$me _items_cust_bill_pkg calculating amount\n"
5222             if $DEBUG > 1;
5223   
5224           my $amount = 0;
5225           if (!$type) {
5226             $amount = $cust_bill_pkg->recur;
5227           } elsif ($type eq 'R') {
5228             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5229           } elsif ($type eq 'U') {
5230             $amount = $cust_bill_pkg->usage;
5231           }
5232   
5233           if ( !$type || $type eq 'R' ) {
5234
5235             warn "$me _items_cust_bill_pkg adding recur\n"
5236               if $DEBUG > 1;
5237
5238             if ( $cust_bill_pkg->hidden ) {
5239               $r->{amount}      += $amount;
5240               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5241               push @{ $r->{ext_description} }, @d;
5242             } else {
5243               $r = {
5244                 description     => $description,
5245                 pkgpart         => $pkgpart,
5246                 pkgnum          => $cust_bill_pkg->pkgnum,
5247                 amount          => $amount,
5248                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5249                 unit_amount     => $cust_bill_pkg->unitrecur,
5250                 quantity        => $cust_bill_pkg->quantity,
5251                 %item_dates,
5252                 ext_description => \@d,
5253                 svc_label       => ($svc_label || ''),
5254               };
5255               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5256             }
5257
5258           } else {  # $type eq 'U'
5259
5260             warn "$me _items_cust_bill_pkg adding usage\n"
5261               if $DEBUG > 1;
5262
5263             if ( $cust_bill_pkg->hidden ) {
5264               $u->{amount}      += $amount;
5265               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5266               push @{ $u->{ext_description} }, @d;
5267             } else {
5268               $u = {
5269                 description     => $description,
5270                 pkgpart         => $pkgpart,
5271                 pkgnum          => $cust_bill_pkg->pkgnum,
5272                 amount          => $amount,
5273                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5274                 unit_amount     => $cust_bill_pkg->unitrecur,
5275                 quantity        => $cust_bill_pkg->quantity,
5276                 %item_dates,
5277                 ext_description => \@d,
5278               };
5279             }
5280           }
5281
5282         } # recurring or usage with recurring charge
5283
5284       } else { #pkgnum tax or one-shot line item (??)
5285
5286         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5287           if $DEBUG > 1;
5288
5289         if ( $cust_bill_pkg->setup != 0 ) {
5290           push @b, {
5291             'description' => $desc,
5292             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
5293           };
5294         }
5295         if ( $cust_bill_pkg->recur != 0 ) {
5296           push @b, {
5297             'description' => "$desc (".
5298                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5299                              time2str($date_format, $cust_bill_pkg->edate). ')',
5300             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
5301           };
5302         }
5303
5304       }
5305
5306     }
5307
5308     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5309                                 && $conf->exists('discount-show-always'));
5310
5311   }
5312
5313   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5314     if ( $_  ) {
5315       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
5316       $_->{amount}      =~ s/^\-0\.00$/0.00/;
5317       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5318       push @b, { %$_ }
5319         if $_->{amount} != 0
5320         || $discount_show_always
5321         || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5322         || (   $_->{_is_setup} && $_->{setup_show_zero} )
5323     }
5324   }
5325
5326   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5327     if $DEBUG > 1;
5328
5329   @b;
5330
5331 }
5332
5333 sub _items_credits {
5334   my( $self, %opt ) = @_;
5335   my $trim_len = $opt{'trim_len'} || 60;
5336
5337   my @b;
5338   #credits
5339   my @objects;
5340   if ( $self->conf->exists('previous_balance-payments_since') ) {
5341     my $date = 0;
5342     $date = $self->previous_bill->_date if $self->previous_bill;
5343     @objects = qsearch('cust_credit', {
5344         'custnum' => $self->custnum,
5345         '_date'   => {op => '>=', value => $date},
5346       });
5347       # hard to do this in the qsearch...
5348     @objects = grep { $_->_date < $self->_date } @objects;
5349   } else {
5350     @objects = $self->cust_credited;
5351   }
5352
5353   foreach my $obj ( @objects ) {
5354     my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
5355
5356     my $reason = substr($cust_credit->reason, 0, $trim_len);
5357     $reason .= '...' if length($reason) < length($cust_credit->reason);
5358     $reason = " ($reason) " if $reason;
5359
5360     push @b, {
5361       #'description' => 'Credit ref\#'. $_->crednum.
5362       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
5363       #                 $reason,
5364       'description' => $self->mt('Credit applied').' '.
5365                        time2str($date_format,$obj->_date). $reason,
5366       'amount'      => sprintf("%.2f",$obj->amount),
5367     };
5368   }
5369
5370   @b;
5371
5372 }
5373
5374 sub _items_payments {
5375   my $self = shift;
5376
5377   my @b;
5378   my $detailed = $self->conf->exists('invoice_payment_details');
5379   my @objects;
5380   if ( $self->conf->exists('previous_balance-payments_since') ) {
5381     my $date = 0;
5382     $date = $self->previous_bill->_date if $self->previous_bill;
5383     @objects = qsearch('cust_pay', {
5384         'custnum' => $self->custnum,
5385         '_date'   => {op => '>=', value => $date},
5386       });
5387     @objects = grep { $_->_date < $self->_date } @objects;
5388   } else {
5389     @objects = $self->cust_bill_pay;
5390   }
5391
5392   foreach my $obj (@objects) {
5393     my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
5394     my $desc = $self->mt('Payment received').' '.
5395                time2str($date_format, $cust_pay->_date );
5396     $desc .= $self->mt(' via ') .
5397              $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
5398       if $detailed;
5399
5400     push @b, {
5401       'description' => $desc,
5402       'amount'      => sprintf("%.2f", $obj->amount )
5403     };
5404   }
5405
5406   @b;
5407
5408 }
5409
5410 =item _items_discounts_avail
5411
5412 Returns an array of line item hashrefs representing available term discounts
5413 for this invoice.  This makes the same assumptions that apply to term 
5414 discounts in general: that the package is billed monthly, at a flat rate, 
5415 with no usage charges.  A prorated first month will be handled, as will 
5416 a setup fee if the discount is allowed to apply to setup fees.
5417
5418 =cut
5419
5420 sub _items_discounts_avail {
5421   my $self = shift;
5422   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5423
5424   my %plans = $self->discount_plans;
5425
5426   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5427
5428   map {
5429     my $months = $_;
5430     my $plan = $plans{$months};
5431
5432     my $term_total = sprintf('%.2f', $plan->discounted_total);
5433     my $percent = sprintf('%.0f', 
5434                           100 * (1 - $term_total / $plan->base_total) );
5435     my $permonth = sprintf('%.2f', $term_total / $months);
5436     my $detail = $self->mt('discount on item'). ' '.
5437                  join(', ', map { "#$_" } $plan->pkgnums)
5438       if $list_pkgnums;
5439
5440     # discounts for non-integer months don't work anyway
5441     $months = sprintf("%d", $months);
5442
5443     +{
5444       description => $self->mt('Save [_1]% by paying for [_2] months',
5445                                 $percent, $months),
5446       amount      => $self->mt('[_1] ([_2] per month)', 
5447                                 $term_total, $money_char.$permonth),
5448       ext_description => ($detail || ''),
5449     }
5450   } #map
5451   sort { $b <=> $a } keys %plans;
5452
5453 }
5454
5455 =item call_details [ OPTION => VALUE ... ]
5456
5457 Returns an array of CSV strings representing the call details for this invoice
5458 The only option available is the boolean prepend_billed_number
5459
5460 =cut
5461
5462 sub call_details {
5463   my ($self, %opt) = @_;
5464
5465   my $format_function = sub { shift };
5466
5467   if ($opt{prepend_billed_number}) {
5468     $format_function = sub {
5469       my $detail = shift;
5470       my $row = shift;
5471
5472       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5473       
5474     };
5475   }
5476
5477   my @details = map { $_->details( 'format_function' => $format_function,
5478                                    'escape_function' => sub{ return() },
5479                                  )
5480                     }
5481                   grep { $_->pkgnum }
5482                   $self->cust_bill_pkg;
5483   my $header = $details[0];
5484   ( $header, grep { $_ ne $header } @details );
5485 }
5486
5487
5488 =back
5489
5490 =head1 SUBROUTINES
5491
5492 =over 4
5493
5494 =item process_reprint
5495
5496 =cut
5497
5498 sub process_reprint {
5499   process_re_X('print', @_);
5500 }
5501
5502 =item process_reemail
5503
5504 =cut
5505
5506 sub process_reemail {
5507   process_re_X('email', @_);
5508 }
5509
5510 =item process_refax
5511
5512 =cut
5513
5514 sub process_refax {
5515   process_re_X('fax', @_);
5516 }
5517
5518 =item process_reftp
5519
5520 =cut
5521
5522 sub process_reftp {
5523   process_re_X('ftp', @_);
5524 }
5525
5526 =item respool
5527
5528 =cut
5529
5530 sub process_respool {
5531   process_re_X('spool', @_);
5532 }
5533
5534 use Storable qw(thaw);
5535 use Data::Dumper;
5536 use MIME::Base64;
5537 sub process_re_X {
5538   my( $method, $job ) = ( shift, shift );
5539   warn "$me process_re_X $method for job $job\n" if $DEBUG;
5540
5541   my $param = thaw(decode_base64(shift));
5542   warn Dumper($param) if $DEBUG;
5543
5544   re_X(
5545     $method,
5546     $job,
5547     %$param,
5548   );
5549
5550 }
5551
5552 sub re_X {
5553   my($method, $job, %param ) = @_;
5554   if ( $DEBUG ) {
5555     warn "re_X $method for job $job with param:\n".
5556          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
5557   }
5558
5559   #some false laziness w/search/cust_bill.html
5560   my $distinct = '';
5561   my $orderby = 'ORDER BY cust_bill._date';
5562
5563   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5564
5565   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5566      
5567   my @cust_bill = qsearch( {
5568     #'select'    => "cust_bill.*",
5569     'table'     => 'cust_bill',
5570     'addl_from' => $addl_from,
5571     'hashref'   => {},
5572     'extra_sql' => $extra_sql,
5573     'order_by'  => $orderby,
5574     'debug' => 1,
5575   } );
5576
5577   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5578
5579   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5580     if $DEBUG;
5581
5582   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5583   foreach my $cust_bill ( @cust_bill ) {
5584     $cust_bill->$method();
5585
5586     if ( $job ) { #progressbar foo
5587       $num++;
5588       if ( time - $min_sec > $last ) {
5589         my $error = $job->update_statustext(
5590           int( 100 * $num / scalar(@cust_bill) )
5591         );
5592         die $error if $error;
5593         $last = time;
5594       }
5595     }
5596
5597   }
5598
5599 }
5600
5601 =back
5602
5603 =head1 CLASS METHODS
5604
5605 =over 4
5606
5607 =item owed_sql
5608
5609 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5610
5611 =cut
5612
5613 sub owed_sql {
5614   my ($class, $start, $end) = @_;
5615   'charged - '. 
5616     $class->paid_sql($start, $end). ' - '. 
5617     $class->credited_sql($start, $end);
5618 }
5619
5620 =item net_sql
5621
5622 Returns an SQL fragment to retreive the net amount (charged minus credited).
5623
5624 =cut
5625
5626 sub net_sql {
5627   my ($class, $start, $end) = @_;
5628   'charged - '. $class->credited_sql($start, $end);
5629 }
5630
5631 =item paid_sql
5632
5633 Returns an SQL fragment to retreive the amount paid against this invoice.
5634
5635 =cut
5636
5637 sub paid_sql {
5638   my ($class, $start, $end) = @_;
5639   $start &&= "AND cust_bill_pay._date <= $start";
5640   $end   &&= "AND cust_bill_pay._date > $end";
5641   $start = '' unless defined($start);
5642   $end   = '' unless defined($end);
5643   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5644        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
5645 }
5646
5647 =item credited_sql
5648
5649 Returns an SQL fragment to retreive the amount credited against this invoice.
5650
5651 =cut
5652
5653 sub credited_sql {
5654   my ($class, $start, $end) = @_;
5655   $start &&= "AND cust_credit_bill._date <= $start";
5656   $end   &&= "AND cust_credit_bill._date >  $end";
5657   $start = '' unless defined($start);
5658   $end   = '' unless defined($end);
5659   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5660        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
5661 }
5662
5663 =item due_date_sql
5664
5665 Returns an SQL fragment to retrieve the due date of an invoice.
5666 Currently only supported on PostgreSQL.
5667
5668 =cut
5669
5670 sub due_date_sql {
5671   my $conf = new FS::Conf;
5672 'COALESCE(
5673   SUBSTRING(
5674     COALESCE(
5675       cust_bill.invoice_terms,
5676       cust_main.invoice_terms,
5677       \''.($conf->config('invoice_default_terms') || '').'\'
5678     ), E\'Net (\\\\d+)\'
5679   )::INTEGER, 0
5680 ) * 86400 + cust_bill._date'
5681 }
5682
5683 =item search_sql_where HASHREF
5684
5685 Class method which returns an SQL WHERE fragment to search for parameters
5686 specified in HASHREF.  Valid parameters are
5687
5688 =over 4
5689
5690 =item _date
5691
5692 List reference of start date, end date, as UNIX timestamps.
5693
5694 =item invnum_min
5695
5696 =item invnum_max
5697
5698 =item agentnum
5699
5700 =item charged
5701
5702 List reference of charged limits (exclusive).
5703
5704 =item owed
5705
5706 List reference of charged limits (exclusive).
5707
5708 =item open
5709
5710 flag, return open invoices only
5711
5712 =item net
5713
5714 flag, return net invoices only
5715
5716 =item days
5717
5718 =item newest_percust
5719
5720 =back
5721
5722 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5723
5724 =cut
5725
5726 sub search_sql_where {
5727   my($class, $param) = @_;
5728   if ( $DEBUG ) {
5729     warn "$me search_sql_where called with params: \n".
5730          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
5731   }
5732
5733   my @search = ();
5734
5735   #agentnum
5736   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5737     push @search, "cust_main.agentnum = $1";
5738   }
5739
5740   #refnum
5741   if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5742     push @search, "cust_main.refnum = $1";
5743   }
5744
5745   #custnum
5746   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5747     push @search, "cust_bill.custnum = $1";
5748   }
5749
5750   #customer classnum
5751   if ( $param->{'cust_classnum'} ) {
5752     my $classnums = $param->{'cust_classnum'};
5753     $classnums = [ $classnums ] if !ref($classnums);
5754     $classnums = [ grep /^\d+$/, @$classnums ];
5755     push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
5756       if @$classnums;
5757   }
5758
5759   #_date
5760   if ( $param->{_date} ) {
5761     my($beginning, $ending) = @{$param->{_date}};
5762
5763     push @search, "cust_bill._date >= $beginning",
5764                   "cust_bill._date <  $ending";
5765   }
5766
5767   #invnum
5768   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5769     push @search, "cust_bill.invnum >= $1";
5770   }
5771   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5772     push @search, "cust_bill.invnum <= $1";
5773   }
5774
5775   #charged
5776   if ( $param->{charged} ) {
5777     my @charged = ref($param->{charged})
5778                     ? @{ $param->{charged} }
5779                     : ($param->{charged});
5780
5781     push @search, map { s/^charged/cust_bill.charged/; $_; }
5782                       @charged;
5783   }
5784
5785   my $owed_sql = FS::cust_bill->owed_sql;
5786
5787   #owed
5788   if ( $param->{owed} ) {
5789     my @owed = ref($param->{owed})
5790                  ? @{ $param->{owed} }
5791                  : ($param->{owed});
5792     push @search, map { s/^owed/$owed_sql/; $_; }
5793                       @owed;
5794   }
5795
5796   #open/net flags
5797   push @search, "0 != $owed_sql"
5798     if $param->{'open'};
5799   push @search, '0 != '. FS::cust_bill->net_sql
5800     if $param->{'net'};
5801
5802   #days
5803   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5804     if $param->{'days'};
5805
5806   #newest_percust
5807   if ( $param->{'newest_percust'} ) {
5808
5809     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5810     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5811
5812     my @newest_where = map { my $x = $_;
5813                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
5814                              $x;
5815                            }
5816                            grep ! /^cust_main./, @search;
5817     my $newest_where = scalar(@newest_where)
5818                          ? ' AND '. join(' AND ', @newest_where)
5819                          : '';
5820
5821
5822     push @search, "cust_bill._date = (
5823       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5824         WHERE newest_cust_bill.custnum = cust_bill.custnum
5825           $newest_where
5826     )";
5827
5828   }
5829
5830   #promised_date - also has an option to accept nulls
5831   if ( $param->{promised_date} ) {
5832     my($beginning, $ending, $null) = @{$param->{promised_date}};
5833
5834     push @search, "(( cust_bill.promised_date >= $beginning AND ".
5835                     "cust_bill.promised_date <  $ending )" .
5836                     ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5837   }
5838
5839   #agent virtualization
5840   my $curuser = $FS::CurrentUser::CurrentUser;
5841   if ( $curuser->username eq 'fs_queue'
5842        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5843     my $username = $1;
5844     my $newuser = qsearchs('access_user', {
5845       'username' => $username,
5846       'disabled' => '',
5847     } );
5848     if ( $newuser ) {
5849       $curuser = $newuser;
5850     } else {
5851       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5852     }
5853   }
5854   push @search, $curuser->agentnums_sql;
5855
5856   join(' AND ', @search );
5857
5858 }
5859
5860 =back
5861
5862 =head1 BUGS
5863
5864 The delete method.
5865
5866 =head1 SEE ALSO
5867
5868 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5869 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
5870 documentation.
5871
5872 =cut
5873
5874 1;
5875