77c571b8c3f5cac19b3ade64ce853e62ecb3e5b1
[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{'no_usage'} = 1 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 DEPRECATED? (expensive, mostly unused?)
4766 format_function: the function used to format CDRs.
4767
4768 section: a hashref containing 'description'; if this is present, 
4769 cust_bill_pkg_display records not belonging to this section are 
4770 ignored.
4771
4772 multisection: a flag indicating that this is a multisection invoice,
4773 which does something complicated.
4774
4775 multilocation: a flag to display the location label for the package.
4776
4777 Returns a list of hashrefs, each of which may contain:
4778
4779 pkgnum, description, amount, unit_amount, quantity, _is_setup, and 
4780 ext_description, which is an arrayref of detail lines to show below 
4781 the package line.
4782
4783 =cut
4784
4785 sub _items_cust_bill_pkg {
4786   my $self = shift;
4787   my $conf = $self->conf;
4788   my $cust_bill_pkgs = shift;
4789   my %opt = @_;
4790
4791   my $format = $opt{format} || '';
4792   my $escape_function = $opt{escape_function} || sub { shift };
4793   my $format_function = $opt{format_function} || '';
4794   my $no_usage = $opt{no_usage} || '';
4795   my $unsquelched = $opt{unsquelched} || ''; #unused
4796   my $section = $opt{section}->{description} if $opt{section};
4797   my $summary_page = $opt{summary_page} || ''; #unused
4798   my $multilocation = $opt{multilocation} || '';
4799   my $multisection = $opt{multisection} || '';
4800   my $discount_show_always = 0;
4801
4802   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4803
4804   my @b = ();
4805   my ($s, $r, $u) = ( undef, undef, undef );
4806   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4807   {
4808
4809     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4810       if ( $_ && !$cust_bill_pkg->hidden ) {
4811         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4812         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4813         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4814         push @b, { %$_ }
4815           if $_->{amount} != 0
4816           || $discount_show_always
4817           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4818           || (   $_->{_is_setup} && $_->{setup_show_zero} )
4819         ;
4820         $_ = undef;
4821       }
4822     }
4823
4824     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4825          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4826       if $DEBUG > 1;
4827
4828     foreach my $display ( grep { defined($section)
4829                                  ? $_->section eq $section
4830                                  : 1
4831                                }
4832                           #grep { !$_->summary || !$summary_page } # bunk!
4833                           grep { !$_->summary || $multisection }
4834                           $cust_bill_pkg->cust_bill_pkg_display
4835                         )
4836     {
4837
4838       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4839            $display->billpkgdisplaynum. "\n"
4840         if $DEBUG > 1;
4841
4842       my $type = $display->type;
4843
4844       my $desc = $cust_bill_pkg->desc;
4845       $desc = substr($desc, 0, $maxlength). '...'
4846         if $format eq 'latex' && length($desc) > $maxlength;
4847
4848       my %details_opt = ( 'format'          => $format,
4849                           'escape_function' => $escape_function,
4850                           'format_function' => $format_function,
4851                           'no_usage'        => $opt{'no_usage'},
4852                         );
4853
4854       if ( $cust_bill_pkg->pkgnum > 0 ) {
4855
4856         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4857           if $DEBUG > 1;
4858  
4859         my $cust_pkg = $cust_bill_pkg->cust_pkg;
4860
4861         # start/end dates for invoice formats that do nonstandard 
4862         # things with them
4863         my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4864
4865         if (    (!$type || $type eq 'S')
4866              && (    $cust_bill_pkg->setup != 0
4867                   || $cust_bill_pkg->setup_show_zero
4868                 )
4869            )
4870          {
4871
4872           warn "$me _items_cust_bill_pkg adding setup\n"
4873             if $DEBUG > 1;
4874
4875           my $description = $desc;
4876           $description .= ' Setup'
4877             if $cust_bill_pkg->recur != 0
4878             || $discount_show_always
4879             || $cust_bill_pkg->recur_show_zero;
4880
4881           my @d = ();
4882           unless ( $cust_pkg->part_pkg->hide_svc_detail
4883                 || $cust_bill_pkg->hidden )
4884           {
4885
4886             push @d, map &{$escape_function}($_),
4887                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
4888               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4889
4890             if ( $multilocation ) {
4891               my $loc = $cust_pkg->location_label;
4892               $loc = substr($loc, 0, $maxlength). '...'
4893                 if $format eq 'latex' && length($loc) > $maxlength;
4894               push @d, &{$escape_function}($loc);
4895             }
4896
4897           } #unless hiding service details
4898
4899           push @d, $cust_bill_pkg->details(%details_opt)
4900             if $cust_bill_pkg->recur == 0;
4901
4902           if ( $cust_bill_pkg->hidden ) {
4903             $s->{amount}      += $cust_bill_pkg->setup;
4904             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4905             push @{ $s->{ext_description} }, @d;
4906           } else {
4907             $s = {
4908               _is_setup       => 1,
4909               description     => $description,
4910               #pkgpart         => $part_pkg->pkgpart,
4911               pkgnum          => $cust_bill_pkg->pkgnum,
4912               amount          => $cust_bill_pkg->setup,
4913               setup_show_zero => $cust_bill_pkg->setup_show_zero,
4914               unit_amount     => $cust_bill_pkg->unitsetup,
4915               quantity        => $cust_bill_pkg->quantity,
4916               ext_description => \@d,
4917             };
4918           };
4919
4920         }
4921
4922         if (    ( !$type || $type eq 'R' || $type eq 'U' )
4923              && (
4924                      $cust_bill_pkg->recur != 0
4925                   || $cust_bill_pkg->setup == 0
4926                   || $discount_show_always
4927                   || $cust_bill_pkg->recur_show_zero
4928                 )
4929            )
4930         {
4931
4932           warn "$me _items_cust_bill_pkg adding recur/usage\n"
4933             if $DEBUG > 1;
4934
4935           my $is_summary = $display->summary;
4936           my $description = ($is_summary && $type && $type eq 'U')
4937                             ? "Usage charges" : $desc;
4938
4939           $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4940                           " - ". time2str($date_format, $cust_bill_pkg->edate).
4941                           ")"
4942             unless $conf->exists('disable_line_item_date_ranges')
4943                 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4944
4945           my @d = ();
4946           my @seconds = (); # for display of usage info
4947
4948           #at least until cust_bill_pkg has "past" ranges in addition to
4949           #the "future" sdate/edate ones... see #3032
4950           my @dates = ( $self->_date );
4951           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4952           push @dates, $prev->sdate if $prev;
4953           push @dates, undef if !$prev;
4954
4955           unless ( $cust_pkg->part_pkg->hide_svc_detail
4956                 || $cust_bill_pkg->itemdesc
4957                 || $cust_bill_pkg->hidden
4958                 || $is_summary && $type && $type eq 'U' )
4959           {
4960
4961             warn "$me _items_cust_bill_pkg adding service details\n"
4962               if $DEBUG > 1;
4963
4964             push @d, map &{$escape_function}($_),
4965                          $cust_pkg->h_labels_short(@dates, 'I')
4966                                                    #$cust_bill_pkg->edate,
4967                                                    #$cust_bill_pkg->sdate)
4968               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4969
4970             warn "$me _items_cust_bill_pkg done adding service details\n"
4971               if $DEBUG > 1;
4972
4973             if ( $multilocation ) {
4974               my $loc = $cust_pkg->location_label;
4975               $loc = substr($loc, 0, $maxlength). '...'
4976                 if $format eq 'latex' && length($loc) > $maxlength;
4977               push @d, &{$escape_function}($loc);
4978             }
4979
4980             # Display of seconds_since_sqlradacct:
4981             # On the invoice, when processing @detail_items, look for a field
4982             # named 'seconds'.  This will contain total seconds for each 
4983             # service, in the same order as @ext_description.  For services 
4984             # that don't support this it will show undef.
4985             if ( $conf->exists('svc_acct-usage_seconds') 
4986                  and ! $cust_bill_pkg->pkgpart_override ) {
4987               foreach my $cust_svc ( 
4988                   $cust_pkg->h_cust_svc(@dates, 'I') 
4989                 ) {
4990
4991                 # eval because not having any part_export_usage exports 
4992                 # is a fatal error, last_bill/_date because that's how 
4993                 # sqlradius_hour billing does it
4994                 my $sec = eval {
4995                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
4996                 };
4997                 push @seconds, $sec;
4998               }
4999             } #if svc_acct-usage_seconds
5000
5001           }
5002
5003           unless ( $is_summary ) {
5004             warn "$me _items_cust_bill_pkg adding details\n"
5005               if $DEBUG > 1;
5006
5007             #instead of omitting details entirely in this case (unwanted side
5008             # effects), just omit CDRs
5009             $details_opt{'no_usage'} = 1
5010               if $type && $type eq 'R';
5011
5012             push @d, $cust_bill_pkg->details(%details_opt);
5013           }
5014
5015           warn "$me _items_cust_bill_pkg calculating amount\n"
5016             if $DEBUG > 1;
5017   
5018           my $amount = 0;
5019           if (!$type) {
5020             $amount = $cust_bill_pkg->recur;
5021           } elsif ($type eq 'R') {
5022             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5023           } elsif ($type eq 'U') {
5024             $amount = $cust_bill_pkg->usage;
5025           }
5026   
5027           if ( !$type || $type eq 'R' ) {
5028
5029             warn "$me _items_cust_bill_pkg adding recur\n"
5030               if $DEBUG > 1;
5031
5032             if ( $cust_bill_pkg->hidden ) {
5033               $r->{amount}      += $amount;
5034               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5035               push @{ $r->{ext_description} }, @d;
5036             } else {
5037               $r = {
5038                 description     => $description,
5039                 #pkgpart         => $part_pkg->pkgpart,
5040                 pkgnum          => $cust_bill_pkg->pkgnum,
5041                 amount          => $amount,
5042                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5043                 unit_amount     => $cust_bill_pkg->unitrecur,
5044                 quantity        => $cust_bill_pkg->quantity,
5045                 %item_dates,
5046                 ext_description => \@d,
5047               };
5048               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5049             }
5050
5051           } else {  # $type eq 'U'
5052
5053             warn "$me _items_cust_bill_pkg adding usage\n"
5054               if $DEBUG > 1;
5055
5056             if ( $cust_bill_pkg->hidden ) {
5057               $u->{amount}      += $amount;
5058               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5059               push @{ $u->{ext_description} }, @d;
5060             } else {
5061               $u = {
5062                 description     => $description,
5063                 #pkgpart         => $part_pkg->pkgpart,
5064                 pkgnum          => $cust_bill_pkg->pkgnum,
5065                 amount          => $amount,
5066                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5067                 unit_amount     => $cust_bill_pkg->unitrecur,
5068                 quantity        => $cust_bill_pkg->quantity,
5069                 %item_dates,
5070                 ext_description => \@d,
5071               };
5072             }
5073           }
5074
5075         } # recurring or usage with recurring charge
5076
5077       } else { #pkgnum tax or one-shot line item (??)
5078
5079         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5080           if $DEBUG > 1;
5081
5082         if ( $cust_bill_pkg->setup != 0 ) {
5083           push @b, {
5084             'description' => $desc,
5085             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
5086           };
5087         }
5088         if ( $cust_bill_pkg->recur != 0 ) {
5089           push @b, {
5090             'description' => "$desc (".
5091                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5092                              time2str($date_format, $cust_bill_pkg->edate). ')',
5093             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
5094           };
5095         }
5096
5097       }
5098
5099     }
5100
5101     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5102                                 && $conf->exists('discount-show-always'));
5103
5104   }
5105
5106   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5107     if ( $_  ) {
5108       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
5109       $_->{amount}      =~ s/^\-0\.00$/0.00/;
5110       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5111       push @b, { %$_ }
5112         if $_->{amount} != 0
5113         || $discount_show_always
5114         || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5115         || (   $_->{_is_setup} && $_->{setup_show_zero} )
5116     }
5117   }
5118
5119   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5120     if $DEBUG > 1;
5121
5122   @b;
5123
5124 }
5125
5126 sub _items_credits {
5127   my( $self, %opt ) = @_;
5128   my $trim_len = $opt{'trim_len'} || 60;
5129
5130   my @b;
5131   #credits
5132   foreach ( $self->cust_credited ) {
5133
5134     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5135
5136     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5137     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5138     $reason = " ($reason) " if $reason;
5139
5140     push @b, {
5141       #'description' => 'Credit ref\#'. $_->crednum.
5142       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
5143       #                 $reason,
5144       'description' => $self->mt('Credit applied').' '.
5145                        time2str($date_format,$_->cust_credit->_date). $reason,
5146       'amount'      => sprintf("%.2f",$_->amount),
5147     };
5148   }
5149
5150   @b;
5151
5152 }
5153
5154 sub _items_payments {
5155   my $self = shift;
5156
5157   my @b;
5158   #get & print payments
5159   foreach ( $self->cust_bill_pay ) {
5160
5161     #something more elaborate if $_->amount ne ->cust_pay->paid ?
5162
5163     push @b, {
5164       'description' => $self->mt('Payment received').' '.
5165                        time2str($date_format,$_->cust_pay->_date ),
5166       'amount'      => sprintf("%.2f", $_->amount )
5167     };
5168   }
5169
5170   @b;
5171
5172 }
5173
5174 =item _items_discounts_avail
5175
5176 Returns an array of line item hashrefs representing available term discounts
5177 for this invoice.  This makes the same assumptions that apply to term 
5178 discounts in general: that the package is billed monthly, at a flat rate, 
5179 with no usage charges.  A prorated first month will be handled, as will 
5180 a setup fee if the discount is allowed to apply to setup fees.
5181
5182 =cut
5183
5184 sub _items_discounts_avail {
5185   my $self = shift;
5186   my %terms;
5187   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5188  
5189   my ($previous_balance) = $self->previous;
5190
5191   foreach (qsearch('discount',{ 'months' => { op => '>', value => 1} })) {
5192     $terms{$_->months} = {
5193       pkgnums       => [],
5194       base          => $previous_balance || 0, # pre-discount sum of charges
5195       discounted    => $previous_balance || 0, # post-discount sum
5196       list_pkgnums  => 0, # whether any packages are not discounted
5197     }
5198   }
5199   foreach my $months (keys %terms) {
5200     my $hash = $terms{$months};
5201
5202     # tricky, because packages may not all be eligible for the same discounts
5203     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
5204       my $cust_pkg = $cust_bill_pkg->cust_pkg or next;
5205       my $part_pkg = $cust_pkg->part_pkg or next;
5206
5207       next if $part_pkg->freq ne '1';
5208       my $setup = $cust_bill_pkg->setup || 0;
5209       my $recur = $cust_bill_pkg->recur || 0;
5210       my $permonth = $part_pkg->base_recur_permonth || 0;
5211
5212       my ($discount) = grep { $_->months == $months } 
5213                        map { $_->discount } $part_pkg->part_pkg_discount;
5214
5215       $hash->{base} += $setup + $recur + ($months - 1) * $permonth;
5216       if ($discount) {
5217
5218         my $discountable;
5219         if ( $discount->setup ) {
5220           $discountable += $setup;
5221         }
5222         else {
5223           $hash->{discounted} += $setup;
5224         }
5225
5226         if ( $discount->percent ) {
5227           $discountable += $months * $permonth;
5228           $discountable -= ($discountable * $discount->percent / 100);
5229           $discountable -= ($permonth - $recur); # correct for prorate
5230           $hash->{discounted} += $discountable;
5231         }
5232         else {
5233           $discountable += $recur;
5234           $discountable -= $discount->amount * $recur/$permonth;
5235
5236           $discountable += ($months - 1) * max($permonth - $discount->amount,0);
5237         }
5238
5239         $hash->{discounted} += $discountable;
5240         push @{ $hash->{pkgnums} }, $cust_pkg->pkgnum;
5241       }
5242       else { #no discount
5243         $hash->{discounted} += $setup + $recur + ($months - 1) * $permonth;
5244         $hash->{list_pkgnums} = 1;
5245       }
5246     } #foreach $cust_bill_pkg
5247
5248     # don't show this line if no packages have discounts at this term
5249     # or if there are no new charges to apply the discount to
5250     delete $terms{$months} if $hash->{base} == $hash->{discounted}
5251                            or $hash->{base} == 0;
5252
5253   }
5254
5255   $list_pkgnums = grep { $_->{list_pkgnums} > 0 } values %terms;
5256
5257   foreach my $months (keys %terms) {
5258     my $hash = $terms{$months};
5259     my $term_total = sprintf('%.2f', $hash->{discounted});
5260     # possibly shouldn't include previous balance in these?
5261     my $percent = sprintf('%.0f', 100 * (1 - $term_total / $hash->{base}) );
5262     my $permonth = sprintf('%.2f', $term_total / $months);
5263
5264     $hash->{description} = $self->mt('Save [_1]% by paying for [_2] months',
5265       $percent, $months
5266     );
5267     $hash->{amount} = $self->mt('[_1] ([_2] per month)', 
5268       $term_total, $money_char.$permonth
5269     );
5270
5271     my @detail;
5272     if ( $list_pkgnums ) {
5273       push @detail, $self->mt('discount on item'). ' '.
5274                 join(', ', map { "#$_" } @{ $hash->{pkgnums} });
5275     }
5276     $hash->{ext_description} = join ', ', @detail;
5277   }
5278
5279   map { $terms{$_} } sort {$b <=> $a} keys %terms;
5280 }
5281
5282 =item call_details [ OPTION => VALUE ... ]
5283
5284 Returns an array of CSV strings representing the call details for this invoice
5285 The only option available is the boolean prepend_billed_number
5286
5287 =cut
5288
5289 sub call_details {
5290   my ($self, %opt) = @_;
5291
5292   my $format_function = sub { shift };
5293
5294   if ($opt{prepend_billed_number}) {
5295     $format_function = sub {
5296       my $detail = shift;
5297       my $row = shift;
5298
5299       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5300       
5301     };
5302   }
5303
5304   my @details = map { $_->details( 'format_function' => $format_function,
5305                                    'escape_function' => sub{ return() },
5306                                  )
5307                     }
5308                   grep { $_->pkgnum }
5309                   $self->cust_bill_pkg;
5310   my $header = $details[0];
5311   ( $header, grep { $_ ne $header } @details );
5312 }
5313
5314
5315 =back
5316
5317 =head1 SUBROUTINES
5318
5319 =over 4
5320
5321 =item process_reprint
5322
5323 =cut
5324
5325 sub process_reprint {
5326   process_re_X('print', @_);
5327 }
5328
5329 =item process_reemail
5330
5331 =cut
5332
5333 sub process_reemail {
5334   process_re_X('email', @_);
5335 }
5336
5337 =item process_refax
5338
5339 =cut
5340
5341 sub process_refax {
5342   process_re_X('fax', @_);
5343 }
5344
5345 =item process_reftp
5346
5347 =cut
5348
5349 sub process_reftp {
5350   process_re_X('ftp', @_);
5351 }
5352
5353 =item respool
5354
5355 =cut
5356
5357 sub process_respool {
5358   process_re_X('spool', @_);
5359 }
5360
5361 use Storable qw(thaw);
5362 use Data::Dumper;
5363 use MIME::Base64;
5364 sub process_re_X {
5365   my( $method, $job ) = ( shift, shift );
5366   warn "$me process_re_X $method for job $job\n" if $DEBUG;
5367
5368   my $param = thaw(decode_base64(shift));
5369   warn Dumper($param) if $DEBUG;
5370
5371   re_X(
5372     $method,
5373     $job,
5374     %$param,
5375   );
5376
5377 }
5378
5379 sub re_X {
5380   my($method, $job, %param ) = @_;
5381   if ( $DEBUG ) {
5382     warn "re_X $method for job $job with param:\n".
5383          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
5384   }
5385
5386   #some false laziness w/search/cust_bill.html
5387   my $distinct = '';
5388   my $orderby = 'ORDER BY cust_bill._date';
5389
5390   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5391
5392   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5393      
5394   my @cust_bill = qsearch( {
5395     #'select'    => "cust_bill.*",
5396     'table'     => 'cust_bill',
5397     'addl_from' => $addl_from,
5398     'hashref'   => {},
5399     'extra_sql' => $extra_sql,
5400     'order_by'  => $orderby,
5401     'debug' => 1,
5402   } );
5403
5404   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5405
5406   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5407     if $DEBUG;
5408
5409   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5410   foreach my $cust_bill ( @cust_bill ) {
5411     $cust_bill->$method();
5412
5413     if ( $job ) { #progressbar foo
5414       $num++;
5415       if ( time - $min_sec > $last ) {
5416         my $error = $job->update_statustext(
5417           int( 100 * $num / scalar(@cust_bill) )
5418         );
5419         die $error if $error;
5420         $last = time;
5421       }
5422     }
5423
5424   }
5425
5426 }
5427
5428 =back
5429
5430 =head1 CLASS METHODS
5431
5432 =over 4
5433
5434 =item owed_sql
5435
5436 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5437
5438 =cut
5439
5440 sub owed_sql {
5441   my ($class, $start, $end) = @_;
5442   'charged - '. 
5443     $class->paid_sql($start, $end). ' - '. 
5444     $class->credited_sql($start, $end);
5445 }
5446
5447 =item net_sql
5448
5449 Returns an SQL fragment to retreive the net amount (charged minus credited).
5450
5451 =cut
5452
5453 sub net_sql {
5454   my ($class, $start, $end) = @_;
5455   'charged - '. $class->credited_sql($start, $end);
5456 }
5457
5458 =item paid_sql
5459
5460 Returns an SQL fragment to retreive the amount paid against this invoice.
5461
5462 =cut
5463
5464 sub paid_sql {
5465   my ($class, $start, $end) = @_;
5466   $start &&= "AND cust_bill_pay._date <= $start";
5467   $end   &&= "AND cust_bill_pay._date > $end";
5468   $start = '' unless defined($start);
5469   $end   = '' unless defined($end);
5470   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5471        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
5472 }
5473
5474 =item credited_sql
5475
5476 Returns an SQL fragment to retreive the amount credited against this invoice.
5477
5478 =cut
5479
5480 sub credited_sql {
5481   my ($class, $start, $end) = @_;
5482   $start &&= "AND cust_credit_bill._date <= $start";
5483   $end   &&= "AND cust_credit_bill._date >  $end";
5484   $start = '' unless defined($start);
5485   $end   = '' unless defined($end);
5486   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5487        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
5488 }
5489
5490 =item due_date_sql
5491
5492 Returns an SQL fragment to retrieve the due date of an invoice.
5493 Currently only supported on PostgreSQL.
5494
5495 =cut
5496
5497 sub due_date_sql {
5498   my $conf = new FS::Conf;
5499 'COALESCE(
5500   SUBSTRING(
5501     COALESCE(
5502       cust_bill.invoice_terms,
5503       cust_main.invoice_terms,
5504       \''.($conf->config('invoice_default_terms') || '').'\'
5505     ), E\'Net (\\\\d+)\'
5506   )::INTEGER, 0
5507 ) * 86400 + cust_bill._date'
5508 }
5509
5510 =item search_sql_where HASHREF
5511
5512 Class method which returns an SQL WHERE fragment to search for parameters
5513 specified in HASHREF.  Valid parameters are
5514
5515 =over 4
5516
5517 =item _date
5518
5519 List reference of start date, end date, as UNIX timestamps.
5520
5521 =item invnum_min
5522
5523 =item invnum_max
5524
5525 =item agentnum
5526
5527 =item charged
5528
5529 List reference of charged limits (exclusive).
5530
5531 =item owed
5532
5533 List reference of charged limits (exclusive).
5534
5535 =item open
5536
5537 flag, return open invoices only
5538
5539 =item net
5540
5541 flag, return net invoices only
5542
5543 =item days
5544
5545 =item newest_percust
5546
5547 =back
5548
5549 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5550
5551 =cut
5552
5553 sub search_sql_where {
5554   my($class, $param) = @_;
5555   if ( $DEBUG ) {
5556     warn "$me search_sql_where called with params: \n".
5557          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
5558   }
5559
5560   my @search = ();
5561
5562   #agentnum
5563   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5564     push @search, "cust_main.agentnum = $1";
5565   }
5566
5567   #agentnum
5568   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5569     push @search, "cust_bill.custnum = $1";
5570   }
5571
5572   #_date
5573   if ( $param->{_date} ) {
5574     my($beginning, $ending) = @{$param->{_date}};
5575
5576     push @search, "cust_bill._date >= $beginning",
5577                   "cust_bill._date <  $ending";
5578   }
5579
5580   #invnum
5581   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5582     push @search, "cust_bill.invnum >= $1";
5583   }
5584   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5585     push @search, "cust_bill.invnum <= $1";
5586   }
5587
5588   #charged
5589   if ( $param->{charged} ) {
5590     my @charged = ref($param->{charged})
5591                     ? @{ $param->{charged} }
5592                     : ($param->{charged});
5593
5594     push @search, map { s/^charged/cust_bill.charged/; $_; }
5595                       @charged;
5596   }
5597
5598   my $owed_sql = FS::cust_bill->owed_sql;
5599
5600   #owed
5601   if ( $param->{owed} ) {
5602     my @owed = ref($param->{owed})
5603                  ? @{ $param->{owed} }
5604                  : ($param->{owed});
5605     push @search, map { s/^owed/$owed_sql/; $_; }
5606                       @owed;
5607   }
5608
5609   #open/net flags
5610   push @search, "0 != $owed_sql"
5611     if $param->{'open'};
5612   push @search, '0 != '. FS::cust_bill->net_sql
5613     if $param->{'net'};
5614
5615   #days
5616   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5617     if $param->{'days'};
5618
5619   #newest_percust
5620   if ( $param->{'newest_percust'} ) {
5621
5622     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5623     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5624
5625     my @newest_where = map { my $x = $_;
5626                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
5627                              $x;
5628                            }
5629                            grep ! /^cust_main./, @search;
5630     my $newest_where = scalar(@newest_where)
5631                          ? ' AND '. join(' AND ', @newest_where)
5632                          : '';
5633
5634
5635     push @search, "cust_bill._date = (
5636       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5637         WHERE newest_cust_bill.custnum = cust_bill.custnum
5638           $newest_where
5639     )";
5640
5641   }
5642
5643   #agent virtualization
5644   my $curuser = $FS::CurrentUser::CurrentUser;
5645   if ( $curuser->username eq 'fs_queue'
5646        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5647     my $username = $1;
5648     my $newuser = qsearchs('access_user', {
5649       'username' => $username,
5650       'disabled' => '',
5651     } );
5652     if ( $newuser ) {
5653       $curuser = $newuser;
5654     } else {
5655       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5656     }
5657   }
5658   push @search, $curuser->agentnums_sql;
5659
5660   join(' AND ', @search );
5661
5662 }
5663
5664 =back
5665
5666 =head1 BUGS
5667
5668 The delete method.
5669
5670 =head1 SEE ALSO
5671
5672 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5673 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
5674 documentation.
5675
5676 =cut
5677
5678 1;
5679