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