optionally display payments/credits on invoice based on date received, #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                     };
3005   my $tax_weight = _pkg_category($tax_section->{description})
3006                         ? _pkg_category($tax_section->{description})->weight
3007                         : 0;
3008   $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
3009   $tax_section->{'sort_weight'} = $tax_weight;
3010
3011
3012   my $adjusttotal = 0;
3013   my $adjust_section = {
3014     'description'    => $self->mt('Credits, Payments, and Adjustments'),
3015     'adjust_section' => 1,
3016     'subtotal'       => 0,   # adjusted below
3017   };
3018   my $adjust_weight = _pkg_category($adjust_section->{description})
3019                         ? _pkg_category($adjust_section->{description})->weight
3020                         : 0;
3021   $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
3022   $adjust_section->{'sort_weight'} = $adjust_weight;
3023
3024   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
3025   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
3026   $invoice_data{'multisection'} = $multisection;
3027   my $late_sections = [];
3028   my $extra_sections = [];
3029   my $extra_lines = ();
3030
3031   my $default_section = { 'description' => '',
3032                           'subtotal'    => '', 
3033                           'no_subtotal' => 1,
3034                         };
3035
3036   if ( $multisection ) {
3037     ($extra_sections, $extra_lines) =
3038       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3039       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3040
3041     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3042
3043     push @detail_items, @$extra_lines if $extra_lines;
3044     push @sections,
3045       $self->_items_sections( $late_sections,      # this could stand a refactor
3046                               $summarypage,
3047                               $escape_function_nonbsp,
3048                               $extra_sections,
3049                               $format,             #bah
3050                             );
3051     if ($conf->exists('svc_phone_sections')) {
3052       my ($phone_sections, $phone_lines) =
3053         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3054       push @{$late_sections}, @$phone_sections;
3055       push @detail_items, @$phone_lines;
3056     }
3057     if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3058       my ($accountcode_section, $accountcode_lines) =
3059         $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3060       if ( scalar(@$accountcode_lines) ) {
3061           push @{$late_sections}, $accountcode_section;
3062           push @detail_items, @$accountcode_lines;
3063       }
3064     }
3065   } else {# not multisection
3066     # make a default section
3067     push @sections, $default_section;
3068     # and calculate the finance charge total, since it won't get done otherwise.
3069     # XXX possibly other totals?
3070     # XXX possibly finance_pkgclass should not be used in this manner?
3071     if ( $conf->exists('finance_pkgclass') ) {
3072       my @finance_charges;
3073       foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3074         if ( grep { $_->section eq $invoice_data{finance_section} }
3075              $cust_bill_pkg->cust_bill_pkg_display ) {
3076           # I think these are always setup fees, but just to be sure...
3077           push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3078         }
3079       }
3080       $invoice_data{finance_amount} = 
3081         sprintf('%.2f', sum( @finance_charges ) || 0);
3082     }
3083   }
3084
3085   # previous invoice balances in the Previous Charges section if there
3086   # is one, otherwise in the main detail section
3087   if ( $self->can('_items_previous') &&
3088        $self->enable_previous &&
3089        ! $conf->exists('previous_balance-summary_only') ) {
3090
3091     warn "$me adding previous balances\n"
3092       if $DEBUG > 1;
3093
3094     foreach my $line_item ( $self->_items_previous ) {
3095
3096       my $detail = {
3097         ext_description => [],
3098       };
3099       $detail->{'ref'} = $line_item->{'pkgnum'};
3100       $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3101       $detail->{'quantity'} = 1;
3102       $detail->{'section'} = $multisection ? $previous_section
3103                                            : $default_section;
3104       $detail->{'description'} = &$escape_function($line_item->{'description'});
3105       if ( exists $line_item->{'ext_description'} ) {
3106         @{$detail->{'ext_description'}} = map {
3107           &$escape_function($_);
3108         } @{$line_item->{'ext_description'}};
3109       }
3110       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3111                             $line_item->{'amount'};
3112       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3113
3114       push @detail_items, $detail;
3115       push @buf, [ $detail->{'description'},
3116                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3117                  ];
3118     }
3119
3120   }
3121
3122   if ( @pr_cust_bill && $self->enable_previous ) {
3123     push @buf, ['','-----------'];
3124     push @buf, [ $self->mt('Total Previous Balance'),
3125                  $money_char. sprintf("%10.2f", $pr_total) ];
3126     push @buf, ['',''];
3127   }
3128  
3129   if ( $conf->exists('svc_phone-did-summary') ) {
3130       warn "$me adding DID summary\n"
3131         if $DEBUG > 1;
3132
3133       my ($didsummary,$minutes) = $self->_did_summary;
3134       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3135       push @detail_items, 
3136        { 'description' => $didsummary_desc,
3137            'ext_description' => [ $didsummary, $minutes ],
3138        };
3139   }
3140
3141   foreach my $section (@sections, @$late_sections) {
3142
3143     warn "$me adding section \n". Dumper($section)
3144       if $DEBUG > 1;
3145
3146     # begin some normalization
3147     $section->{'subtotal'} = $section->{'amount'}
3148       if $multisection
3149          && !exists($section->{subtotal})
3150          && exists($section->{amount});
3151
3152     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3153       if ( $invoice_data{finance_section} &&
3154            $section->{'description'} eq $invoice_data{finance_section} );
3155
3156     $section->{'subtotal'} = $other_money_char.
3157                              sprintf('%.2f', $section->{'subtotal'})
3158       if $multisection;
3159
3160     # continue some normalization
3161     $section->{'amount'}   = $section->{'subtotal'}
3162       if $multisection;
3163
3164
3165     if ( $section->{'description'} ) {
3166       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3167                    [ '', '' ],
3168                  );
3169     }
3170
3171     warn "$me   setting options\n"
3172       if $DEBUG > 1;
3173
3174     my $multilocation = scalar($cust_main->cust_location); #too expensive?
3175     my %options = ();
3176     $options{'section'} = $section if $multisection;
3177     $options{'format'} = $format;
3178     $options{'escape_function'} = $escape_function;
3179     $options{'no_usage'} = 1 unless $unsquelched;
3180     $options{'unsquelched'} = $unsquelched;
3181     $options{'summary_page'} = $summarypage;
3182     $options{'skip_usage'} =
3183       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3184     $options{'multilocation'} = $multilocation;
3185     $options{'multisection'} = $multisection;
3186
3187     warn "$me   searching for line items\n"
3188       if $DEBUG > 1;
3189
3190     foreach my $line_item ( $self->_items_pkg(%options) ) {
3191
3192       warn "$me     adding line item $line_item\n"
3193         if $DEBUG > 1;
3194
3195       my $detail = {
3196         ext_description => [],
3197       };
3198       $detail->{'ref'} = $line_item->{'pkgnum'};
3199       $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3200       $detail->{'quantity'} = $line_item->{'quantity'};
3201       $detail->{'section'} = $section;
3202       $detail->{'description'} = &$escape_function($line_item->{'description'});
3203       if ( exists $line_item->{'ext_description'} ) {
3204         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3205       }
3206       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3207                               $line_item->{'amount'};
3208       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3209                                  $line_item->{'unit_amount'};
3210       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3211
3212       $detail->{'sdate'} = $line_item->{'sdate'};
3213       $detail->{'edate'} = $line_item->{'edate'};
3214       $detail->{'seconds'} = $line_item->{'seconds'};
3215       $detail->{'svc_label'} = $line_item->{'svc_label'};
3216   
3217       push @detail_items, $detail;
3218       push @buf, ( [ $detail->{'description'},
3219                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3220                    ],
3221                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3222                  );
3223     }
3224
3225     if ( $section->{'description'} ) {
3226       push @buf, ( ['','-----------'],
3227                    [ $section->{'description'}. ' sub-total',
3228                       $section->{'subtotal'} # already formatted this 
3229                    ],
3230                    [ '', '' ],
3231                    [ '', '' ],
3232                  );
3233     }
3234   
3235   }
3236
3237   $invoice_data{current_less_finance} =
3238     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3239
3240   # create a major section for previous balance if we have major sections,
3241   # or if previous_section is in summary form
3242   if ( ( $multisection && $self->enable_previous )
3243     || $conf->exists('previous_balance-summary_only') )
3244   {
3245     unshift @sections, $previous_section if $pr_total;
3246   }
3247
3248   warn "$me adding taxes\n"
3249     if $DEBUG > 1;
3250
3251   foreach my $tax ( $self->_items_tax ) {
3252
3253     $taxtotal += $tax->{'amount'};
3254
3255     my $description = &$escape_function( $tax->{'description'} );
3256     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
3257
3258     if ( $multisection ) {
3259
3260       my $money = $old_latex ? '' : $money_char;
3261       push @detail_items, {
3262         ext_description => [],
3263         ref          => '',
3264         quantity     => '',
3265         description  => $description,
3266         amount       => $money. $amount,
3267         product_code => '',
3268         section      => $tax_section,
3269       };
3270
3271     } else {
3272
3273       push @total_items, {
3274         'total_item'   => $description,
3275         'total_amount' => $other_money_char. $amount,
3276       };
3277
3278     }
3279
3280     push @buf,[ $description,
3281                 $money_char. $amount,
3282               ];
3283
3284   }
3285   
3286   if ( $taxtotal ) {
3287     my $total = {};
3288     $total->{'total_item'} = $self->mt('Sub-total');
3289     $total->{'total_amount'} =
3290       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3291
3292     if ( $multisection ) {
3293       $tax_section->{'subtotal'} = $other_money_char.
3294                                    sprintf('%.2f', $taxtotal);
3295       $tax_section->{'pretotal'} = 'New charges sub-total '.
3296                                    $total->{'total_amount'};
3297       push @sections, $tax_section if $taxtotal;
3298     }else{
3299       unshift @total_items, $total;
3300     }
3301   }
3302   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3303
3304   push @buf,['','-----------'];
3305   push @buf,[$self->mt( 
3306               (!$self->enable_previous)
3307                ? 'Total Charges'
3308                : 'Total New Charges'
3309              ),
3310              $money_char. sprintf("%10.2f",$self->charged) ];
3311   push @buf,['',''];
3312
3313   # calculate total, possibly including total owed on previous
3314   # invoices
3315   {
3316     my $total = {};
3317     my $item = 'Total';
3318     $item = $conf->config('previous_balance-exclude_from_total')
3319          || 'Total New Charges'
3320       if $conf->exists('previous_balance-exclude_from_total');
3321     my $amount = $self->charged;
3322     if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3323       $amount += $pr_total;
3324     }
3325
3326     $total->{'total_item'} = &$embolden_function($self->mt($item));
3327     $total->{'total_amount'} =
3328       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
3329     if ( $multisection ) {
3330       if ( $adjust_section->{'sort_weight'} ) {
3331         $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3332           $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
3333       } else {
3334         $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3335           $other_money_char.  sprintf('%.2f', $self->charged );
3336       } 
3337     } else {
3338       push @total_items, $total;
3339     }
3340     push @buf,['','-----------'];
3341     push @buf,[$item,
3342                $money_char.
3343                sprintf( '%10.2f', $amount )
3344               ];
3345     push @buf,['',''];
3346   }
3347
3348   # if we're showing previous invoices, also show previous
3349   # credits and payments 
3350   if ( $self->enable_previous 
3351         and $self->can('_items_credits')
3352         and $self->can('_items_payments') )
3353     {
3354     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3355   
3356     # credits
3357     my $credittotal = 0;
3358     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3359
3360       my $total;
3361       $total->{'total_item'} = &$escape_function($credit->{'description'});
3362       $credittotal += $credit->{'amount'};
3363       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3364       $adjusttotal += $credit->{'amount'};
3365       if ( $multisection ) {
3366         my $money = $old_latex ? '' : $money_char;
3367         push @detail_items, {
3368           ext_description => [],
3369           ref          => '',
3370           quantity     => '',
3371           description  => &$escape_function($credit->{'description'}),
3372           amount       => $money. $credit->{'amount'},
3373           product_code => '',
3374           section      => $adjust_section,
3375         };
3376       } else {
3377         push @total_items, $total;
3378       }
3379
3380     }
3381     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3382
3383     #credits (again)
3384     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3385       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3386     }
3387
3388     # payments
3389     my $paymenttotal = 0;
3390     foreach my $payment ( $self->_items_payments ) {
3391       my $total = {};
3392       $total->{'total_item'} = &$escape_function($payment->{'description'});
3393       $paymenttotal += $payment->{'amount'};
3394       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3395       $adjusttotal += $payment->{'amount'};
3396       if ( $multisection ) {
3397         my $money = $old_latex ? '' : $money_char;
3398         push @detail_items, {
3399           ext_description => [],
3400           ref          => '',
3401           quantity     => '',
3402           description  => &$escape_function($payment->{'description'}),
3403           amount       => $money. $payment->{'amount'},
3404           product_code => '',
3405           section      => $adjust_section,
3406         };
3407       }else{
3408         push @total_items, $total;
3409       }
3410       push @buf, [ $payment->{'description'},
3411                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
3412                  ];
3413     }
3414     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3415   
3416     if ( $multisection ) {
3417       $adjust_section->{'subtotal'} = $other_money_char.
3418                                       sprintf('%.2f', $adjusttotal);
3419       push @sections, $adjust_section
3420         unless $adjust_section->{sort_weight};
3421     }
3422
3423     # create Balance Due message
3424     { 
3425       my $total;
3426       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3427       $total->{'total_amount'} =
3428         &$embolden_function(
3429           $other_money_char. sprintf('%.2f', #why? $summarypage 
3430                                              #  ? $self->charged +
3431                                              #    $self->billing_balance
3432                                              #  :
3433                                                  $self->owed + $pr_total
3434                                     )
3435         );
3436       if ( $multisection && !$adjust_section->{sort_weight} ) {
3437         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3438                                          $total->{'total_amount'};
3439       }else{
3440         push @total_items, $total;
3441       }
3442       push @buf,['','-----------'];
3443       push @buf,[$self->balance_due_msg, $money_char. 
3444         sprintf("%10.2f", $balance_due ) ];
3445     }
3446
3447     if ( $conf->exists('previous_balance-show_credit')
3448         and $cust_main->balance < 0 ) {
3449       my $credit_total = {
3450         'total_item'    => &$embolden_function($self->credit_balance_msg),
3451         'total_amount'  => &$embolden_function(
3452           $other_money_char. sprintf('%.2f', -$cust_main->balance)
3453         ),
3454       };
3455       if ( $multisection ) {
3456         $adjust_section->{'posttotal'} .= $newline_token .
3457           $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3458       }
3459       else {
3460         push @total_items, $credit_total;
3461       }
3462       push @buf,['','-----------'];
3463       push @buf,[$self->credit_balance_msg, $money_char. 
3464         sprintf("%10.2f", -$cust_main->balance ) ];
3465     }
3466   }
3467
3468   if ( $multisection ) {
3469     if ($conf->exists('svc_phone_sections')) {
3470       my $total;
3471       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3472       $total->{'total_amount'} =
3473         &$embolden_function(
3474           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3475         );
3476       my $last_section = pop @sections;
3477       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3478                                      $total->{'total_amount'};
3479       push @sections, $last_section;
3480     }
3481     push @sections, @$late_sections
3482       if $unsquelched;
3483   }
3484
3485   # make a discounts-available section, even without multisection
3486   if ( $conf->exists('discount-show_available') 
3487        and my @discounts_avail = $self->_items_discounts_avail ) {
3488     my $discount_section = {
3489       'description' => $self->mt('Discounts Available'),
3490       'subtotal'    => '',
3491       'no_subtotal' => 1,
3492     };
3493
3494     push @sections, $discount_section;
3495     push @detail_items, map { +{
3496         'ref'         => '', #should this be something else?
3497         'section'     => $discount_section,
3498         'description' => &$escape_function( $_->{description} ),
3499         'amount'      => $money_char . &$escape_function( $_->{amount} ),
3500         'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3501     } } @discounts_avail;
3502   }
3503
3504   # debugging hook: call this with 'diag' => 1 to just get a hash of
3505   # the invoice variables
3506   return \%invoice_data if ( $params{'diag'} );
3507
3508   # All sections and items are built; now fill in templates.
3509   my @includelist = ();
3510   push @includelist, 'summary' if $summarypage;
3511   foreach my $include ( @includelist ) {
3512
3513     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3514     my @inc_src;
3515
3516     if ( length( $conf->config($inc_file, $agentnum) ) ) {
3517
3518       @inc_src = $conf->config($inc_file, $agentnum);
3519
3520     } else {
3521
3522       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3523
3524       my $convert_map = $convert_maps{$format}{$include};
3525
3526       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3527                        s/--\@\]/$delimiters{$format}[1]/g;
3528                        $_;
3529                      } 
3530                  &$convert_map( $conf->config($inc_file, $agentnum) );
3531
3532     }
3533
3534     my $inc_tt = new Text::Template (
3535       TYPE       => 'ARRAY',
3536       SOURCE     => [ map "$_\n", @inc_src ],
3537       DELIMITERS => $delimiters{$format},
3538     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3539
3540     unless ( $inc_tt->compile() ) {
3541       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3542       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3543       die $error;
3544     }
3545
3546     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3547
3548     $invoice_data{$include} =~ s/\n+$//
3549       if ($format eq 'latex');
3550   }
3551
3552   $invoice_lines = 0;
3553   my $wasfunc = 0;
3554   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3555     /invoice_lines\((\d*)\)/;
3556     $invoice_lines += $1 || scalar(@buf);
3557     $wasfunc=1;
3558   }
3559   die "no invoice_lines() functions in template?"
3560     if ( $format eq 'template' && !$wasfunc );
3561
3562   if ($format eq 'template') {
3563
3564     if ( $invoice_lines ) {
3565       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3566       $invoice_data{'total_pages'}++
3567         if scalar(@buf) % $invoice_lines;
3568     }
3569
3570     #setup subroutine for the template
3571     $invoice_data{invoice_lines} = sub {
3572       my $lines = shift || scalar(@buf);
3573       map { 
3574         scalar(@buf)
3575           ? shift @buf
3576           : [ '', '' ];
3577       }
3578       ( 1 .. $lines );
3579     };
3580
3581     my $lines;
3582     my @collect;
3583     while (@buf) {
3584       push @collect, split("\n",
3585         $text_template->fill_in( HASH => \%invoice_data )
3586       );
3587       $invoice_data{'page'}++;
3588     }
3589     map "$_\n", @collect;
3590   }else{
3591     # this is where we actually create the invoice
3592     warn "filling in template for invoice ". $self->invnum. "\n"
3593       if $DEBUG;
3594     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3595       if $DEBUG > 1;
3596
3597     $text_template->fill_in(HASH => \%invoice_data);
3598   }
3599 }
3600
3601 # helper routine for generating date ranges
3602 sub _prior_month30s {
3603   my $self = shift;
3604   my @ranges = (
3605    [ 1,       2592000 ], # 0-30 days ago
3606    [ 2592000, 5184000 ], # 30-60 days ago
3607    [ 5184000, 7776000 ], # 60-90 days ago
3608    [ 7776000, 0       ], # 90+   days ago
3609   );
3610
3611   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3612           $_->[1] ? $self->_date - $_->[1] - 1 : '',
3613       ] }
3614   @ranges;
3615 }
3616
3617 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3618
3619 Returns an postscript invoice, as a scalar.
3620
3621 Options can be passed as a hashref (recommended) or as a list of time, template
3622 and then any key/value pairs for any other options.
3623
3624 I<time> an optional value used to control the printing of overdue messages.  The
3625 default is now.  It isn't the date of the invoice; that's the `_date' field.
3626 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3627 L<Time::Local> and L<Date::Parse> for conversion functions.
3628
3629 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3630
3631 =cut
3632
3633 sub print_ps {
3634   my $self = shift;
3635
3636   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3637   my $ps = generate_ps($file);
3638   unlink($logofile);
3639   unlink($barcodefile) if $barcodefile;
3640
3641   $ps;
3642 }
3643
3644 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3645
3646 Returns an PDF invoice, as a scalar.
3647
3648 Options can be passed as a hashref (recommended) or as a list of time, template
3649 and then any key/value pairs for any other options.
3650
3651 I<time> an optional value used to control the printing of overdue messages.  The
3652 default is now.  It isn't the date of the invoice; that's the `_date' field.
3653 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3654 L<Time::Local> and L<Date::Parse> for conversion functions.
3655
3656 I<template>, if specified, is the name of a suffix for alternate invoices.
3657
3658 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3659
3660 =cut
3661
3662 sub print_pdf {
3663   my $self = shift;
3664
3665   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3666   my $pdf = generate_pdf($file);
3667   unlink($logofile);
3668   unlink($barcodefile) if $barcodefile;
3669
3670   $pdf;
3671 }
3672
3673 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3674
3675 Returns an HTML invoice, as a scalar.
3676
3677 I<time> an optional value used to control the printing of overdue messages.  The
3678 default is now.  It isn't the date of the invoice; that's the `_date' field.
3679 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3680 L<Time::Local> and L<Date::Parse> for conversion functions.
3681
3682 I<template>, if specified, is the name of a suffix for alternate invoices.
3683
3684 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3685
3686 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3687 when emailing the invoice as part of a multipart/related MIME email.
3688
3689 =cut
3690
3691 sub print_html {
3692   my $self = shift;
3693   my %params;
3694   if ( ref($_[0]) ) {
3695     %params = %{ shift() }; 
3696   }else{
3697     $params{'time'} = shift;
3698     $params{'template'} = shift;
3699     $params{'cid'} = shift;
3700   }
3701
3702   $params{'format'} = 'html';
3703   
3704   $self->print_generic( %params );
3705 }
3706
3707 # quick subroutine for print_latex
3708 #
3709 # There are ten characters that LaTeX treats as special characters, which
3710 # means that they do not simply typeset themselves: 
3711 #      # $ % & ~ _ ^ \ { }
3712 #
3713 # TeX ignores blanks following an escaped character; if you want a blank (as
3714 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3715
3716 sub _latex_escape {
3717   my $value = shift;
3718   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3719   $value =~ s/([<>])/\$$1\$/g;
3720   $value;
3721 }
3722
3723 sub _html_escape {
3724   my $value = shift;
3725   encode_entities($value);
3726   $value;
3727 }
3728
3729 sub _html_escape_nbsp {
3730   my $value = _html_escape(shift);
3731   $value =~ s/ +/&nbsp;/g;
3732   $value;
3733 }
3734
3735 #utility methods for print_*
3736
3737 sub _translate_old_latex_format {
3738   warn "_translate_old_latex_format called\n"
3739     if $DEBUG; 
3740
3741   my @template = ();
3742   while ( @_ ) {
3743     my $line = shift;
3744   
3745     if ( $line =~ /^%%Detail\s*$/ ) {
3746   
3747       push @template, q![@--!,
3748                       q!  foreach my $_tr_line (@detail_items) {!,
3749                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3750                       q!      $_tr_line->{'description'} .= !, 
3751                       q!        "\\tabularnewline\n~~".!,
3752                       q!        join( "\\tabularnewline\n~~",!,
3753                       q!          @{$_tr_line->{'ext_description'}}!,
3754                       q!        );!,
3755                       q!    }!;
3756
3757       while ( ( my $line_item_line = shift )
3758               !~ /^%%EndDetail\s*$/                            ) {
3759         $line_item_line =~ s/'/\\'/g;    # nice LTS
3760         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3761         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3762         push @template, "    \$OUT .= '$line_item_line';";
3763       }
3764
3765       push @template, '}',
3766                       '--@]';
3767       #' doh, gvim
3768     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3769
3770       push @template, '[@--',
3771                       '  foreach my $_tr_line (@total_items) {';
3772
3773       while ( ( my $total_item_line = shift )
3774               !~ /^%%EndTotalDetails\s*$/                      ) {
3775         $total_item_line =~ s/'/\\'/g;    # nice LTS
3776         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3777         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3778         push @template, "    \$OUT .= '$total_item_line';";
3779       }
3780
3781       push @template, '}',
3782                       '--@]';
3783
3784     } else {
3785       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3786       push @template, $line;  
3787     }
3788   
3789   }
3790
3791   if ($DEBUG) {
3792     warn "$_\n" foreach @template;
3793   }
3794
3795   (@template);
3796 }
3797
3798 sub terms {
3799   my $self = shift;
3800   my $conf = $self->conf;
3801
3802   #check for an invoice-specific override
3803   return $self->invoice_terms if $self->invoice_terms;
3804   
3805   #check for a customer- specific override
3806   my $cust_main = $self->cust_main;
3807   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3808
3809   #use configured default
3810   $conf->config('invoice_default_terms') || '';
3811 }
3812
3813 sub due_date {
3814   my $self = shift;
3815   my $duedate = '';
3816   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3817     $duedate = $self->_date() + ( $1 * 86400 );
3818   }
3819   $duedate;
3820 }
3821
3822 sub due_date2str {
3823   my $self = shift;
3824   $self->due_date ? time2str(shift, $self->due_date) : '';
3825 }
3826
3827 sub balance_due_msg {
3828   my $self = shift;
3829   my $msg = $self->mt('Balance Due');
3830   return $msg unless $self->terms;
3831   if ( $self->due_date ) {
3832     $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3833       $self->due_date2str($date_format);
3834   } elsif ( $self->terms ) {
3835     $msg .= ' - '. $self->terms;
3836   }
3837   $msg;
3838 }
3839
3840 sub balance_due_date {
3841   my $self = shift;
3842   my $conf = $self->conf;
3843   my $duedate = '';
3844   if (    $conf->exists('invoice_default_terms') 
3845        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3846     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3847   }
3848   $duedate;
3849 }
3850
3851 sub credit_balance_msg { 
3852   my $self = shift;
3853   $self->mt('Credit Balance Remaining')
3854 }
3855
3856 =item invnum_date_pretty
3857
3858 Returns a string with the invoice number and date, for example:
3859 "Invoice #54 (3/20/2008)"
3860
3861 =cut
3862
3863 sub invnum_date_pretty {
3864   my $self = shift;
3865   $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3866 }
3867
3868 =item _date_pretty
3869
3870 Returns a string with the date, for example: "3/20/2008"
3871
3872 =cut
3873
3874 sub _date_pretty {
3875   my $self = shift;
3876   time2str($date_format, $self->_date);
3877 }
3878
3879 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3880
3881 Generate section information for all items appearing on this invoice.
3882 This will only be called for multi-section invoices.
3883
3884 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
3885 related display records (L<FS::cust_bill_pkg_display>) and organize 
3886 them into two groups ("early" and "late" according to whether they come 
3887 before or after the total), then into sections.  A subtotal is calculated 
3888 for each section.
3889
3890 Section descriptions are returned in sort weight order.  Each consists 
3891 of a hash containing:
3892
3893 description: the package category name, escaped
3894 subtotal: the total charges in that section
3895 tax_section: a flag indicating that the section contains only tax charges
3896 summarized: same as tax_section, for some reason
3897 sort_weight: the package category's sort weight
3898
3899 If 'condense' is set on the display record, it also contains everything 
3900 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3901 coderefs to generate parts of the invoice.  This is not advised.
3902
3903 Arguments:
3904
3905 LATE: an arrayref to push the "late" section hashes onto.  The "early"
3906 group is simply returned from the method.
3907
3908 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3909 Turning this on has the following effects:
3910 - Ignores display items with the 'summary' flag.
3911 - Combines all items into the "early" group.
3912 - Creates sections for all non-disabled package categories, even if they 
3913 have no charges on this invoice, as well as a section with no name.
3914
3915 ESCAPE: an escape function to use for section titles.
3916
3917 EXTRA_SECTIONS: an arrayref of additional sections to return after the 
3918 sorted list.  If there are any of these, section subtotals exclude 
3919 usage charges.
3920
3921 FORMAT: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
3922 passed through to C<_condense_section()>.
3923
3924 =cut
3925
3926 use vars qw(%pkg_category_cache);
3927 sub _items_sections {
3928   my $self = shift;
3929   my $late = shift;
3930   my $summarypage = shift;
3931   my $escape = shift;
3932   my $extra_sections = shift;
3933   my $format = shift;
3934
3935   my %subtotal = ();
3936   my %late_subtotal = ();
3937   my %not_tax = ();
3938
3939   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3940   {
3941
3942       my $usage = $cust_bill_pkg->usage;
3943
3944       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3945         next if ( $display->summary && $summarypage );
3946
3947         my $section = $display->section;
3948         my $type    = $display->type;
3949
3950         $not_tax{$section} = 1
3951           unless $cust_bill_pkg->pkgnum == 0;
3952
3953         if ( $display->post_total && !$summarypage ) {
3954           if (! $type || $type eq 'S') {
3955             $late_subtotal{$section} += $cust_bill_pkg->setup
3956               if $cust_bill_pkg->setup != 0
3957               || $cust_bill_pkg->setup_show_zero;
3958           }
3959
3960           if (! $type) {
3961             $late_subtotal{$section} += $cust_bill_pkg->recur
3962               if $cust_bill_pkg->recur != 0
3963               || $cust_bill_pkg->recur_show_zero;
3964           }
3965
3966           if ($type && $type eq 'R') {
3967             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3968               if $cust_bill_pkg->recur != 0
3969               || $cust_bill_pkg->recur_show_zero;
3970           }
3971           
3972           if ($type && $type eq 'U') {
3973             $late_subtotal{$section} += $usage
3974               unless scalar(@$extra_sections);
3975           }
3976
3977         } else {
3978
3979           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3980
3981           if (! $type || $type eq 'S') {
3982             $subtotal{$section} += $cust_bill_pkg->setup
3983               if $cust_bill_pkg->setup != 0
3984               || $cust_bill_pkg->setup_show_zero;
3985           }
3986
3987           if (! $type) {
3988             $subtotal{$section} += $cust_bill_pkg->recur
3989               if $cust_bill_pkg->recur != 0
3990               || $cust_bill_pkg->recur_show_zero;
3991           }
3992
3993           if ($type && $type eq 'R') {
3994             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3995               if $cust_bill_pkg->recur != 0
3996               || $cust_bill_pkg->recur_show_zero;
3997           }
3998           
3999           if ($type && $type eq 'U') {
4000             $subtotal{$section} += $usage
4001               unless scalar(@$extra_sections);
4002           }
4003
4004         }
4005
4006       }
4007
4008   }
4009
4010   %pkg_category_cache = ();
4011
4012   push @$late, map { { 'description' => &{$escape}($_),
4013                        'subtotal'    => $late_subtotal{$_},
4014                        'post_total'  => 1,
4015                        'sort_weight' => ( _pkg_category($_)
4016                                             ? _pkg_category($_)->weight
4017                                             : 0
4018                                        ),
4019                        ((_pkg_category($_) && _pkg_category($_)->condense)
4020                                            ? $self->_condense_section($format)
4021                                            : ()
4022                        ),
4023                    } }
4024                  sort _sectionsort keys %late_subtotal;
4025
4026   my @sections;
4027   if ( $summarypage ) {
4028     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
4029                 map { $_->categoryname } qsearch('pkg_category', {});
4030     push @sections, '' if exists($subtotal{''});
4031   } else {
4032     @sections = keys %subtotal;
4033   }
4034
4035   my @early = map { { 'description' => &{$escape}($_),
4036                       'subtotal'    => $subtotal{$_},
4037                       'summarized'  => $not_tax{$_} ? '' : 'Y',
4038                       'tax_section' => $not_tax{$_} ? '' : 'Y',
4039                       'sort_weight' => ( _pkg_category($_)
4040                                            ? _pkg_category($_)->weight
4041                                            : 0
4042                                        ),
4043                        ((_pkg_category($_) && _pkg_category($_)->condense)
4044                                            ? $self->_condense_section($format)
4045                                            : ()
4046                        ),
4047                     }
4048                   } @sections;
4049   push @early, @$extra_sections if $extra_sections;
4050
4051   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4052
4053 }
4054
4055 #helper subs for above
4056
4057 sub _sectionsort {
4058   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4059 }
4060
4061 sub _pkg_category {
4062   my $categoryname = shift;
4063   $pkg_category_cache{$categoryname} ||=
4064     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4065 }
4066
4067 my %condensed_format = (
4068   'label' => [ qw( Description Qty Amount ) ],
4069   'fields' => [
4070                 sub { shift->{description} },
4071                 sub { shift->{quantity} },
4072                 sub { my($href, %opt) = @_;
4073                       ($opt{dollar} || ''). $href->{amount};
4074                     },
4075               ],
4076   'align'  => [ qw( l r r ) ],
4077   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
4078   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
4079 );
4080
4081 sub _condense_section {
4082   my ( $self, $format ) = ( shift, shift );
4083   ( 'condensed' => 1,
4084     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4085       qw( description_generator
4086           header_generator
4087           total_generator
4088           total_line_generator
4089         )
4090   );
4091 }
4092
4093 sub _condensed_generator_defaults {
4094   my ( $self, $format ) = ( shift, shift );
4095   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4096 }
4097
4098 my %html_align = (
4099   'c' => 'center',
4100   'l' => 'left',
4101   'r' => 'right',
4102 );
4103
4104 sub _condensed_header_generator {
4105   my ( $self, $format ) = ( shift, shift );
4106
4107   my ( $f, $prefix, $suffix, $separator, $column ) =
4108     _condensed_generator_defaults($format);
4109
4110   if ($format eq 'latex') {
4111     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4112     $suffix = "\\\\\n\\hline";
4113     $separator = "&\n";
4114     $column =
4115       sub { my ($d,$a,$s,$w) = @_;
4116             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4117           };
4118   } elsif ( $format eq 'html' ) {
4119     $prefix = '<th></th>';
4120     $suffix = '';
4121     $separator = '';
4122     $column =
4123       sub { my ($d,$a,$s,$w) = @_;
4124             return qq!<th align="$html_align{$a}">$d</th>!;
4125       };
4126   }
4127
4128   sub {
4129     my @args = @_;
4130     my @result = ();
4131
4132     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4133       push @result,
4134         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4135     }
4136
4137     $prefix. join($separator, @result). $suffix;
4138   };
4139
4140 }
4141
4142 sub _condensed_description_generator {
4143   my ( $self, $format ) = ( shift, shift );
4144
4145   my ( $f, $prefix, $suffix, $separator, $column ) =
4146     _condensed_generator_defaults($format);
4147
4148   my $money_char = '$';
4149   if ($format eq 'latex') {
4150     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4151     $suffix = '\\\\';
4152     $separator = " & \n";
4153     $column =
4154       sub { my ($d,$a,$s,$w) = @_;
4155             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4156           };
4157     $money_char = '\\dollar';
4158   }elsif ( $format eq 'html' ) {
4159     $prefix = '"><td align="center"></td>';
4160     $suffix = '';
4161     $separator = '';
4162     $column =
4163       sub { my ($d,$a,$s,$w) = @_;
4164             return qq!<td align="$html_align{$a}">$d</td>!;
4165       };
4166     #$money_char = $conf->config('money_char') || '$';
4167     $money_char = '';  # this is madness
4168   }
4169
4170   sub {
4171     #my @args = @_;
4172     my $href = shift;
4173     my @result = ();
4174
4175     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4176       my $dollar = '';
4177       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4178       push @result,
4179         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4180                     map { $f->{$_}->[$i] } qw(align span width)
4181                   );
4182     }
4183
4184     $prefix. join( $separator, @result ). $suffix;
4185   };
4186
4187 }
4188
4189 sub _condensed_total_generator {
4190   my ( $self, $format ) = ( shift, shift );
4191
4192   my ( $f, $prefix, $suffix, $separator, $column ) =
4193     _condensed_generator_defaults($format);
4194   my $style = '';
4195
4196   if ($format eq 'latex') {
4197     $prefix = "& ";
4198     $suffix = "\\\\\n";
4199     $separator = " & \n";
4200     $column =
4201       sub { my ($d,$a,$s,$w) = @_;
4202             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4203           };
4204   }elsif ( $format eq 'html' ) {
4205     $prefix = '';
4206     $suffix = '';
4207     $separator = '';
4208     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4209     $column =
4210       sub { my ($d,$a,$s,$w) = @_;
4211             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4212       };
4213   }
4214
4215
4216   sub {
4217     my @args = @_;
4218     my @result = ();
4219
4220     #  my $r = &{$f->{fields}->[$i]}(@args);
4221     #  $r .= ' Total' unless $i;
4222
4223     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4224       push @result,
4225         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4226                     map { $f->{$_}->[$i] } qw(align span width)
4227                   );
4228     }
4229
4230     $prefix. join( $separator, @result ). $suffix;
4231   };
4232
4233 }
4234
4235 =item total_line_generator FORMAT
4236
4237 Returns a coderef used for generation of invoice total line items for this
4238 usage_class.  FORMAT is either html or latex
4239
4240 =cut
4241
4242 # should not be used: will have issues with hash element names (description vs
4243 # total_item and amount vs total_amount -- another array of functions?
4244
4245 sub _condensed_total_line_generator {
4246   my ( $self, $format ) = ( shift, shift );
4247
4248   my ( $f, $prefix, $suffix, $separator, $column ) =
4249     _condensed_generator_defaults($format);
4250   my $style = '';
4251
4252   if ($format eq 'latex') {
4253     $prefix = "& ";
4254     $suffix = "\\\\\n";
4255     $separator = " & \n";
4256     $column =
4257       sub { my ($d,$a,$s,$w) = @_;
4258             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4259           };
4260   }elsif ( $format eq 'html' ) {
4261     $prefix = '';
4262     $suffix = '';
4263     $separator = '';
4264     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4265     $column =
4266       sub { my ($d,$a,$s,$w) = @_;
4267             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4268       };
4269   }
4270
4271
4272   sub {
4273     my @args = @_;
4274     my @result = ();
4275
4276     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4277       push @result,
4278         &{$column}( &{$f->{fields}->[$i]}(@args),
4279                     map { $f->{$_}->[$i] } qw(align span width)
4280                   );
4281     }
4282
4283     $prefix. join( $separator, @result ). $suffix;
4284   };
4285
4286 }
4287
4288 #sub _items_extra_usage_sections {
4289 #  my $self = shift;
4290 #  my $escape = shift;
4291 #
4292 #  my %sections = ();
4293 #
4294 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
4295 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4296 #  {
4297 #    next unless $cust_bill_pkg->pkgnum > 0;
4298 #
4299 #    foreach my $section ( keys %usage_class ) {
4300 #
4301 #      my $usage = $cust_bill_pkg->usage($section);
4302 #
4303 #      next unless $usage && $usage > 0;
4304 #
4305 #      $sections{$section} ||= 0;
4306 #      $sections{$section} += $usage;
4307 #
4308 #    }
4309 #
4310 #  }
4311 #
4312 #  map { { 'description' => &{$escape}($_),
4313 #          'subtotal'    => $sections{$_},
4314 #          'summarized'  => '',
4315 #          'tax_section' => '',
4316 #        }
4317 #      }
4318 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4319 #
4320 #}
4321
4322 sub _items_extra_usage_sections {
4323   my $self = shift;
4324   my $conf = $self->conf;
4325   my $escape = shift;
4326   my $format = shift;
4327
4328   my %sections = ();
4329   my %classnums = ();
4330   my %lines = ();
4331
4332   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4333
4334   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4335   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4336     next unless $cust_bill_pkg->pkgnum > 0;
4337
4338     foreach my $classnum ( keys %usage_class ) {
4339       my $section = $usage_class{$classnum}->classname;
4340       $classnums{$section} = $classnum;
4341
4342       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4343         my $amount = $detail->amount;
4344         next unless $amount && $amount > 0;
4345  
4346         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4347         $sections{$section}{amount} += $amount;  #subtotal
4348         $sections{$section}{calls}++;
4349         $sections{$section}{duration} += $detail->duration;
4350
4351         my $desc = $detail->regionname; 
4352         my $description = $desc;
4353         $description = substr($desc, 0, $maxlength). '...'
4354           if $format eq 'latex' && length($desc) > $maxlength;
4355
4356         $lines{$section}{$desc} ||= {
4357           description     => &{$escape}($description),
4358           #pkgpart         => $part_pkg->pkgpart,
4359           pkgnum          => $cust_bill_pkg->pkgnum,
4360           ref             => '',
4361           amount          => 0,
4362           calls           => 0,
4363           duration        => 0,
4364           #unit_amount     => $cust_bill_pkg->unitrecur,
4365           quantity        => $cust_bill_pkg->quantity,
4366           product_code    => 'N/A',
4367           ext_description => [],
4368         };
4369
4370         $lines{$section}{$desc}{amount} += $amount;
4371         $lines{$section}{$desc}{calls}++;
4372         $lines{$section}{$desc}{duration} += $detail->duration;
4373
4374       }
4375     }
4376   }
4377
4378   my %sectionmap = ();
4379   foreach (keys %sections) {
4380     my $usage_class = $usage_class{$classnums{$_}};
4381     $sectionmap{$_} = { 'description' => &{$escape}($_),
4382                         'amount'    => $sections{$_}{amount},    #subtotal
4383                         'calls'       => $sections{$_}{calls},
4384                         'duration'    => $sections{$_}{duration},
4385                         'summarized'  => '',
4386                         'tax_section' => '',
4387                         'sort_weight' => $usage_class->weight,
4388                         ( $usage_class->format
4389                           ? ( map { $_ => $usage_class->$_($format) }
4390                               qw( description_generator header_generator total_generator total_line_generator )
4391                             )
4392                           : ()
4393                         ), 
4394                       };
4395   }
4396
4397   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4398                  values %sectionmap;
4399
4400   my @lines = ();
4401   foreach my $section ( keys %lines ) {
4402     foreach my $line ( keys %{$lines{$section}} ) {
4403       my $l = $lines{$section}{$line};
4404       $l->{section}     = $sectionmap{$section};
4405       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4406       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4407       push @lines, $l;
4408     }
4409   }
4410
4411   return(\@sections, \@lines);
4412
4413 }
4414
4415 sub _did_summary {
4416     my $self = shift;
4417     my $end = $self->_date;
4418
4419     # start at date of previous invoice + 1 second or 0 if no previous invoice
4420     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4421     $start = 0 if !$start;
4422     $start++;
4423
4424     my $cust_main = $self->cust_main;
4425     my @pkgs = $cust_main->all_pkgs;
4426     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4427         = (0,0,0,0,0);
4428     my @seen = ();
4429     foreach my $pkg ( @pkgs ) {
4430         my @h_cust_svc = $pkg->h_cust_svc($end);
4431         foreach my $h_cust_svc ( @h_cust_svc ) {
4432             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4433             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4434
4435             my $inserted = $h_cust_svc->date_inserted;
4436             my $deleted = $h_cust_svc->date_deleted;
4437             my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4438             my $phone_deleted;
4439             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
4440             
4441 # DID either activated or ported in; cannot be both for same DID simultaneously
4442             if ($inserted >= $start && $inserted <= $end && $phone_inserted
4443                 && (!$phone_inserted->lnp_status 
4444                     || $phone_inserted->lnp_status eq ''
4445                     || $phone_inserted->lnp_status eq 'native')) {
4446                 $num_activated++;
4447             }
4448             else { # this one not so clean, should probably move to (h_)svc_phone
4449                  my $phone_portedin = qsearchs( 'h_svc_phone',
4450                       { 'svcnum' => $h_cust_svc->svcnum, 
4451                         'lnp_status' => 'portedin' },  
4452                       FS::h_svc_phone->sql_h_searchs($end),  
4453                     );
4454                  $num_portedin++ if $phone_portedin;
4455             }
4456
4457 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4458             if($deleted >= $start && $deleted <= $end && $phone_deleted
4459                 && (!$phone_deleted->lnp_status 
4460                     || $phone_deleted->lnp_status ne 'portingout')) {
4461                 $num_deactivated++;
4462             } 
4463             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
4464                 && $phone_deleted->lnp_status 
4465                 && $phone_deleted->lnp_status eq 'portingout') {
4466                 $num_portedout++;
4467             }
4468
4469             # increment usage minutes
4470         if ( $phone_inserted ) {
4471             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4472             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4473         }
4474         else {
4475             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4476         }
4477
4478             # don't look at this service again
4479             push @seen, $h_cust_svc->svcnum;
4480         }
4481     }
4482
4483     $minutes = sprintf("%d", $minutes);
4484     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
4485         . "$num_deactivated  Ported-Out: $num_portedout ",
4486             "Total Minutes: $minutes");
4487 }
4488
4489 sub _items_accountcode_cdr {
4490     my $self = shift;
4491     my $escape = shift;
4492     my $format = shift;
4493
4494     my $section = { 'amount'        => 0,
4495                     'calls'         => 0,
4496                     'duration'      => 0,
4497                     'sort_weight'   => '',
4498                     'phonenum'      => '',
4499                     'description'   => 'Usage by Account Code',
4500                     'post_total'    => '',
4501                     'summarized'    => '',
4502                     'header'        => '',
4503                   };
4504     my @lines;
4505     my %accountcodes = ();
4506
4507     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4508         next unless $cust_bill_pkg->pkgnum > 0;
4509
4510         my @header = $cust_bill_pkg->details_header;
4511         next unless scalar(@header);
4512         $section->{'header'} = join(',',@header);
4513
4514         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4515
4516             $section->{'header'} = $detail->formatted('format' => $format)
4517                 if($detail->detail eq $section->{'header'}); 
4518       
4519             my $accountcode = $detail->accountcode;
4520             next unless $accountcode;
4521
4522             my $amount = $detail->amount;
4523             next unless $amount && $amount > 0;
4524
4525             $accountcodes{$accountcode} ||= {
4526                     description => $accountcode,
4527                     pkgnum      => '',
4528                     ref         => '',
4529                     amount      => 0,
4530                     calls       => 0,
4531                     duration    => 0,
4532                     quantity    => '',
4533                     product_code => 'N/A',
4534                     section     => $section,
4535                     ext_description => [ $section->{'header'} ],
4536                     detail_temp => [],
4537             };
4538
4539             $section->{'amount'} += $amount;
4540             $accountcodes{$accountcode}{'amount'} += $amount;
4541             $accountcodes{$accountcode}{calls}++;
4542             $accountcodes{$accountcode}{duration} += $detail->duration;
4543             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4544         }
4545     }
4546
4547     foreach my $l ( values %accountcodes ) {
4548         $l->{amount} = sprintf( "%.2f", $l->{amount} );
4549         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4550         foreach my $sorted_detail ( @sorted_detail ) {
4551             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4552         }
4553         delete $l->{detail_temp};
4554         push @lines, $l;
4555     }
4556
4557     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4558
4559     return ($section,\@sorted_lines);
4560 }
4561
4562 sub _items_svc_phone_sections {
4563   my $self = shift;
4564   my $conf = $self->conf;
4565   my $escape = shift;
4566   my $format = shift;
4567
4568   my %sections = ();
4569   my %classnums = ();
4570   my %lines = ();
4571
4572   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4573
4574   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4575   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4576
4577   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4578     next unless $cust_bill_pkg->pkgnum > 0;
4579
4580     my @header = $cust_bill_pkg->details_header;
4581     next unless scalar(@header);
4582
4583     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4584
4585       my $phonenum = $detail->phonenum;
4586       next unless $phonenum;
4587
4588       my $amount = $detail->amount;
4589       next unless $amount && $amount > 0;
4590
4591       $sections{$phonenum} ||= { 'amount'      => 0,
4592                                  'calls'       => 0,
4593                                  'duration'    => 0,
4594                                  'sort_weight' => -1,
4595                                  'phonenum'    => $phonenum,
4596                                 };
4597       $sections{$phonenum}{amount} += $amount;  #subtotal
4598       $sections{$phonenum}{calls}++;
4599       $sections{$phonenum}{duration} += $detail->duration;
4600
4601       my $desc = $detail->regionname; 
4602       my $description = $desc;
4603       $description = substr($desc, 0, $maxlength). '...'
4604         if $format eq 'latex' && length($desc) > $maxlength;
4605
4606       $lines{$phonenum}{$desc} ||= {
4607         description     => &{$escape}($description),
4608         #pkgpart         => $part_pkg->pkgpart,
4609         pkgnum          => '',
4610         ref             => '',
4611         amount          => 0,
4612         calls           => 0,
4613         duration        => 0,
4614         #unit_amount     => '',
4615         quantity        => '',
4616         product_code    => 'N/A',
4617         ext_description => [],
4618       };
4619
4620       $lines{$phonenum}{$desc}{amount} += $amount;
4621       $lines{$phonenum}{$desc}{calls}++;
4622       $lines{$phonenum}{$desc}{duration} += $detail->duration;
4623
4624       my $line = $usage_class{$detail->classnum}->classname;
4625       $sections{"$phonenum $line"} ||=
4626         { 'amount' => 0,
4627           'calls' => 0,
4628           'duration' => 0,
4629           'sort_weight' => $usage_class{$detail->classnum}->weight,
4630           'phonenum' => $phonenum,
4631           'header'  => [ @header ],
4632         };
4633       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
4634       $sections{"$phonenum $line"}{calls}++;
4635       $sections{"$phonenum $line"}{duration} += $detail->duration;
4636
4637       $lines{"$phonenum $line"}{$desc} ||= {
4638         description     => &{$escape}($description),
4639         #pkgpart         => $part_pkg->pkgpart,
4640         pkgnum          => '',
4641         ref             => '',
4642         amount          => 0,
4643         calls           => 0,
4644         duration        => 0,
4645         #unit_amount     => '',
4646         quantity        => '',
4647         product_code    => 'N/A',
4648         ext_description => [],
4649       };
4650
4651       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4652       $lines{"$phonenum $line"}{$desc}{calls}++;
4653       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4654       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4655            $detail->formatted('format' => $format);
4656
4657     }
4658   }
4659
4660   my %sectionmap = ();
4661   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4662   foreach ( keys %sections ) {
4663     my @header = @{ $sections{$_}{header} || [] };
4664     my $usage_simple =
4665       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4666     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4667     my $usage_class = $summary ? $simple : $usage_simple;
4668     my $ending = $summary ? ' usage charges' : '';
4669     my %gen_opt = ();
4670     unless ($summary) {
4671       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4672     }
4673     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4674                         'amount'    => $sections{$_}{amount},    #subtotal
4675                         'calls'       => $sections{$_}{calls},
4676                         'duration'    => $sections{$_}{duration},
4677                         'summarized'  => '',
4678                         'tax_section' => '',
4679                         'phonenum'    => $sections{$_}{phonenum},
4680                         'sort_weight' => $sections{$_}{sort_weight},
4681                         'post_total'  => $summary, #inspire pagebreak
4682                         (
4683                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4684                             qw( description_generator
4685                                 header_generator
4686                                 total_generator
4687                                 total_line_generator
4688                               )
4689                           )
4690                         ), 
4691                       };
4692   }
4693
4694   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4695                         $a->{sort_weight} <=> $b->{sort_weight}
4696                       }
4697                  values %sectionmap;
4698
4699   my @lines = ();
4700   foreach my $section ( keys %lines ) {
4701     foreach my $line ( keys %{$lines{$section}} ) {
4702       my $l = $lines{$section}{$line};
4703       $l->{section}     = $sectionmap{$section};
4704       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4705       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4706       push @lines, $l;
4707     }
4708   }
4709   
4710   if($conf->exists('phone_usage_class_summary')) { 
4711       # this only works with Latex
4712       my @newlines;
4713       my @newsections;
4714
4715       # after this, we'll have only two sections per DID:
4716       # Calls Summary and Calls Detail
4717       foreach my $section ( @sections ) {
4718         if($section->{'post_total'}) {
4719             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4720             $section->{'total_line_generator'} = sub { '' };
4721             $section->{'total_generator'} = sub { '' };
4722             $section->{'header_generator'} = sub { '' };
4723             $section->{'description_generator'} = '';
4724             push @newsections, $section;
4725             my %calls_detail = %$section;
4726             $calls_detail{'post_total'} = '';
4727             $calls_detail{'sort_weight'} = '';
4728             $calls_detail{'description_generator'} = sub { '' };
4729             $calls_detail{'header_generator'} = sub {
4730                 return ' & Date/Time & Called Number & Duration & Price'
4731                     if $format eq 'latex';
4732                 '';
4733             };
4734             $calls_detail{'description'} = 'Calls Detail: '
4735                                                     . $section->{'phonenum'};
4736             push @newsections, \%calls_detail;  
4737         }
4738       }
4739
4740       # after this, each usage class is collapsed/summarized into a single
4741       # line under the Calls Summary section
4742       foreach my $newsection ( @newsections ) {
4743         if($newsection->{'post_total'}) { # this means Calls Summary
4744             foreach my $section ( @sections ) {
4745                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
4746                                 && !$section->{'post_total'});
4747                 my $newdesc = $section->{'description'};
4748                 my $tn = $section->{'phonenum'};
4749                 $newdesc =~ s/$tn//g;
4750                 my $line = {  ext_description => [],
4751                               pkgnum => '',
4752                               ref => '',
4753                               quantity => '',
4754                               calls => $section->{'calls'},
4755                               section => $newsection,
4756                               duration => $section->{'duration'},
4757                               description => $newdesc,
4758                               amount => sprintf("%.2f",$section->{'amount'}),
4759                               product_code => 'N/A',
4760                             };
4761                 push @newlines, $line;
4762             }
4763         }
4764       }
4765
4766       # after this, Calls Details is populated with all CDRs
4767       foreach my $newsection ( @newsections ) {
4768         if(!$newsection->{'post_total'}) { # this means Calls Details
4769             foreach my $line ( @lines ) {
4770                 next unless (scalar(@{$line->{'ext_description'}}) &&
4771                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4772                             );
4773                 my @extdesc = @{$line->{'ext_description'}};
4774                 my @newextdesc;
4775                 foreach my $extdesc ( @extdesc ) {
4776                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4777                     push @newextdesc, $extdesc;
4778                 }
4779                 $line->{'ext_description'} = \@newextdesc;
4780                 $line->{'section'} = $newsection;
4781                 push @newlines, $line;
4782             }
4783         }
4784       }
4785
4786       return(\@newsections, \@newlines);
4787   }
4788
4789   return(\@sections, \@lines);
4790
4791 }
4792
4793 sub _items { # seems to be unused
4794   my $self = shift;
4795
4796   #my @display = scalar(@_)
4797   #              ? @_
4798   #              : qw( _items_previous _items_pkg );
4799   #              #: qw( _items_pkg );
4800   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4801   my @display = qw( _items_previous _items_pkg );
4802
4803   my @b = ();
4804   foreach my $display ( @display ) {
4805     push @b, $self->$display(@_);
4806   }
4807   @b;
4808 }
4809
4810 sub _items_previous {
4811   my $self = shift;
4812   my $conf = $self->conf;
4813   my $cust_main = $self->cust_main;
4814   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4815   my @b = ();
4816   foreach ( @pr_cust_bill ) {
4817     my $date = $conf->exists('invoice_show_prior_due_date')
4818                ? 'due '. $_->due_date2str($date_format)
4819                : time2str($date_format, $_->_date);
4820     push @b, {
4821       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4822       #'pkgpart'     => 'N/A',
4823       'pkgnum'      => 'N/A',
4824       'amount'      => sprintf("%.2f", $_->owed),
4825     };
4826   }
4827   @b;
4828
4829   #{
4830   #    'description'     => 'Previous Balance',
4831   #    #'pkgpart'         => 'N/A',
4832   #    'pkgnum'          => 'N/A',
4833   #    'amount'          => sprintf("%10.2f", $pr_total ),
4834   #    'ext_description' => [ map {
4835   #                                 "Invoice ". $_->invnum.
4836   #                                 " (". time2str("%x",$_->_date). ") ".
4837   #                                 sprintf("%10.2f", $_->owed)
4838   #                         } @pr_cust_bill ],
4839
4840   #};
4841 }
4842
4843 =item _items_pkg [ OPTIONS ]
4844
4845 Return line item hashes for each package item on this invoice. Nearly 
4846 equivalent to 
4847
4848 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4849
4850 The only OPTIONS accepted is 'section', which may point to a hashref 
4851 with a key named 'condensed', which may have a true value.  If it 
4852 does, this method tries to merge identical items into items with 
4853 'quantity' equal to the number of items (not the sum of their 
4854 separate quantities, for some reason).
4855
4856 =cut
4857
4858 sub _items_pkg {
4859   my $self = shift;
4860   my %options = @_;
4861
4862   warn "$me _items_pkg searching for all package line items\n"
4863     if $DEBUG > 1;
4864
4865   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4866
4867   warn "$me _items_pkg filtering line items\n"
4868     if $DEBUG > 1;
4869   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4870
4871   if ($options{section} && $options{section}->{condensed}) {
4872
4873     warn "$me _items_pkg condensing section\n"
4874       if $DEBUG > 1;
4875
4876     my %itemshash = ();
4877     local $Storable::canonical = 1;
4878     foreach ( @items ) {
4879       my $item = { %$_ };
4880       delete $item->{ref};
4881       delete $item->{ext_description};
4882       my $key = freeze($item);
4883       $itemshash{$key} ||= 0;
4884       $itemshash{$key} ++; # += $item->{quantity};
4885     }
4886     @items = sort { $a->{description} cmp $b->{description} }
4887              map { my $i = thaw($_);
4888                    $i->{quantity} = $itemshash{$_};
4889                    $i->{amount} =
4890                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4891                    $i;
4892                  }
4893              keys %itemshash;
4894   }
4895
4896   warn "$me _items_pkg returning ". scalar(@items). " items\n"
4897     if $DEBUG > 1;
4898
4899   @items;
4900 }
4901
4902 sub _taxsort {
4903   return 0 unless $a->itemdesc cmp $b->itemdesc;
4904   return -1 if $b->itemdesc eq 'Tax';
4905   return 1 if $a->itemdesc eq 'Tax';
4906   return -1 if $b->itemdesc eq 'Other surcharges';
4907   return 1 if $a->itemdesc eq 'Other surcharges';
4908   $a->itemdesc cmp $b->itemdesc;
4909 }
4910
4911 sub _items_tax {
4912   my $self = shift;
4913   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4914   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4915 }
4916
4917 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4918
4919 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4920 list of hashrefs describing the line items they generate on the invoice.
4921
4922 OPTIONS may include:
4923
4924 format: the invoice format.
4925
4926 escape_function: the function used to escape strings.
4927
4928 DEPRECATED? (expensive, mostly unused?)
4929 format_function: the function used to format CDRs.
4930
4931 section: a hashref containing 'description'; if this is present, 
4932 cust_bill_pkg_display records not belonging to this section are 
4933 ignored.
4934
4935 multisection: a flag indicating that this is a multisection invoice,
4936 which does something complicated.
4937
4938 multilocation: a flag to display the location label for the package.
4939
4940 Returns a list of hashrefs, each of which may contain:
4941
4942 pkgnum, description, amount, unit_amount, quantity, _is_setup, and 
4943 ext_description, which is an arrayref of detail lines to show below 
4944 the package line.
4945
4946 =cut
4947
4948 sub _items_cust_bill_pkg {
4949   my $self = shift;
4950   my $conf = $self->conf;
4951   my $cust_bill_pkgs = shift;
4952   my %opt = @_;
4953
4954   my $format = $opt{format} || '';
4955   my $escape_function = $opt{escape_function} || sub { shift };
4956   my $format_function = $opt{format_function} || '';
4957   my $no_usage = $opt{no_usage} || '';
4958   my $unsquelched = $opt{unsquelched} || ''; #unused
4959   my $section = $opt{section}->{description} if $opt{section};
4960   my $summary_page = $opt{summary_page} || ''; #unused
4961   my $multilocation = $opt{multilocation} || '';
4962   my $multisection = $opt{multisection} || '';
4963   my $discount_show_always = 0;
4964
4965   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4966
4967   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4968
4969   my @b = ();
4970   my ($s, $r, $u) = ( undef, undef, undef );
4971   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4972   {
4973
4974     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4975       if ( $_ && !$cust_bill_pkg->hidden ) {
4976         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4977         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4978         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4979         push @b, { %$_ }
4980           if $_->{amount} != 0
4981           || $discount_show_always
4982           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4983           || (   $_->{_is_setup} && $_->{setup_show_zero} )
4984         ;
4985         $_ = undef;
4986       }
4987     }
4988
4989     my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4990
4991     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4992          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4993       if $DEBUG > 1;
4994
4995     foreach my $display ( grep { defined($section)
4996                                  ? $_->section eq $section
4997                                  : 1
4998                                }
4999                           #grep { !$_->summary || !$summary_page } # bunk!
5000                           grep { !$_->summary || $multisection }
5001                           @cust_bill_pkg_display
5002                         )
5003     {
5004
5005       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
5006            $display->billpkgdisplaynum. "\n"
5007         if $DEBUG > 1;
5008
5009       my $type = $display->type;
5010
5011       my $desc = $cust_bill_pkg->desc;
5012       $desc = substr($desc, 0, $maxlength). '...'
5013         if $format eq 'latex' && length($desc) > $maxlength;
5014
5015       my %details_opt = ( 'format'          => $format,
5016                           'escape_function' => $escape_function,
5017                           'format_function' => $format_function,
5018                           'no_usage'        => $opt{'no_usage'},
5019                         );
5020
5021       if ( $cust_bill_pkg->pkgnum > 0 ) {
5022
5023         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
5024           if $DEBUG > 1;
5025  
5026         my $cust_pkg = $cust_bill_pkg->cust_pkg;
5027
5028         # which pkgpart to show for display purposes?
5029         my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
5030
5031         # start/end dates for invoice formats that do nonstandard 
5032         # things with them
5033         my %item_dates = ();
5034         %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
5035           unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
5036
5037         if (    (!$type || $type eq 'S')
5038              && (    $cust_bill_pkg->setup != 0
5039                   || $cust_bill_pkg->setup_show_zero
5040                 )
5041            )
5042          {
5043
5044           warn "$me _items_cust_bill_pkg adding setup\n"
5045             if $DEBUG > 1;
5046
5047           my $description = $desc;
5048           $description .= ' Setup'
5049             if $cust_bill_pkg->recur != 0
5050             || $discount_show_always
5051             || $cust_bill_pkg->recur_show_zero;
5052
5053           my @d = ();
5054           my $svc_label;
5055           unless ( $cust_pkg->part_pkg->hide_svc_detail
5056                 || $cust_bill_pkg->hidden )
5057           {
5058
5059             my @svc_labels = map &{$escape_function}($_),
5060                         $cust_pkg->h_labels_short($self->_date, undef, 'I');
5061             push @d, @svc_labels
5062               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5063             $svc_label = $svc_labels[0];
5064
5065             if ( $multilocation ) {
5066               my $loc = $cust_pkg->location_label;
5067               $loc = substr($loc, 0, $maxlength). '...'
5068                 if $format eq 'latex' && length($loc) > $maxlength;
5069               push @d, &{$escape_function}($loc);
5070             }
5071
5072           } #unless hiding service details
5073
5074           push @d, $cust_bill_pkg->details(%details_opt)
5075             if $cust_bill_pkg->recur == 0;
5076
5077           if ( $cust_bill_pkg->hidden ) {
5078             $s->{amount}      += $cust_bill_pkg->setup;
5079             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5080             push @{ $s->{ext_description} }, @d;
5081           } else {
5082             $s = {
5083               _is_setup       => 1,
5084               description     => $description,
5085               pkgpart         => $pkgpart,
5086               pkgnum          => $cust_bill_pkg->pkgnum,
5087               amount          => $cust_bill_pkg->setup,
5088               setup_show_zero => $cust_bill_pkg->setup_show_zero,
5089               unit_amount     => $cust_bill_pkg->unitsetup,
5090               quantity        => $cust_bill_pkg->quantity,
5091               ext_description => \@d,
5092               svc_label       => ($svc_label || ''),
5093             };
5094           };
5095
5096         }
5097
5098         if (    ( !$type || $type eq 'R' || $type eq 'U' )
5099              && (
5100                      $cust_bill_pkg->recur != 0
5101                   || $cust_bill_pkg->setup == 0
5102                   || $discount_show_always
5103                   || $cust_bill_pkg->recur_show_zero
5104                 )
5105            )
5106         {
5107
5108           warn "$me _items_cust_bill_pkg adding recur/usage\n"
5109             if $DEBUG > 1;
5110
5111           my $is_summary = $display->summary;
5112           my $description = ($is_summary && $type && $type eq 'U')
5113                             ? "Usage charges" : $desc;
5114
5115           my $part_pkg = $cust_pkg->part_pkg;
5116
5117           #pry be a bit more efficient to look some of this conf stuff up
5118           # outside the loop
5119           unless (
5120             $conf->exists('disable_line_item_date_ranges')
5121               || $part_pkg->option('disable_line_item_date_ranges',1)
5122               || ! $cust_bill_pkg->sdate
5123               || ! $cust_bill_pkg->edate
5124           ) {
5125             my $time_period;
5126             my $date_style = '';                                               
5127             $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monthly',
5128                                          $cust_main->agentnum                  
5129                                        )                                       
5130               if $part_pkg && $part_pkg->freq !~ /^1m?$/;                      
5131             $date_style ||= $conf->config( 'cust_bill-line_item-date_style',   
5132                                             $cust_main->agentnum
5133                                           );
5134             if ( defined($date_style) && $date_style eq 'month_of' ) {
5135               $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5136             } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5137               my $desc = $conf->config( 'cust_bill-line_item-date_description',
5138                                          $cust_main->agentnum
5139                                       );
5140               $desc .= ' ' unless $desc =~ /\s$/;
5141               $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5142             } else {
5143               $time_period =      time2str($date_format, $cust_bill_pkg->sdate).
5144                            " - ". time2str($date_format, $cust_bill_pkg->edate);
5145             }
5146             $description .= " ($time_period)";
5147           }
5148
5149           my @d = ();
5150           my @seconds = (); # for display of usage info
5151           my $svc_label = '';
5152
5153           #at least until cust_bill_pkg has "past" ranges in addition to
5154           #the "future" sdate/edate ones... see #3032
5155           my @dates = ( $self->_date );
5156           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5157           push @dates, $prev->sdate if $prev;
5158           push @dates, undef if !$prev;
5159
5160           unless ( $cust_pkg->part_pkg->hide_svc_detail
5161                 || $cust_bill_pkg->itemdesc
5162                 || $cust_bill_pkg->hidden
5163                 || $is_summary && $type && $type eq 'U' )
5164           {
5165
5166             warn "$me _items_cust_bill_pkg adding service details\n"
5167               if $DEBUG > 1;
5168
5169             my @svc_labels = map &{$escape_function}($_),
5170                         $cust_pkg->h_labels_short(@dates, 'I');
5171             push @d, @svc_labels
5172               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5173             $svc_label = $svc_labels[0];
5174
5175             warn "$me _items_cust_bill_pkg done adding service details\n"
5176               if $DEBUG > 1;
5177
5178             if ( $multilocation ) {
5179               my $loc = $cust_pkg->location_label;
5180               $loc = substr($loc, 0, $maxlength). '...'
5181                 if $format eq 'latex' && length($loc) > $maxlength;
5182               push @d, &{$escape_function}($loc);
5183             }
5184
5185             # Display of seconds_since_sqlradacct:
5186             # On the invoice, when processing @detail_items, look for a field
5187             # named 'seconds'.  This will contain total seconds for each 
5188             # service, in the same order as @ext_description.  For services 
5189             # that don't support this it will show undef.
5190             if ( $conf->exists('svc_acct-usage_seconds') 
5191                  and ! $cust_bill_pkg->pkgpart_override ) {
5192               foreach my $cust_svc ( 
5193                   $cust_pkg->h_cust_svc(@dates, 'I') 
5194                 ) {
5195
5196                 # eval because not having any part_export_usage exports 
5197                 # is a fatal error, last_bill/_date because that's how 
5198                 # sqlradius_hour billing does it
5199                 my $sec = eval {
5200                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5201                 };
5202                 push @seconds, $sec;
5203               }
5204             } #if svc_acct-usage_seconds
5205
5206           }
5207
5208           unless ( $is_summary ) {
5209             warn "$me _items_cust_bill_pkg adding details\n"
5210               if $DEBUG > 1;
5211
5212             #instead of omitting details entirely in this case (unwanted side
5213             # effects), just omit CDRs
5214             $details_opt{'no_usage'} = 1
5215               if $type && $type eq 'R';
5216
5217             push @d, $cust_bill_pkg->details(%details_opt);
5218           }
5219
5220           warn "$me _items_cust_bill_pkg calculating amount\n"
5221             if $DEBUG > 1;
5222   
5223           my $amount = 0;
5224           if (!$type) {
5225             $amount = $cust_bill_pkg->recur;
5226           } elsif ($type eq 'R') {
5227             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5228           } elsif ($type eq 'U') {
5229             $amount = $cust_bill_pkg->usage;
5230           }
5231   
5232           if ( !$type || $type eq 'R' ) {
5233
5234             warn "$me _items_cust_bill_pkg adding recur\n"
5235               if $DEBUG > 1;
5236
5237             if ( $cust_bill_pkg->hidden ) {
5238               $r->{amount}      += $amount;
5239               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5240               push @{ $r->{ext_description} }, @d;
5241             } else {
5242               $r = {
5243                 description     => $description,
5244                 pkgpart         => $pkgpart,
5245                 pkgnum          => $cust_bill_pkg->pkgnum,
5246                 amount          => $amount,
5247                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5248                 unit_amount     => $cust_bill_pkg->unitrecur,
5249                 quantity        => $cust_bill_pkg->quantity,
5250                 %item_dates,
5251                 ext_description => \@d,
5252                 svc_label       => ($svc_label || ''),
5253               };
5254               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5255             }
5256
5257           } else {  # $type eq 'U'
5258
5259             warn "$me _items_cust_bill_pkg adding usage\n"
5260               if $DEBUG > 1;
5261
5262             if ( $cust_bill_pkg->hidden ) {
5263               $u->{amount}      += $amount;
5264               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5265               push @{ $u->{ext_description} }, @d;
5266             } else {
5267               $u = {
5268                 description     => $description,
5269                 pkgpart         => $pkgpart,
5270                 pkgnum          => $cust_bill_pkg->pkgnum,
5271                 amount          => $amount,
5272                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5273                 unit_amount     => $cust_bill_pkg->unitrecur,
5274                 quantity        => $cust_bill_pkg->quantity,
5275                 %item_dates,
5276                 ext_description => \@d,
5277               };
5278             }
5279           }
5280
5281         } # recurring or usage with recurring charge
5282
5283       } else { #pkgnum tax or one-shot line item (??)
5284
5285         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5286           if $DEBUG > 1;
5287
5288         if ( $cust_bill_pkg->setup != 0 ) {
5289           push @b, {
5290             'description' => $desc,
5291             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
5292           };
5293         }
5294         if ( $cust_bill_pkg->recur != 0 ) {
5295           push @b, {
5296             'description' => "$desc (".
5297                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5298                              time2str($date_format, $cust_bill_pkg->edate). ')',
5299             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
5300           };
5301         }
5302
5303       }
5304
5305     }
5306
5307     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5308                                 && $conf->exists('discount-show-always'));
5309
5310   }
5311
5312   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5313     if ( $_  ) {
5314       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
5315       $_->{amount}      =~ s/^\-0\.00$/0.00/;
5316       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5317       push @b, { %$_ }
5318         if $_->{amount} != 0
5319         || $discount_show_always
5320         || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5321         || (   $_->{_is_setup} && $_->{setup_show_zero} )
5322     }
5323   }
5324
5325   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5326     if $DEBUG > 1;
5327
5328   @b;
5329
5330 }
5331
5332 sub _items_credits {
5333   my( $self, %opt ) = @_;
5334   my $trim_len = $opt{'trim_len'} || 60;
5335
5336   my @b;
5337   #credits
5338   my @objects;
5339   if ( $self->conf->exists('previous_balance-payments_since') ) {
5340     my $date = 0;
5341     $date = $self->previous_bill->_date if $self->previous_bill;
5342     @objects = qsearch('cust_credit', {
5343         'custnum' => $self->custnum,
5344         '_date'   => {op => '>=', value => $date},
5345       });
5346       # hard to do this in the qsearch...
5347     @objects = grep { $_->_date < $self->_date } @objects;
5348   } else {
5349     @objects = $self->cust_credited;
5350   }
5351
5352   foreach my $obj ( @objects ) {
5353     my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
5354
5355     my $reason = substr($cust_credit->reason, 0, $trim_len);
5356     $reason .= '...' if length($reason) < length($cust_credit->reason);
5357     $reason = " ($reason) " if $reason;
5358
5359     push @b, {
5360       #'description' => 'Credit ref\#'. $_->crednum.
5361       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
5362       #                 $reason,
5363       'description' => $self->mt('Credit applied').' '.
5364                        time2str($date_format,$obj->_date). $reason,
5365       'amount'      => sprintf("%.2f",$obj->amount),
5366     };
5367   }
5368
5369   @b;
5370
5371 }
5372
5373 sub _items_payments {
5374   my $self = shift;
5375
5376   my @b;
5377   my $detailed = $self->conf->exists('invoice_payment_details');
5378   my @objects;
5379   if ( $self->conf->exists('previous_balance-payments_since') ) {
5380     my $date = 0;
5381     $date = $self->previous_bill->_date if $self->previous_bill;
5382     @objects = qsearch('cust_pay', {
5383         'custnum' => $self->custnum,
5384         '_date'   => {op => '>=', value => $date},
5385       });
5386     @objects = grep { $_->_date < $self->_date } @objects;
5387   } else {
5388     @objects = $self->cust_bill_pay;
5389   }
5390
5391   foreach my $obj (@objects) {
5392     my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
5393     my $desc = $self->mt('Payment received').' '.
5394                time2str($date_format, $cust_pay->_date );
5395     $desc .= $self->mt(' via ' . $cust_pay->payby_payinfo_pretty)
5396       if $detailed;
5397
5398     push @b, {
5399       'description' => $desc,
5400       'amount'      => sprintf("%.2f", $obj->amount )
5401     };
5402   }
5403
5404   @b;
5405
5406 }
5407
5408 =item _items_discounts_avail
5409
5410 Returns an array of line item hashrefs representing available term discounts
5411 for this invoice.  This makes the same assumptions that apply to term 
5412 discounts in general: that the package is billed monthly, at a flat rate, 
5413 with no usage charges.  A prorated first month will be handled, as will 
5414 a setup fee if the discount is allowed to apply to setup fees.
5415
5416 =cut
5417
5418 sub _items_discounts_avail {
5419   my $self = shift;
5420   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5421
5422   my %plans = $self->discount_plans;
5423
5424   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5425
5426   map {
5427     my $months = $_;
5428     my $plan = $plans{$months};
5429
5430     my $term_total = sprintf('%.2f', $plan->discounted_total);
5431     my $percent = sprintf('%.0f', 
5432                           100 * (1 - $term_total / $plan->base_total) );
5433     my $permonth = sprintf('%.2f', $term_total / $months);
5434     my $detail = $self->mt('discount on item'). ' '.
5435                  join(', ', map { "#$_" } $plan->pkgnums)
5436       if $list_pkgnums;
5437
5438     # discounts for non-integer months don't work anyway
5439     $months = sprintf("%d", $months);
5440
5441     +{
5442       description => $self->mt('Save [_1]% by paying for [_2] months',
5443                                 $percent, $months),
5444       amount      => $self->mt('[_1] ([_2] per month)', 
5445                                 $term_total, $money_char.$permonth),
5446       ext_description => ($detail || ''),
5447     }
5448   } #map
5449   sort { $b <=> $a } keys %plans;
5450
5451 }
5452
5453 =item call_details [ OPTION => VALUE ... ]
5454
5455 Returns an array of CSV strings representing the call details for this invoice
5456 The only option available is the boolean prepend_billed_number
5457
5458 =cut
5459
5460 sub call_details {
5461   my ($self, %opt) = @_;
5462
5463   my $format_function = sub { shift };
5464
5465   if ($opt{prepend_billed_number}) {
5466     $format_function = sub {
5467       my $detail = shift;
5468       my $row = shift;
5469
5470       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5471       
5472     };
5473   }
5474
5475   my @details = map { $_->details( 'format_function' => $format_function,
5476                                    'escape_function' => sub{ return() },
5477                                  )
5478                     }
5479                   grep { $_->pkgnum }
5480                   $self->cust_bill_pkg;
5481   my $header = $details[0];
5482   ( $header, grep { $_ ne $header } @details );
5483 }
5484
5485
5486 =back
5487
5488 =head1 SUBROUTINES
5489
5490 =over 4
5491
5492 =item process_reprint
5493
5494 =cut
5495
5496 sub process_reprint {
5497   process_re_X('print', @_);
5498 }
5499
5500 =item process_reemail
5501
5502 =cut
5503
5504 sub process_reemail {
5505   process_re_X('email', @_);
5506 }
5507
5508 =item process_refax
5509
5510 =cut
5511
5512 sub process_refax {
5513   process_re_X('fax', @_);
5514 }
5515
5516 =item process_reftp
5517
5518 =cut
5519
5520 sub process_reftp {
5521   process_re_X('ftp', @_);
5522 }
5523
5524 =item respool
5525
5526 =cut
5527
5528 sub process_respool {
5529   process_re_X('spool', @_);
5530 }
5531
5532 use Storable qw(thaw);
5533 use Data::Dumper;
5534 use MIME::Base64;
5535 sub process_re_X {
5536   my( $method, $job ) = ( shift, shift );
5537   warn "$me process_re_X $method for job $job\n" if $DEBUG;
5538
5539   my $param = thaw(decode_base64(shift));
5540   warn Dumper($param) if $DEBUG;
5541
5542   re_X(
5543     $method,
5544     $job,
5545     %$param,
5546   );
5547
5548 }
5549
5550 sub re_X {
5551   my($method, $job, %param ) = @_;
5552   if ( $DEBUG ) {
5553     warn "re_X $method for job $job with param:\n".
5554          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
5555   }
5556
5557   #some false laziness w/search/cust_bill.html
5558   my $distinct = '';
5559   my $orderby = 'ORDER BY cust_bill._date';
5560
5561   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5562
5563   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5564      
5565   my @cust_bill = qsearch( {
5566     #'select'    => "cust_bill.*",
5567     'table'     => 'cust_bill',
5568     'addl_from' => $addl_from,
5569     'hashref'   => {},
5570     'extra_sql' => $extra_sql,
5571     'order_by'  => $orderby,
5572     'debug' => 1,
5573   } );
5574
5575   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5576
5577   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5578     if $DEBUG;
5579
5580   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5581   foreach my $cust_bill ( @cust_bill ) {
5582     $cust_bill->$method();
5583
5584     if ( $job ) { #progressbar foo
5585       $num++;
5586       if ( time - $min_sec > $last ) {
5587         my $error = $job->update_statustext(
5588           int( 100 * $num / scalar(@cust_bill) )
5589         );
5590         die $error if $error;
5591         $last = time;
5592       }
5593     }
5594
5595   }
5596
5597 }
5598
5599 =back
5600
5601 =head1 CLASS METHODS
5602
5603 =over 4
5604
5605 =item owed_sql
5606
5607 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5608
5609 =cut
5610
5611 sub owed_sql {
5612   my ($class, $start, $end) = @_;
5613   'charged - '. 
5614     $class->paid_sql($start, $end). ' - '. 
5615     $class->credited_sql($start, $end);
5616 }
5617
5618 =item net_sql
5619
5620 Returns an SQL fragment to retreive the net amount (charged minus credited).
5621
5622 =cut
5623
5624 sub net_sql {
5625   my ($class, $start, $end) = @_;
5626   'charged - '. $class->credited_sql($start, $end);
5627 }
5628
5629 =item paid_sql
5630
5631 Returns an SQL fragment to retreive the amount paid against this invoice.
5632
5633 =cut
5634
5635 sub paid_sql {
5636   my ($class, $start, $end) = @_;
5637   $start &&= "AND cust_bill_pay._date <= $start";
5638   $end   &&= "AND cust_bill_pay._date > $end";
5639   $start = '' unless defined($start);
5640   $end   = '' unless defined($end);
5641   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5642        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
5643 }
5644
5645 =item credited_sql
5646
5647 Returns an SQL fragment to retreive the amount credited against this invoice.
5648
5649 =cut
5650
5651 sub credited_sql {
5652   my ($class, $start, $end) = @_;
5653   $start &&= "AND cust_credit_bill._date <= $start";
5654   $end   &&= "AND cust_credit_bill._date >  $end";
5655   $start = '' unless defined($start);
5656   $end   = '' unless defined($end);
5657   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5658        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
5659 }
5660
5661 =item due_date_sql
5662
5663 Returns an SQL fragment to retrieve the due date of an invoice.
5664 Currently only supported on PostgreSQL.
5665
5666 =cut
5667
5668 sub due_date_sql {
5669   my $conf = new FS::Conf;
5670 'COALESCE(
5671   SUBSTRING(
5672     COALESCE(
5673       cust_bill.invoice_terms,
5674       cust_main.invoice_terms,
5675       \''.($conf->config('invoice_default_terms') || '').'\'
5676     ), E\'Net (\\\\d+)\'
5677   )::INTEGER, 0
5678 ) * 86400 + cust_bill._date'
5679 }
5680
5681 =item search_sql_where HASHREF
5682
5683 Class method which returns an SQL WHERE fragment to search for parameters
5684 specified in HASHREF.  Valid parameters are
5685
5686 =over 4
5687
5688 =item _date
5689
5690 List reference of start date, end date, as UNIX timestamps.
5691
5692 =item invnum_min
5693
5694 =item invnum_max
5695
5696 =item agentnum
5697
5698 =item charged
5699
5700 List reference of charged limits (exclusive).
5701
5702 =item owed
5703
5704 List reference of charged limits (exclusive).
5705
5706 =item open
5707
5708 flag, return open invoices only
5709
5710 =item net
5711
5712 flag, return net invoices only
5713
5714 =item days
5715
5716 =item newest_percust
5717
5718 =back
5719
5720 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5721
5722 =cut
5723
5724 sub search_sql_where {
5725   my($class, $param) = @_;
5726   if ( $DEBUG ) {
5727     warn "$me search_sql_where called with params: \n".
5728          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
5729   }
5730
5731   my @search = ();
5732
5733   #agentnum
5734   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5735     push @search, "cust_main.agentnum = $1";
5736   }
5737
5738   #refnum
5739   if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5740     push @search, "cust_main.refnum = $1";
5741   }
5742
5743   #custnum
5744   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5745     push @search, "cust_bill.custnum = $1";
5746   }
5747
5748   #customer classnum
5749   if ( $param->{'cust_classnum'} ) {
5750     my $classnums = $param->{'cust_classnum'};
5751     $classnums = [ $classnums ] if !ref($classnums);
5752     $classnums = [ grep /^\d+$/, @$classnums ];
5753     push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
5754       if @$classnums;
5755   }
5756
5757   #_date
5758   if ( $param->{_date} ) {
5759     my($beginning, $ending) = @{$param->{_date}};
5760
5761     push @search, "cust_bill._date >= $beginning",
5762                   "cust_bill._date <  $ending";
5763   }
5764
5765   #invnum
5766   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5767     push @search, "cust_bill.invnum >= $1";
5768   }
5769   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5770     push @search, "cust_bill.invnum <= $1";
5771   }
5772
5773   #charged
5774   if ( $param->{charged} ) {
5775     my @charged = ref($param->{charged})
5776                     ? @{ $param->{charged} }
5777                     : ($param->{charged});
5778
5779     push @search, map { s/^charged/cust_bill.charged/; $_; }
5780                       @charged;
5781   }
5782
5783   my $owed_sql = FS::cust_bill->owed_sql;
5784
5785   #owed
5786   if ( $param->{owed} ) {
5787     my @owed = ref($param->{owed})
5788                  ? @{ $param->{owed} }
5789                  : ($param->{owed});
5790     push @search, map { s/^owed/$owed_sql/; $_; }
5791                       @owed;
5792   }
5793
5794   #open/net flags
5795   push @search, "0 != $owed_sql"
5796     if $param->{'open'};
5797   push @search, '0 != '. FS::cust_bill->net_sql
5798     if $param->{'net'};
5799
5800   #days
5801   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5802     if $param->{'days'};
5803
5804   #newest_percust
5805   if ( $param->{'newest_percust'} ) {
5806
5807     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5808     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5809
5810     my @newest_where = map { my $x = $_;
5811                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
5812                              $x;
5813                            }
5814                            grep ! /^cust_main./, @search;
5815     my $newest_where = scalar(@newest_where)
5816                          ? ' AND '. join(' AND ', @newest_where)
5817                          : '';
5818
5819
5820     push @search, "cust_bill._date = (
5821       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5822         WHERE newest_cust_bill.custnum = cust_bill.custnum
5823           $newest_where
5824     )";
5825
5826   }
5827
5828   #promised_date - also has an option to accept nulls
5829   if ( $param->{promised_date} ) {
5830     my($beginning, $ending, $null) = @{$param->{promised_date}};
5831
5832     push @search, "(( cust_bill.promised_date >= $beginning AND ".
5833                     "cust_bill.promised_date <  $ending )" .
5834                     ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5835   }
5836
5837   #agent virtualization
5838   my $curuser = $FS::CurrentUser::CurrentUser;
5839   if ( $curuser->username eq 'fs_queue'
5840        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5841     my $username = $1;
5842     my $newuser = qsearchs('access_user', {
5843       'username' => $username,
5844       'disabled' => '',
5845     } );
5846     if ( $newuser ) {
5847       $curuser = $newuser;
5848     } else {
5849       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5850     }
5851   }
5852   push @search, $curuser->agentnums_sql;
5853
5854   join(' AND ', @search );
5855
5856 }
5857
5858 =back
5859
5860 =head1 BUGS
5861
5862 The delete method.
5863
5864 =head1 SEE ALSO
5865
5866 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5867 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
5868 documentation.
5869
5870 =cut
5871
5872 1;
5873