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