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