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