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