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