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