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