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