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