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