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