add advertising source to sales/credits/receipts summary, RT#18349
[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
2985   my $default_section = { 'description' => '',
2986                           'subtotal'    => '', 
2987                           'no_subtotal' => 1,
2988                         };
2989
2990   if ( $multisection ) {
2991     ($extra_sections, $extra_lines) =
2992       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2993       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2994
2995     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2996
2997     push @detail_items, @$extra_lines if $extra_lines;
2998     push @sections,
2999       $self->_items_sections( $late_sections,      # this could stand a refactor
3000                               $summarypage,
3001                               $escape_function_nonbsp,
3002                               $extra_sections,
3003                               $format,             #bah
3004                             );
3005     if ($conf->exists('svc_phone_sections')) {
3006       my ($phone_sections, $phone_lines) =
3007         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3008       push @{$late_sections}, @$phone_sections;
3009       push @detail_items, @$phone_lines;
3010     }
3011     if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3012       my ($accountcode_section, $accountcode_lines) =
3013         $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3014       if ( scalar(@$accountcode_lines) ) {
3015           push @{$late_sections}, $accountcode_section;
3016           push @detail_items, @$accountcode_lines;
3017       }
3018     }
3019   } else {# not multisection
3020     # make a default section
3021     push @sections, $default_section;
3022     # and calculate the finance charge total, since it won't get done otherwise.
3023     # XXX possibly other totals?
3024     # XXX possibly finance_pkgclass should not be used in this manner?
3025     if ( $conf->exists('finance_pkgclass') ) {
3026       my @finance_charges;
3027       foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3028         if ( grep { $_->section eq $invoice_data{finance_section} }
3029              $cust_bill_pkg->cust_bill_pkg_display ) {
3030           # I think these are always setup fees, but just to be sure...
3031           push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3032         }
3033       }
3034       $invoice_data{finance_amount} = 
3035         sprintf('%.2f', sum( @finance_charges ) || 0);
3036     }
3037   }
3038
3039   unless (    $conf->exists('disable_previous_balance', $agentnum)
3040            || $conf->exists('previous_balance-summary_only')
3041          )
3042   {
3043
3044     warn "$me adding previous balances\n"
3045       if $DEBUG > 1;
3046
3047     foreach my $line_item ( $self->_items_previous ) {
3048
3049       my $detail = {
3050         ext_description => [],
3051       };
3052       $detail->{'ref'} = $line_item->{'pkgnum'};
3053       $detail->{'quantity'} = 1;
3054       $detail->{'section'} = $multisection ? $previous_section
3055                                            : $default_section;
3056       $detail->{'description'} = &$escape_function($line_item->{'description'});
3057       if ( exists $line_item->{'ext_description'} ) {
3058         @{$detail->{'ext_description'}} = map {
3059           &$escape_function($_);
3060         } @{$line_item->{'ext_description'}};
3061       }
3062       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3063                             $line_item->{'amount'};
3064       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3065
3066       push @detail_items, $detail;
3067       push @buf, [ $detail->{'description'},
3068                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3069                  ];
3070     }
3071
3072   }
3073   
3074   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) ) 
3075     {
3076     push @buf, ['','-----------'];
3077     push @buf, [ $self->mt('Total Previous Balance'),
3078                  $money_char. sprintf("%10.2f", $pr_total) ];
3079     push @buf, ['',''];
3080   }
3081  
3082   if ( $conf->exists('svc_phone-did-summary') ) {
3083       warn "$me adding DID summary\n"
3084         if $DEBUG > 1;
3085
3086       my ($didsummary,$minutes) = $self->_did_summary;
3087       my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3088       push @detail_items, 
3089        { 'description' => $didsummary_desc,
3090            'ext_description' => [ $didsummary, $minutes ],
3091        };
3092   }
3093
3094   foreach my $section (@sections, @$late_sections) {
3095
3096     warn "$me adding section \n". Dumper($section)
3097       if $DEBUG > 1;
3098
3099     # begin some normalization
3100     $section->{'subtotal'} = $section->{'amount'}
3101       if $multisection
3102          && !exists($section->{subtotal})
3103          && exists($section->{amount});
3104
3105     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3106       if ( $invoice_data{finance_section} &&
3107            $section->{'description'} eq $invoice_data{finance_section} );
3108
3109     $section->{'subtotal'} = $other_money_char.
3110                              sprintf('%.2f', $section->{'subtotal'})
3111       if $multisection;
3112
3113     # continue some normalization
3114     $section->{'amount'}   = $section->{'subtotal'}
3115       if $multisection;
3116
3117
3118     if ( $section->{'description'} ) {
3119       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3120                    [ '', '' ],
3121                  );
3122     }
3123
3124     warn "$me   setting options\n"
3125       if $DEBUG > 1;
3126
3127     my $multilocation = scalar($cust_main->cust_location); #too expensive?
3128     my %options = ();
3129     $options{'section'} = $section if $multisection;
3130     $options{'format'} = $format;
3131     $options{'escape_function'} = $escape_function;
3132     $options{'no_usage'} = 1 unless $unsquelched;
3133     $options{'unsquelched'} = $unsquelched;
3134     $options{'summary_page'} = $summarypage;
3135     $options{'skip_usage'} =
3136       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3137     $options{'multilocation'} = $multilocation;
3138     $options{'multisection'} = $multisection;
3139
3140     warn "$me   searching for line items\n"
3141       if $DEBUG > 1;
3142
3143     foreach my $line_item ( $self->_items_pkg(%options) ) {
3144
3145       warn "$me     adding line item $line_item\n"
3146         if $DEBUG > 1;
3147
3148       my $detail = {
3149         ext_description => [],
3150       };
3151       $detail->{'ref'} = $line_item->{'pkgnum'};
3152       $detail->{'quantity'} = $line_item->{'quantity'};
3153       $detail->{'section'} = $section;
3154       $detail->{'description'} = &$escape_function($line_item->{'description'});
3155       if ( exists $line_item->{'ext_description'} ) {
3156         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3157       }
3158       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3159                               $line_item->{'amount'};
3160       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3161                                  $line_item->{'unit_amount'};
3162       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3163
3164       $detail->{'sdate'} = $line_item->{'sdate'};
3165       $detail->{'edate'} = $line_item->{'edate'};
3166       $detail->{'seconds'} = $line_item->{'seconds'};
3167   
3168       push @detail_items, $detail;
3169       push @buf, ( [ $detail->{'description'},
3170                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3171                    ],
3172                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3173                  );
3174     }
3175
3176     if ( $section->{'description'} ) {
3177       push @buf, ( ['','-----------'],
3178                    [ $section->{'description'}. ' sub-total',
3179                       $section->{'subtotal'} # already formatted this 
3180                    ],
3181                    [ '', '' ],
3182                    [ '', '' ],
3183                  );
3184     }
3185   
3186   }
3187
3188   $invoice_data{current_less_finance} =
3189     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3190
3191   if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
3192     || $conf->exists('previous_balance-summary_only') )
3193   {
3194     unshift @sections, $previous_section if $pr_total;
3195   }
3196
3197   warn "$me adding taxes\n"
3198     if $DEBUG > 1;
3199
3200   foreach my $tax ( $self->_items_tax ) {
3201
3202     $taxtotal += $tax->{'amount'};
3203
3204     my $description = &$escape_function( $tax->{'description'} );
3205     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
3206
3207     if ( $multisection ) {
3208
3209       my $money = $old_latex ? '' : $money_char;
3210       push @detail_items, {
3211         ext_description => [],
3212         ref          => '',
3213         quantity     => '',
3214         description  => $description,
3215         amount       => $money. $amount,
3216         product_code => '',
3217         section      => $tax_section,
3218       };
3219
3220     } else {
3221
3222       push @total_items, {
3223         'total_item'   => $description,
3224         'total_amount' => $other_money_char. $amount,
3225       };
3226
3227     }
3228
3229     push @buf,[ $description,
3230                 $money_char. $amount,
3231               ];
3232
3233   }
3234   
3235   if ( $taxtotal ) {
3236     my $total = {};
3237     $total->{'total_item'} = $self->mt('Sub-total');
3238     $total->{'total_amount'} =
3239       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3240
3241     if ( $multisection ) {
3242       $tax_section->{'subtotal'} = $other_money_char.
3243                                    sprintf('%.2f', $taxtotal);
3244       $tax_section->{'pretotal'} = 'New charges sub-total '.
3245                                    $total->{'total_amount'};
3246       push @sections, $tax_section if $taxtotal;
3247     }else{
3248       unshift @total_items, $total;
3249     }
3250   }
3251   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3252
3253   push @buf,['','-----------'];
3254   push @buf,[$self->mt( 
3255               $conf->exists('disable_previous_balance', $agentnum) 
3256                ? 'Total Charges'
3257                : 'Total New Charges'
3258              ),
3259              $money_char. sprintf("%10.2f",$self->charged) ];
3260   push @buf,['',''];
3261
3262   {
3263     my $total = {};
3264     my $item = 'Total';
3265     $item = $conf->config('previous_balance-exclude_from_total')
3266          || 'Total New Charges'
3267       if $conf->exists('previous_balance-exclude_from_total');
3268     my $amount = $self->charged +
3269                    ( $conf->exists('disable_previous_balance', $agentnum) ||
3270                      $conf->exists('previous_balance-exclude_from_total')
3271                      ? 0
3272                      : $pr_total
3273                    );
3274     $total->{'total_item'} = &$embolden_function($self->mt($item));
3275     $total->{'total_amount'} =
3276       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
3277     if ( $multisection ) {
3278       if ( $adjust_section->{'sort_weight'} ) {
3279         $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3280           $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
3281       } else {
3282         $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3283           $other_money_char.  sprintf('%.2f', $self->charged );
3284       } 
3285     }else{
3286       push @total_items, $total;
3287     }
3288     push @buf,['','-----------'];
3289     push @buf,[$item,
3290                $money_char.
3291                sprintf( '%10.2f', $amount )
3292               ];
3293     push @buf,['',''];
3294   }
3295   
3296   unless ( $conf->exists('disable_previous_balance', $agentnum) ) {
3297     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3298   
3299     # credits
3300     my $credittotal = 0;
3301     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3302
3303       my $total;
3304       $total->{'total_item'} = &$escape_function($credit->{'description'});
3305       $credittotal += $credit->{'amount'};
3306       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3307       $adjusttotal += $credit->{'amount'};
3308       if ( $multisection ) {
3309         my $money = $old_latex ? '' : $money_char;
3310         push @detail_items, {
3311           ext_description => [],
3312           ref          => '',
3313           quantity     => '',
3314           description  => &$escape_function($credit->{'description'}),
3315           amount       => $money. $credit->{'amount'},
3316           product_code => '',
3317           section      => $adjust_section,
3318         };
3319       } else {
3320         push @total_items, $total;
3321       }
3322
3323     }
3324     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3325
3326     #credits (again)
3327     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3328       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3329     }
3330
3331     # payments
3332     my $paymenttotal = 0;
3333     foreach my $payment ( $self->_items_payments ) {
3334       my $total = {};
3335       $total->{'total_item'} = &$escape_function($payment->{'description'});
3336       $paymenttotal += $payment->{'amount'};
3337       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3338       $adjusttotal += $payment->{'amount'};
3339       if ( $multisection ) {
3340         my $money = $old_latex ? '' : $money_char;
3341         push @detail_items, {
3342           ext_description => [],
3343           ref          => '',
3344           quantity     => '',
3345           description  => &$escape_function($payment->{'description'}),
3346           amount       => $money. $payment->{'amount'},
3347           product_code => '',
3348           section      => $adjust_section,
3349         };
3350       }else{
3351         push @total_items, $total;
3352       }
3353       push @buf, [ $payment->{'description'},
3354                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
3355                  ];
3356     }
3357     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3358   
3359     if ( $multisection ) {
3360       $adjust_section->{'subtotal'} = $other_money_char.
3361                                       sprintf('%.2f', $adjusttotal);
3362       push @sections, $adjust_section
3363         unless $adjust_section->{sort_weight};
3364     }
3365
3366     # create Balance Due message
3367     { 
3368       my $total;
3369       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3370       $total->{'total_amount'} =
3371         &$embolden_function(
3372           $other_money_char. sprintf('%.2f', $summarypage 
3373                                                ? $self->charged +
3374                                                  $self->billing_balance
3375                                                : $self->owed + $pr_total
3376                                     )
3377         );
3378       if ( $multisection && !$adjust_section->{sort_weight} ) {
3379         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3380                                          $total->{'total_amount'};
3381       }else{
3382         push @total_items, $total;
3383       }
3384       push @buf,['','-----------'];
3385       push @buf,[$self->balance_due_msg, $money_char. 
3386         sprintf("%10.2f", $balance_due ) ];
3387     }
3388
3389     if ( $conf->exists('previous_balance-show_credit')
3390         and $cust_main->balance < 0 ) {
3391       my $credit_total = {
3392         'total_item'    => &$embolden_function($self->credit_balance_msg),
3393         'total_amount'  => &$embolden_function(
3394           $other_money_char. sprintf('%.2f', -$cust_main->balance)
3395         ),
3396       };
3397       if ( $multisection ) {
3398         $adjust_section->{'posttotal'} .= $newline_token .
3399           $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3400       }
3401       else {
3402         push @total_items, $credit_total;
3403       }
3404       push @buf,['','-----------'];
3405       push @buf,[$self->credit_balance_msg, $money_char. 
3406         sprintf("%10.2f", -$cust_main->balance ) ];
3407     }
3408   }
3409
3410   if ( $multisection ) {
3411     if ($conf->exists('svc_phone_sections')) {
3412       my $total;
3413       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3414       $total->{'total_amount'} =
3415         &$embolden_function(
3416           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3417         );
3418       my $last_section = pop @sections;
3419       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3420                                      $total->{'total_amount'};
3421       push @sections, $last_section;
3422     }
3423     push @sections, @$late_sections
3424       if $unsquelched;
3425   }
3426
3427   # make a discounts-available section, even without multisection
3428   if ( $conf->exists('discount-show_available') 
3429        and my @discounts_avail = $self->_items_discounts_avail ) {
3430     my $discount_section = {
3431       'description' => $self->mt('Discounts Available'),
3432       'subtotal'    => '',
3433       'no_subtotal' => 1,
3434     };
3435
3436     push @sections, $discount_section;
3437     push @detail_items, map { +{
3438         'ref'         => '', #should this be something else?
3439         'section'     => $discount_section,
3440         'description' => &$escape_function( $_->{description} ),
3441         'amount'      => $money_char . &$escape_function( $_->{amount} ),
3442         'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3443     } } @discounts_avail;
3444   }
3445
3446   # All sections and items are built; now fill in templates.
3447   my @includelist = ();
3448   push @includelist, 'summary' if $summarypage;
3449   foreach my $include ( @includelist ) {
3450
3451     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3452     my @inc_src;
3453
3454     if ( length( $conf->config($inc_file, $agentnum) ) ) {
3455
3456       @inc_src = $conf->config($inc_file, $agentnum);
3457
3458     } else {
3459
3460       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3461
3462       my $convert_map = $convert_maps{$format}{$include};
3463
3464       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3465                        s/--\@\]/$delimiters{$format}[1]/g;
3466                        $_;
3467                      } 
3468                  &$convert_map( $conf->config($inc_file, $agentnum) );
3469
3470     }
3471
3472     my $inc_tt = new Text::Template (
3473       TYPE       => 'ARRAY',
3474       SOURCE     => [ map "$_\n", @inc_src ],
3475       DELIMITERS => $delimiters{$format},
3476     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3477
3478     unless ( $inc_tt->compile() ) {
3479       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3480       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3481       die $error;
3482     }
3483
3484     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3485
3486     $invoice_data{$include} =~ s/\n+$//
3487       if ($format eq 'latex');
3488   }
3489
3490   $invoice_lines = 0;
3491   my $wasfunc = 0;
3492   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3493     /invoice_lines\((\d*)\)/;
3494     $invoice_lines += $1 || scalar(@buf);
3495     $wasfunc=1;
3496   }
3497   die "no invoice_lines() functions in template?"
3498     if ( $format eq 'template' && !$wasfunc );
3499
3500   if ($format eq 'template') {
3501
3502     if ( $invoice_lines ) {
3503       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3504       $invoice_data{'total_pages'}++
3505         if scalar(@buf) % $invoice_lines;
3506     }
3507
3508     #setup subroutine for the template
3509     $invoice_data{invoice_lines} = sub {
3510       my $lines = shift || scalar(@buf);
3511       map { 
3512         scalar(@buf)
3513           ? shift @buf
3514           : [ '', '' ];
3515       }
3516       ( 1 .. $lines );
3517     };
3518
3519     my $lines;
3520     my @collect;
3521     while (@buf) {
3522       push @collect, split("\n",
3523         $text_template->fill_in( HASH => \%invoice_data )
3524       );
3525       $invoice_data{'page'}++;
3526     }
3527     map "$_\n", @collect;
3528   }else{
3529     # this is where we actually create the invoice
3530     warn "filling in template for invoice ". $self->invnum. "\n"
3531       if $DEBUG;
3532     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3533       if $DEBUG > 1;
3534
3535     $text_template->fill_in(HASH => \%invoice_data);
3536   }
3537 }
3538
3539 # helper routine for generating date ranges
3540 sub _prior_month30s {
3541   my $self = shift;
3542   my @ranges = (
3543    [ 1,       2592000 ], # 0-30 days ago
3544    [ 2592000, 5184000 ], # 30-60 days ago
3545    [ 5184000, 7776000 ], # 60-90 days ago
3546    [ 7776000, 0       ], # 90+   days ago
3547   );
3548
3549   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3550           $_->[1] ? $self->_date - $_->[1] - 1 : '',
3551       ] }
3552   @ranges;
3553 }
3554
3555 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3556
3557 Returns an postscript invoice, as a scalar.
3558
3559 Options can be passed as a hashref (recommended) or as a list of time, template
3560 and then any key/value pairs for any other options.
3561
3562 I<time> an optional value used to control the printing of overdue messages.  The
3563 default is now.  It isn't the date of the invoice; that's the `_date' field.
3564 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3565 L<Time::Local> and L<Date::Parse> for conversion functions.
3566
3567 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3568
3569 =cut
3570
3571 sub print_ps {
3572   my $self = shift;
3573
3574   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3575   my $ps = generate_ps($file);
3576   unlink($logofile);
3577   unlink($barcodefile) if $barcodefile;
3578
3579   $ps;
3580 }
3581
3582 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3583
3584 Returns an PDF invoice, as a scalar.
3585
3586 Options can be passed as a hashref (recommended) or as a list of time, template
3587 and then any key/value pairs for any other options.
3588
3589 I<time> an optional value used to control the printing of overdue messages.  The
3590 default is now.  It isn't the date of the invoice; that's the `_date' field.
3591 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3592 L<Time::Local> and L<Date::Parse> for conversion functions.
3593
3594 I<template>, if specified, is the name of a suffix for alternate invoices.
3595
3596 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3597
3598 =cut
3599
3600 sub print_pdf {
3601   my $self = shift;
3602
3603   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3604   my $pdf = generate_pdf($file);
3605   unlink($logofile);
3606   unlink($barcodefile) if $barcodefile;
3607
3608   $pdf;
3609 }
3610
3611 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3612
3613 Returns an HTML invoice, as a scalar.
3614
3615 I<time> an optional value used to control the printing of overdue messages.  The
3616 default is now.  It isn't the date of the invoice; that's the `_date' field.
3617 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3618 L<Time::Local> and L<Date::Parse> for conversion functions.
3619
3620 I<template>, if specified, is the name of a suffix for alternate invoices.
3621
3622 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3623
3624 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3625 when emailing the invoice as part of a multipart/related MIME email.
3626
3627 =cut
3628
3629 sub print_html {
3630   my $self = shift;
3631   my %params;
3632   if ( ref($_[0]) ) {
3633     %params = %{ shift() }; 
3634   }else{
3635     $params{'time'} = shift;
3636     $params{'template'} = shift;
3637     $params{'cid'} = shift;
3638   }
3639
3640   $params{'format'} = 'html';
3641   
3642   $self->print_generic( %params );
3643 }
3644
3645 # quick subroutine for print_latex
3646 #
3647 # There are ten characters that LaTeX treats as special characters, which
3648 # means that they do not simply typeset themselves: 
3649 #      # $ % & ~ _ ^ \ { }
3650 #
3651 # TeX ignores blanks following an escaped character; if you want a blank (as
3652 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3653
3654 sub _latex_escape {
3655   my $value = shift;
3656   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3657   $value =~ s/([<>])/\$$1\$/g;
3658   $value;
3659 }
3660
3661 sub _html_escape {
3662   my $value = shift;
3663   encode_entities($value);
3664   $value;
3665 }
3666
3667 sub _html_escape_nbsp {
3668   my $value = _html_escape(shift);
3669   $value =~ s/ +/&nbsp;/g;
3670   $value;
3671 }
3672
3673 #utility methods for print_*
3674
3675 sub _translate_old_latex_format {
3676   warn "_translate_old_latex_format called\n"
3677     if $DEBUG; 
3678
3679   my @template = ();
3680   while ( @_ ) {
3681     my $line = shift;
3682   
3683     if ( $line =~ /^%%Detail\s*$/ ) {
3684   
3685       push @template, q![@--!,
3686                       q!  foreach my $_tr_line (@detail_items) {!,
3687                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3688                       q!      $_tr_line->{'description'} .= !, 
3689                       q!        "\\tabularnewline\n~~".!,
3690                       q!        join( "\\tabularnewline\n~~",!,
3691                       q!          @{$_tr_line->{'ext_description'}}!,
3692                       q!        );!,
3693                       q!    }!;
3694
3695       while ( ( my $line_item_line = shift )
3696               !~ /^%%EndDetail\s*$/                            ) {
3697         $line_item_line =~ s/'/\\'/g;    # nice LTS
3698         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3699         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3700         push @template, "    \$OUT .= '$line_item_line';";
3701       }
3702
3703       push @template, '}',
3704                       '--@]';
3705       #' doh, gvim
3706     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3707
3708       push @template, '[@--',
3709                       '  foreach my $_tr_line (@total_items) {';
3710
3711       while ( ( my $total_item_line = shift )
3712               !~ /^%%EndTotalDetails\s*$/                      ) {
3713         $total_item_line =~ s/'/\\'/g;    # nice LTS
3714         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3715         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3716         push @template, "    \$OUT .= '$total_item_line';";
3717       }
3718
3719       push @template, '}',
3720                       '--@]';
3721
3722     } else {
3723       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3724       push @template, $line;  
3725     }
3726   
3727   }
3728
3729   if ($DEBUG) {
3730     warn "$_\n" foreach @template;
3731   }
3732
3733   (@template);
3734 }
3735
3736 sub terms {
3737   my $self = shift;
3738   my $conf = $self->conf;
3739
3740   #check for an invoice-specific override
3741   return $self->invoice_terms if $self->invoice_terms;
3742   
3743   #check for a customer- specific override
3744   my $cust_main = $self->cust_main;
3745   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3746
3747   #use configured default
3748   $conf->config('invoice_default_terms') || '';
3749 }
3750
3751 sub due_date {
3752   my $self = shift;
3753   my $duedate = '';
3754   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3755     $duedate = $self->_date() + ( $1 * 86400 );
3756   }
3757   $duedate;
3758 }
3759
3760 sub due_date2str {
3761   my $self = shift;
3762   $self->due_date ? time2str(shift, $self->due_date) : '';
3763 }
3764
3765 sub balance_due_msg {
3766   my $self = shift;
3767   my $msg = $self->mt('Balance Due');
3768   return $msg unless $self->terms;
3769   if ( $self->due_date ) {
3770     $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3771       $self->due_date2str($date_format);
3772   } elsif ( $self->terms ) {
3773     $msg .= ' - '. $self->terms;
3774   }
3775   $msg;
3776 }
3777
3778 sub balance_due_date {
3779   my $self = shift;
3780   my $conf = $self->conf;
3781   my $duedate = '';
3782   if (    $conf->exists('invoice_default_terms') 
3783        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3784     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3785   }
3786   $duedate;
3787 }
3788
3789 sub credit_balance_msg { 
3790   my $self = shift;
3791   $self->mt('Credit Balance Remaining')
3792 }
3793
3794 =item invnum_date_pretty
3795
3796 Returns a string with the invoice number and date, for example:
3797 "Invoice #54 (3/20/2008)"
3798
3799 =cut
3800
3801 sub invnum_date_pretty {
3802   my $self = shift;
3803   $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3804 }
3805
3806 =item _date_pretty
3807
3808 Returns a string with the date, for example: "3/20/2008"
3809
3810 =cut
3811
3812 sub _date_pretty {
3813   my $self = shift;
3814   time2str($date_format, $self->_date);
3815 }
3816
3817 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3818
3819 Generate section information for all items appearing on this invoice.
3820 This will only be called for multi-section invoices.
3821
3822 For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
3823 related display records (L<FS::cust_bill_pkg_display>) and organize 
3824 them into two groups ("early" and "late" according to whether they come 
3825 before or after the total), then into sections.  A subtotal is calculated 
3826 for each section.
3827
3828 Section descriptions are returned in sort weight order.  Each consists 
3829 of a hash containing:
3830
3831 description: the package category name, escaped
3832 subtotal: the total charges in that section
3833 tax_section: a flag indicating that the section contains only tax charges
3834 summarized: same as tax_section, for some reason
3835 sort_weight: the package category's sort weight
3836
3837 If 'condense' is set on the display record, it also contains everything 
3838 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3839 coderefs to generate parts of the invoice.  This is not advised.
3840
3841 Arguments:
3842
3843 LATE: an arrayref to push the "late" section hashes onto.  The "early"
3844 group is simply returned from the method.
3845
3846 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3847 Turning this on has the following effects:
3848 - Ignores display items with the 'summary' flag.
3849 - Combines all items into the "early" group.
3850 - Creates sections for all non-disabled package categories, even if they 
3851 have no charges on this invoice, as well as a section with no name.
3852
3853 ESCAPE: an escape function to use for section titles.
3854
3855 EXTRA_SECTIONS: an arrayref of additional sections to return after the 
3856 sorted list.  If there are any of these, section subtotals exclude 
3857 usage charges.
3858
3859 FORMAT: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
3860 passed through to C<_condense_section()>.
3861
3862 =cut
3863
3864 use vars qw(%pkg_category_cache);
3865 sub _items_sections {
3866   my $self = shift;
3867   my $late = shift;
3868   my $summarypage = shift;
3869   my $escape = shift;
3870   my $extra_sections = shift;
3871   my $format = shift;
3872
3873   my %subtotal = ();
3874   my %late_subtotal = ();
3875   my %not_tax = ();
3876
3877   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3878   {
3879
3880       my $usage = $cust_bill_pkg->usage;
3881
3882       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3883         next if ( $display->summary && $summarypage );
3884
3885         my $section = $display->section;
3886         my $type    = $display->type;
3887
3888         $not_tax{$section} = 1
3889           unless $cust_bill_pkg->pkgnum == 0;
3890
3891         if ( $display->post_total && !$summarypage ) {
3892           if (! $type || $type eq 'S') {
3893             $late_subtotal{$section} += $cust_bill_pkg->setup
3894               if $cust_bill_pkg->setup != 0
3895               || $cust_bill_pkg->setup_show_zero;
3896           }
3897
3898           if (! $type) {
3899             $late_subtotal{$section} += $cust_bill_pkg->recur
3900               if $cust_bill_pkg->recur != 0
3901               || $cust_bill_pkg->recur_show_zero;
3902           }
3903
3904           if ($type && $type eq 'R') {
3905             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3906               if $cust_bill_pkg->recur != 0
3907               || $cust_bill_pkg->recur_show_zero;
3908           }
3909           
3910           if ($type && $type eq 'U') {
3911             $late_subtotal{$section} += $usage
3912               unless scalar(@$extra_sections);
3913           }
3914
3915         } else {
3916
3917           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3918
3919           if (! $type || $type eq 'S') {
3920             $subtotal{$section} += $cust_bill_pkg->setup
3921               if $cust_bill_pkg->setup != 0
3922               || $cust_bill_pkg->setup_show_zero;
3923           }
3924
3925           if (! $type) {
3926             $subtotal{$section} += $cust_bill_pkg->recur
3927               if $cust_bill_pkg->recur != 0
3928               || $cust_bill_pkg->recur_show_zero;
3929           }
3930
3931           if ($type && $type eq 'R') {
3932             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3933               if $cust_bill_pkg->recur != 0
3934               || $cust_bill_pkg->recur_show_zero;
3935           }
3936           
3937           if ($type && $type eq 'U') {
3938             $subtotal{$section} += $usage
3939               unless scalar(@$extra_sections);
3940           }
3941
3942         }
3943
3944       }
3945
3946   }
3947
3948   %pkg_category_cache = ();
3949
3950   push @$late, map { { 'description' => &{$escape}($_),
3951                        'subtotal'    => $late_subtotal{$_},
3952                        'post_total'  => 1,
3953                        'sort_weight' => ( _pkg_category($_)
3954                                             ? _pkg_category($_)->weight
3955                                             : 0
3956                                        ),
3957                        ((_pkg_category($_) && _pkg_category($_)->condense)
3958                                            ? $self->_condense_section($format)
3959                                            : ()
3960                        ),
3961                    } }
3962                  sort _sectionsort keys %late_subtotal;
3963
3964   my @sections;
3965   if ( $summarypage ) {
3966     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3967                 map { $_->categoryname } qsearch('pkg_category', {});
3968     push @sections, '' if exists($subtotal{''});
3969   } else {
3970     @sections = keys %subtotal;
3971   }
3972
3973   my @early = map { { 'description' => &{$escape}($_),
3974                       'subtotal'    => $subtotal{$_},
3975                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3976                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3977                       'sort_weight' => ( _pkg_category($_)
3978                                            ? _pkg_category($_)->weight
3979                                            : 0
3980                                        ),
3981                        ((_pkg_category($_) && _pkg_category($_)->condense)
3982                                            ? $self->_condense_section($format)
3983                                            : ()
3984                        ),
3985                     }
3986                   } @sections;
3987   push @early, @$extra_sections if $extra_sections;
3988
3989   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3990
3991 }
3992
3993 #helper subs for above
3994
3995 sub _sectionsort {
3996   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3997 }
3998
3999 sub _pkg_category {
4000   my $categoryname = shift;
4001   $pkg_category_cache{$categoryname} ||=
4002     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4003 }
4004
4005 my %condensed_format = (
4006   'label' => [ qw( Description Qty Amount ) ],
4007   'fields' => [
4008                 sub { shift->{description} },
4009                 sub { shift->{quantity} },
4010                 sub { my($href, %opt) = @_;
4011                       ($opt{dollar} || ''). $href->{amount};
4012                     },
4013               ],
4014   'align'  => [ qw( l r r ) ],
4015   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
4016   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
4017 );
4018
4019 sub _condense_section {
4020   my ( $self, $format ) = ( shift, shift );
4021   ( 'condensed' => 1,
4022     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4023       qw( description_generator
4024           header_generator
4025           total_generator
4026           total_line_generator
4027         )
4028   );
4029 }
4030
4031 sub _condensed_generator_defaults {
4032   my ( $self, $format ) = ( shift, shift );
4033   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4034 }
4035
4036 my %html_align = (
4037   'c' => 'center',
4038   'l' => 'left',
4039   'r' => 'right',
4040 );
4041
4042 sub _condensed_header_generator {
4043   my ( $self, $format ) = ( shift, shift );
4044
4045   my ( $f, $prefix, $suffix, $separator, $column ) =
4046     _condensed_generator_defaults($format);
4047
4048   if ($format eq 'latex') {
4049     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4050     $suffix = "\\\\\n\\hline";
4051     $separator = "&\n";
4052     $column =
4053       sub { my ($d,$a,$s,$w) = @_;
4054             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4055           };
4056   } elsif ( $format eq 'html' ) {
4057     $prefix = '<th></th>';
4058     $suffix = '';
4059     $separator = '';
4060     $column =
4061       sub { my ($d,$a,$s,$w) = @_;
4062             return qq!<th align="$html_align{$a}">$d</th>!;
4063       };
4064   }
4065
4066   sub {
4067     my @args = @_;
4068     my @result = ();
4069
4070     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4071       push @result,
4072         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4073     }
4074
4075     $prefix. join($separator, @result). $suffix;
4076   };
4077
4078 }
4079
4080 sub _condensed_description_generator {
4081   my ( $self, $format ) = ( shift, shift );
4082
4083   my ( $f, $prefix, $suffix, $separator, $column ) =
4084     _condensed_generator_defaults($format);
4085
4086   my $money_char = '$';
4087   if ($format eq 'latex') {
4088     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4089     $suffix = '\\\\';
4090     $separator = " & \n";
4091     $column =
4092       sub { my ($d,$a,$s,$w) = @_;
4093             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4094           };
4095     $money_char = '\\dollar';
4096   }elsif ( $format eq 'html' ) {
4097     $prefix = '"><td align="center"></td>';
4098     $suffix = '';
4099     $separator = '';
4100     $column =
4101       sub { my ($d,$a,$s,$w) = @_;
4102             return qq!<td align="$html_align{$a}">$d</td>!;
4103       };
4104     #$money_char = $conf->config('money_char') || '$';
4105     $money_char = '';  # this is madness
4106   }
4107
4108   sub {
4109     #my @args = @_;
4110     my $href = shift;
4111     my @result = ();
4112
4113     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4114       my $dollar = '';
4115       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4116       push @result,
4117         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4118                     map { $f->{$_}->[$i] } qw(align span width)
4119                   );
4120     }
4121
4122     $prefix. join( $separator, @result ). $suffix;
4123   };
4124
4125 }
4126
4127 sub _condensed_total_generator {
4128   my ( $self, $format ) = ( shift, shift );
4129
4130   my ( $f, $prefix, $suffix, $separator, $column ) =
4131     _condensed_generator_defaults($format);
4132   my $style = '';
4133
4134   if ($format eq 'latex') {
4135     $prefix = "& ";
4136     $suffix = "\\\\\n";
4137     $separator = " & \n";
4138     $column =
4139       sub { my ($d,$a,$s,$w) = @_;
4140             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4141           };
4142   }elsif ( $format eq 'html' ) {
4143     $prefix = '';
4144     $suffix = '';
4145     $separator = '';
4146     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4147     $column =
4148       sub { my ($d,$a,$s,$w) = @_;
4149             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4150       };
4151   }
4152
4153
4154   sub {
4155     my @args = @_;
4156     my @result = ();
4157
4158     #  my $r = &{$f->{fields}->[$i]}(@args);
4159     #  $r .= ' Total' unless $i;
4160
4161     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4162       push @result,
4163         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4164                     map { $f->{$_}->[$i] } qw(align span width)
4165                   );
4166     }
4167
4168     $prefix. join( $separator, @result ). $suffix;
4169   };
4170
4171 }
4172
4173 =item total_line_generator FORMAT
4174
4175 Returns a coderef used for generation of invoice total line items for this
4176 usage_class.  FORMAT is either html or latex
4177
4178 =cut
4179
4180 # should not be used: will have issues with hash element names (description vs
4181 # total_item and amount vs total_amount -- another array of functions?
4182
4183 sub _condensed_total_line_generator {
4184   my ( $self, $format ) = ( shift, shift );
4185
4186   my ( $f, $prefix, $suffix, $separator, $column ) =
4187     _condensed_generator_defaults($format);
4188   my $style = '';
4189
4190   if ($format eq 'latex') {
4191     $prefix = "& ";
4192     $suffix = "\\\\\n";
4193     $separator = " & \n";
4194     $column =
4195       sub { my ($d,$a,$s,$w) = @_;
4196             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4197           };
4198   }elsif ( $format eq 'html' ) {
4199     $prefix = '';
4200     $suffix = '';
4201     $separator = '';
4202     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4203     $column =
4204       sub { my ($d,$a,$s,$w) = @_;
4205             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4206       };
4207   }
4208
4209
4210   sub {
4211     my @args = @_;
4212     my @result = ();
4213
4214     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
4215       push @result,
4216         &{$column}( &{$f->{fields}->[$i]}(@args),
4217                     map { $f->{$_}->[$i] } qw(align span width)
4218                   );
4219     }
4220
4221     $prefix. join( $separator, @result ). $suffix;
4222   };
4223
4224 }
4225
4226 #sub _items_extra_usage_sections {
4227 #  my $self = shift;
4228 #  my $escape = shift;
4229 #
4230 #  my %sections = ();
4231 #
4232 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
4233 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4234 #  {
4235 #    next unless $cust_bill_pkg->pkgnum > 0;
4236 #
4237 #    foreach my $section ( keys %usage_class ) {
4238 #
4239 #      my $usage = $cust_bill_pkg->usage($section);
4240 #
4241 #      next unless $usage && $usage > 0;
4242 #
4243 #      $sections{$section} ||= 0;
4244 #      $sections{$section} += $usage;
4245 #
4246 #    }
4247 #
4248 #  }
4249 #
4250 #  map { { 'description' => &{$escape}($_),
4251 #          'subtotal'    => $sections{$_},
4252 #          'summarized'  => '',
4253 #          'tax_section' => '',
4254 #        }
4255 #      }
4256 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4257 #
4258 #}
4259
4260 sub _items_extra_usage_sections {
4261   my $self = shift;
4262   my $conf = $self->conf;
4263   my $escape = shift;
4264   my $format = shift;
4265
4266   my %sections = ();
4267   my %classnums = ();
4268   my %lines = ();
4269
4270   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4271
4272   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4273   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4274     next unless $cust_bill_pkg->pkgnum > 0;
4275
4276     foreach my $classnum ( keys %usage_class ) {
4277       my $section = $usage_class{$classnum}->classname;
4278       $classnums{$section} = $classnum;
4279
4280       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4281         my $amount = $detail->amount;
4282         next unless $amount && $amount > 0;
4283  
4284         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4285         $sections{$section}{amount} += $amount;  #subtotal
4286         $sections{$section}{calls}++;
4287         $sections{$section}{duration} += $detail->duration;
4288
4289         my $desc = $detail->regionname; 
4290         my $description = $desc;
4291         $description = substr($desc, 0, $maxlength). '...'
4292           if $format eq 'latex' && length($desc) > $maxlength;
4293
4294         $lines{$section}{$desc} ||= {
4295           description     => &{$escape}($description),
4296           #pkgpart         => $part_pkg->pkgpart,
4297           pkgnum          => $cust_bill_pkg->pkgnum,
4298           ref             => '',
4299           amount          => 0,
4300           calls           => 0,
4301           duration        => 0,
4302           #unit_amount     => $cust_bill_pkg->unitrecur,
4303           quantity        => $cust_bill_pkg->quantity,
4304           product_code    => 'N/A',
4305           ext_description => [],
4306         };
4307
4308         $lines{$section}{$desc}{amount} += $amount;
4309         $lines{$section}{$desc}{calls}++;
4310         $lines{$section}{$desc}{duration} += $detail->duration;
4311
4312       }
4313     }
4314   }
4315
4316   my %sectionmap = ();
4317   foreach (keys %sections) {
4318     my $usage_class = $usage_class{$classnums{$_}};
4319     $sectionmap{$_} = { 'description' => &{$escape}($_),
4320                         'amount'    => $sections{$_}{amount},    #subtotal
4321                         'calls'       => $sections{$_}{calls},
4322                         'duration'    => $sections{$_}{duration},
4323                         'summarized'  => '',
4324                         'tax_section' => '',
4325                         'sort_weight' => $usage_class->weight,
4326                         ( $usage_class->format
4327                           ? ( map { $_ => $usage_class->$_($format) }
4328                               qw( description_generator header_generator total_generator total_line_generator )
4329                             )
4330                           : ()
4331                         ), 
4332                       };
4333   }
4334
4335   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4336                  values %sectionmap;
4337
4338   my @lines = ();
4339   foreach my $section ( keys %lines ) {
4340     foreach my $line ( keys %{$lines{$section}} ) {
4341       my $l = $lines{$section}{$line};
4342       $l->{section}     = $sectionmap{$section};
4343       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4344       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4345       push @lines, $l;
4346     }
4347   }
4348
4349   return(\@sections, \@lines);
4350
4351 }
4352
4353 sub _did_summary {
4354     my $self = shift;
4355     my $end = $self->_date;
4356
4357     # start at date of previous invoice + 1 second or 0 if no previous invoice
4358     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4359     $start = 0 if !$start;
4360     $start++;
4361
4362     my $cust_main = $self->cust_main;
4363     my @pkgs = $cust_main->all_pkgs;
4364     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4365         = (0,0,0,0,0);
4366     my @seen = ();
4367     foreach my $pkg ( @pkgs ) {
4368         my @h_cust_svc = $pkg->h_cust_svc($end);
4369         foreach my $h_cust_svc ( @h_cust_svc ) {
4370             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4371             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4372
4373             my $inserted = $h_cust_svc->date_inserted;
4374             my $deleted = $h_cust_svc->date_deleted;
4375             my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4376             my $phone_deleted;
4377             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
4378             
4379 # DID either activated or ported in; cannot be both for same DID simultaneously
4380             if ($inserted >= $start && $inserted <= $end && $phone_inserted
4381                 && (!$phone_inserted->lnp_status 
4382                     || $phone_inserted->lnp_status eq ''
4383                     || $phone_inserted->lnp_status eq 'native')) {
4384                 $num_activated++;
4385             }
4386             else { # this one not so clean, should probably move to (h_)svc_phone
4387                  my $phone_portedin = qsearchs( 'h_svc_phone',
4388                       { 'svcnum' => $h_cust_svc->svcnum, 
4389                         'lnp_status' => 'portedin' },  
4390                       FS::h_svc_phone->sql_h_searchs($end),  
4391                     );
4392                  $num_portedin++ if $phone_portedin;
4393             }
4394
4395 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4396             if($deleted >= $start && $deleted <= $end && $phone_deleted
4397                 && (!$phone_deleted->lnp_status 
4398                     || $phone_deleted->lnp_status ne 'portingout')) {
4399                 $num_deactivated++;
4400             } 
4401             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
4402                 && $phone_deleted->lnp_status 
4403                 && $phone_deleted->lnp_status eq 'portingout') {
4404                 $num_portedout++;
4405             }
4406
4407             # increment usage minutes
4408         if ( $phone_inserted ) {
4409             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4410             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4411         }
4412         else {
4413             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4414         }
4415
4416             # don't look at this service again
4417             push @seen, $h_cust_svc->svcnum;
4418         }
4419     }
4420
4421     $minutes = sprintf("%d", $minutes);
4422     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
4423         . "$num_deactivated  Ported-Out: $num_portedout ",
4424             "Total Minutes: $minutes");
4425 }
4426
4427 sub _items_accountcode_cdr {
4428     my $self = shift;
4429     my $escape = shift;
4430     my $format = shift;
4431
4432     my $section = { 'amount'        => 0,
4433                     'calls'         => 0,
4434                     'duration'      => 0,
4435                     'sort_weight'   => '',
4436                     'phonenum'      => '',
4437                     'description'   => 'Usage by Account Code',
4438                     'post_total'    => '',
4439                     'summarized'    => '',
4440                     'header'        => '',
4441                   };
4442     my @lines;
4443     my %accountcodes = ();
4444
4445     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4446         next unless $cust_bill_pkg->pkgnum > 0;
4447
4448         my @header = $cust_bill_pkg->details_header;
4449         next unless scalar(@header);
4450         $section->{'header'} = join(',',@header);
4451
4452         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4453
4454             $section->{'header'} = $detail->formatted('format' => $format)
4455                 if($detail->detail eq $section->{'header'}); 
4456       
4457             my $accountcode = $detail->accountcode;
4458             next unless $accountcode;
4459
4460             my $amount = $detail->amount;
4461             next unless $amount && $amount > 0;
4462
4463             $accountcodes{$accountcode} ||= {
4464                     description => $accountcode,
4465                     pkgnum      => '',
4466                     ref         => '',
4467                     amount      => 0,
4468                     calls       => 0,
4469                     duration    => 0,
4470                     quantity    => '',
4471                     product_code => 'N/A',
4472                     section     => $section,
4473                     ext_description => [ $section->{'header'} ],
4474                     detail_temp => [],
4475             };
4476
4477             $section->{'amount'} += $amount;
4478             $accountcodes{$accountcode}{'amount'} += $amount;
4479             $accountcodes{$accountcode}{calls}++;
4480             $accountcodes{$accountcode}{duration} += $detail->duration;
4481             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4482         }
4483     }
4484
4485     foreach my $l ( values %accountcodes ) {
4486         $l->{amount} = sprintf( "%.2f", $l->{amount} );
4487         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4488         foreach my $sorted_detail ( @sorted_detail ) {
4489             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4490         }
4491         delete $l->{detail_temp};
4492         push @lines, $l;
4493     }
4494
4495     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4496
4497     return ($section,\@sorted_lines);
4498 }
4499
4500 sub _items_svc_phone_sections {
4501   my $self = shift;
4502   my $conf = $self->conf;
4503   my $escape = shift;
4504   my $format = shift;
4505
4506   my %sections = ();
4507   my %classnums = ();
4508   my %lines = ();
4509
4510   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4511
4512   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4513   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4514
4515   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4516     next unless $cust_bill_pkg->pkgnum > 0;
4517
4518     my @header = $cust_bill_pkg->details_header;
4519     next unless scalar(@header);
4520
4521     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4522
4523       my $phonenum = $detail->phonenum;
4524       next unless $phonenum;
4525
4526       my $amount = $detail->amount;
4527       next unless $amount && $amount > 0;
4528
4529       $sections{$phonenum} ||= { 'amount'      => 0,
4530                                  'calls'       => 0,
4531                                  'duration'    => 0,
4532                                  'sort_weight' => -1,
4533                                  'phonenum'    => $phonenum,
4534                                 };
4535       $sections{$phonenum}{amount} += $amount;  #subtotal
4536       $sections{$phonenum}{calls}++;
4537       $sections{$phonenum}{duration} += $detail->duration;
4538
4539       my $desc = $detail->regionname; 
4540       my $description = $desc;
4541       $description = substr($desc, 0, $maxlength). '...'
4542         if $format eq 'latex' && length($desc) > $maxlength;
4543
4544       $lines{$phonenum}{$desc} ||= {
4545         description     => &{$escape}($description),
4546         #pkgpart         => $part_pkg->pkgpart,
4547         pkgnum          => '',
4548         ref             => '',
4549         amount          => 0,
4550         calls           => 0,
4551         duration        => 0,
4552         #unit_amount     => '',
4553         quantity        => '',
4554         product_code    => 'N/A',
4555         ext_description => [],
4556       };
4557
4558       $lines{$phonenum}{$desc}{amount} += $amount;
4559       $lines{$phonenum}{$desc}{calls}++;
4560       $lines{$phonenum}{$desc}{duration} += $detail->duration;
4561
4562       my $line = $usage_class{$detail->classnum}->classname;
4563       $sections{"$phonenum $line"} ||=
4564         { 'amount' => 0,
4565           'calls' => 0,
4566           'duration' => 0,
4567           'sort_weight' => $usage_class{$detail->classnum}->weight,
4568           'phonenum' => $phonenum,
4569           'header'  => [ @header ],
4570         };
4571       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
4572       $sections{"$phonenum $line"}{calls}++;
4573       $sections{"$phonenum $line"}{duration} += $detail->duration;
4574
4575       $lines{"$phonenum $line"}{$desc} ||= {
4576         description     => &{$escape}($description),
4577         #pkgpart         => $part_pkg->pkgpart,
4578         pkgnum          => '',
4579         ref             => '',
4580         amount          => 0,
4581         calls           => 0,
4582         duration        => 0,
4583         #unit_amount     => '',
4584         quantity        => '',
4585         product_code    => 'N/A',
4586         ext_description => [],
4587       };
4588
4589       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4590       $lines{"$phonenum $line"}{$desc}{calls}++;
4591       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4592       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4593            $detail->formatted('format' => $format);
4594
4595     }
4596   }
4597
4598   my %sectionmap = ();
4599   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4600   foreach ( keys %sections ) {
4601     my @header = @{ $sections{$_}{header} || [] };
4602     my $usage_simple =
4603       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4604     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4605     my $usage_class = $summary ? $simple : $usage_simple;
4606     my $ending = $summary ? ' usage charges' : '';
4607     my %gen_opt = ();
4608     unless ($summary) {
4609       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4610     }
4611     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4612                         'amount'    => $sections{$_}{amount},    #subtotal
4613                         'calls'       => $sections{$_}{calls},
4614                         'duration'    => $sections{$_}{duration},
4615                         'summarized'  => '',
4616                         'tax_section' => '',
4617                         'phonenum'    => $sections{$_}{phonenum},
4618                         'sort_weight' => $sections{$_}{sort_weight},
4619                         'post_total'  => $summary, #inspire pagebreak
4620                         (
4621                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4622                             qw( description_generator
4623                                 header_generator
4624                                 total_generator
4625                                 total_line_generator
4626                               )
4627                           )
4628                         ), 
4629                       };
4630   }
4631
4632   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4633                         $a->{sort_weight} <=> $b->{sort_weight}
4634                       }
4635                  values %sectionmap;
4636
4637   my @lines = ();
4638   foreach my $section ( keys %lines ) {
4639     foreach my $line ( keys %{$lines{$section}} ) {
4640       my $l = $lines{$section}{$line};
4641       $l->{section}     = $sectionmap{$section};
4642       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4643       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4644       push @lines, $l;
4645     }
4646   }
4647   
4648   if($conf->exists('phone_usage_class_summary')) { 
4649       # this only works with Latex
4650       my @newlines;
4651       my @newsections;
4652
4653       # after this, we'll have only two sections per DID:
4654       # Calls Summary and Calls Detail
4655       foreach my $section ( @sections ) {
4656         if($section->{'post_total'}) {
4657             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4658             $section->{'total_line_generator'} = sub { '' };
4659             $section->{'total_generator'} = sub { '' };
4660             $section->{'header_generator'} = sub { '' };
4661             $section->{'description_generator'} = '';
4662             push @newsections, $section;
4663             my %calls_detail = %$section;
4664             $calls_detail{'post_total'} = '';
4665             $calls_detail{'sort_weight'} = '';
4666             $calls_detail{'description_generator'} = sub { '' };
4667             $calls_detail{'header_generator'} = sub {
4668                 return ' & Date/Time & Called Number & Duration & Price'
4669                     if $format eq 'latex';
4670                 '';
4671             };
4672             $calls_detail{'description'} = 'Calls Detail: '
4673                                                     . $section->{'phonenum'};
4674             push @newsections, \%calls_detail;  
4675         }
4676       }
4677
4678       # after this, each usage class is collapsed/summarized into a single
4679       # line under the Calls Summary section
4680       foreach my $newsection ( @newsections ) {
4681         if($newsection->{'post_total'}) { # this means Calls Summary
4682             foreach my $section ( @sections ) {
4683                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
4684                                 && !$section->{'post_total'});
4685                 my $newdesc = $section->{'description'};
4686                 my $tn = $section->{'phonenum'};
4687                 $newdesc =~ s/$tn//g;
4688                 my $line = {  ext_description => [],
4689                               pkgnum => '',
4690                               ref => '',
4691                               quantity => '',
4692                               calls => $section->{'calls'},
4693                               section => $newsection,
4694                               duration => $section->{'duration'},
4695                               description => $newdesc,
4696                               amount => sprintf("%.2f",$section->{'amount'}),
4697                               product_code => 'N/A',
4698                             };
4699                 push @newlines, $line;
4700             }
4701         }
4702       }
4703
4704       # after this, Calls Details is populated with all CDRs
4705       foreach my $newsection ( @newsections ) {
4706         if(!$newsection->{'post_total'}) { # this means Calls Details
4707             foreach my $line ( @lines ) {
4708                 next unless (scalar(@{$line->{'ext_description'}}) &&
4709                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4710                             );
4711                 my @extdesc = @{$line->{'ext_description'}};
4712                 my @newextdesc;
4713                 foreach my $extdesc ( @extdesc ) {
4714                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4715                     push @newextdesc, $extdesc;
4716                 }
4717                 $line->{'ext_description'} = \@newextdesc;
4718                 $line->{'section'} = $newsection;
4719                 push @newlines, $line;
4720             }
4721         }
4722       }
4723
4724       return(\@newsections, \@newlines);
4725   }
4726
4727   return(\@sections, \@lines);
4728
4729 }
4730
4731 sub _items { # seems to be unused
4732   my $self = shift;
4733
4734   #my @display = scalar(@_)
4735   #              ? @_
4736   #              : qw( _items_previous _items_pkg );
4737   #              #: qw( _items_pkg );
4738   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4739   my @display = qw( _items_previous _items_pkg );
4740
4741   my @b = ();
4742   foreach my $display ( @display ) {
4743     push @b, $self->$display(@_);
4744   }
4745   @b;
4746 }
4747
4748 sub _items_previous {
4749   my $self = shift;
4750   my $conf = $self->conf;
4751   my $cust_main = $self->cust_main;
4752   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4753   my @b = ();
4754   foreach ( @pr_cust_bill ) {
4755     my $date = $conf->exists('invoice_show_prior_due_date')
4756                ? 'due '. $_->due_date2str($date_format)
4757                : time2str($date_format, $_->_date);
4758     push @b, {
4759       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4760       #'pkgpart'     => 'N/A',
4761       'pkgnum'      => 'N/A',
4762       'amount'      => sprintf("%.2f", $_->owed),
4763     };
4764   }
4765   @b;
4766
4767   #{
4768   #    'description'     => 'Previous Balance',
4769   #    #'pkgpart'         => 'N/A',
4770   #    'pkgnum'          => 'N/A',
4771   #    'amount'          => sprintf("%10.2f", $pr_total ),
4772   #    'ext_description' => [ map {
4773   #                                 "Invoice ". $_->invnum.
4774   #                                 " (". time2str("%x",$_->_date). ") ".
4775   #                                 sprintf("%10.2f", $_->owed)
4776   #                         } @pr_cust_bill ],
4777
4778   #};
4779 }
4780
4781 =item _items_pkg [ OPTIONS ]
4782
4783 Return line item hashes for each package item on this invoice. Nearly 
4784 equivalent to 
4785
4786 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4787
4788 The only OPTIONS accepted is 'section', which may point to a hashref 
4789 with a key named 'condensed', which may have a true value.  If it 
4790 does, this method tries to merge identical items into items with 
4791 'quantity' equal to the number of items (not the sum of their 
4792 separate quantities, for some reason).
4793
4794 =cut
4795
4796 sub _items_pkg {
4797   my $self = shift;
4798   my %options = @_;
4799
4800   warn "$me _items_pkg searching for all package line items\n"
4801     if $DEBUG > 1;
4802
4803   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4804
4805   warn "$me _items_pkg filtering line items\n"
4806     if $DEBUG > 1;
4807   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4808
4809   if ($options{section} && $options{section}->{condensed}) {
4810
4811     warn "$me _items_pkg condensing section\n"
4812       if $DEBUG > 1;
4813
4814     my %itemshash = ();
4815     local $Storable::canonical = 1;
4816     foreach ( @items ) {
4817       my $item = { %$_ };
4818       delete $item->{ref};
4819       delete $item->{ext_description};
4820       my $key = freeze($item);
4821       $itemshash{$key} ||= 0;
4822       $itemshash{$key} ++; # += $item->{quantity};
4823     }
4824     @items = sort { $a->{description} cmp $b->{description} }
4825              map { my $i = thaw($_);
4826                    $i->{quantity} = $itemshash{$_};
4827                    $i->{amount} =
4828                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4829                    $i;
4830                  }
4831              keys %itemshash;
4832   }
4833
4834   warn "$me _items_pkg returning ". scalar(@items). " items\n"
4835     if $DEBUG > 1;
4836
4837   @items;
4838 }
4839
4840 sub _taxsort {
4841   return 0 unless $a->itemdesc cmp $b->itemdesc;
4842   return -1 if $b->itemdesc eq 'Tax';
4843   return 1 if $a->itemdesc eq 'Tax';
4844   return -1 if $b->itemdesc eq 'Other surcharges';
4845   return 1 if $a->itemdesc eq 'Other surcharges';
4846   $a->itemdesc cmp $b->itemdesc;
4847 }
4848
4849 sub _items_tax {
4850   my $self = shift;
4851   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4852   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4853 }
4854
4855 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4856
4857 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4858 list of hashrefs describing the line items they generate on the invoice.
4859
4860 OPTIONS may include:
4861
4862 format: the invoice format.
4863
4864 escape_function: the function used to escape strings.
4865
4866 DEPRECATED? (expensive, mostly unused?)
4867 format_function: the function used to format CDRs.
4868
4869 section: a hashref containing 'description'; if this is present, 
4870 cust_bill_pkg_display records not belonging to this section are 
4871 ignored.
4872
4873 multisection: a flag indicating that this is a multisection invoice,
4874 which does something complicated.
4875
4876 multilocation: a flag to display the location label for the package.
4877
4878 Returns a list of hashrefs, each of which may contain:
4879
4880 pkgnum, description, amount, unit_amount, quantity, _is_setup, and 
4881 ext_description, which is an arrayref of detail lines to show below 
4882 the package line.
4883
4884 =cut
4885
4886 sub _items_cust_bill_pkg {
4887   my $self = shift;
4888   my $conf = $self->conf;
4889   my $cust_bill_pkgs = shift;
4890   my %opt = @_;
4891
4892   my $format = $opt{format} || '';
4893   my $escape_function = $opt{escape_function} || sub { shift };
4894   my $format_function = $opt{format_function} || '';
4895   my $no_usage = $opt{no_usage} || '';
4896   my $unsquelched = $opt{unsquelched} || ''; #unused
4897   my $section = $opt{section}->{description} if $opt{section};
4898   my $summary_page = $opt{summary_page} || ''; #unused
4899   my $multilocation = $opt{multilocation} || '';
4900   my $multisection = $opt{multisection} || '';
4901   my $discount_show_always = 0;
4902
4903   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4904
4905   my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4906
4907   my @b = ();
4908   my ($s, $r, $u) = ( undef, undef, undef );
4909   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4910   {
4911
4912     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4913       if ( $_ && !$cust_bill_pkg->hidden ) {
4914         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4915         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4916         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4917         push @b, { %$_ }
4918           if $_->{amount} != 0
4919           || $discount_show_always
4920           || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4921           || (   $_->{_is_setup} && $_->{setup_show_zero} )
4922         ;
4923         $_ = undef;
4924       }
4925     }
4926
4927     my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4928
4929     warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4930          $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4931       if $DEBUG > 1;
4932
4933     foreach my $display ( grep { defined($section)
4934                                  ? $_->section eq $section
4935                                  : 1
4936                                }
4937                           #grep { !$_->summary || !$summary_page } # bunk!
4938                           grep { !$_->summary || $multisection }
4939                           @cust_bill_pkg_display
4940                         )
4941     {
4942
4943       warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4944            $display->billpkgdisplaynum. "\n"
4945         if $DEBUG > 1;
4946
4947       my $type = $display->type;
4948
4949       my $desc = $cust_bill_pkg->desc;
4950       $desc = substr($desc, 0, $maxlength). '...'
4951         if $format eq 'latex' && length($desc) > $maxlength;
4952
4953       my %details_opt = ( 'format'          => $format,
4954                           'escape_function' => $escape_function,
4955                           'format_function' => $format_function,
4956                           'no_usage'        => $opt{'no_usage'},
4957                         );
4958
4959       if ( $cust_bill_pkg->pkgnum > 0 ) {
4960
4961         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4962           if $DEBUG > 1;
4963  
4964         my $cust_pkg = $cust_bill_pkg->cust_pkg;
4965
4966         # start/end dates for invoice formats that do nonstandard 
4967         # things with them
4968         my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4969
4970         if (    (!$type || $type eq 'S')
4971              && (    $cust_bill_pkg->setup != 0
4972                   || $cust_bill_pkg->setup_show_zero
4973                 )
4974            )
4975          {
4976
4977           warn "$me _items_cust_bill_pkg adding setup\n"
4978             if $DEBUG > 1;
4979
4980           my $description = $desc;
4981           $description .= ' Setup'
4982             if $cust_bill_pkg->recur != 0
4983             || $discount_show_always
4984             || $cust_bill_pkg->recur_show_zero;
4985
4986           my @d = ();
4987           unless ( $cust_pkg->part_pkg->hide_svc_detail
4988                 || $cust_bill_pkg->hidden )
4989           {
4990
4991             push @d, map &{$escape_function}($_),
4992                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
4993               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4994
4995             if ( $multilocation ) {
4996               my $loc = $cust_pkg->location_label;
4997               $loc = substr($loc, 0, $maxlength). '...'
4998                 if $format eq 'latex' && length($loc) > $maxlength;
4999               push @d, &{$escape_function}($loc);
5000             }
5001
5002           } #unless hiding service details
5003
5004           push @d, $cust_bill_pkg->details(%details_opt)
5005             if $cust_bill_pkg->recur == 0;
5006
5007           if ( $cust_bill_pkg->hidden ) {
5008             $s->{amount}      += $cust_bill_pkg->setup;
5009             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5010             push @{ $s->{ext_description} }, @d;
5011           } else {
5012             $s = {
5013               _is_setup       => 1,
5014               description     => $description,
5015               #pkgpart         => $part_pkg->pkgpart,
5016               pkgnum          => $cust_bill_pkg->pkgnum,
5017               amount          => $cust_bill_pkg->setup,
5018               setup_show_zero => $cust_bill_pkg->setup_show_zero,
5019               unit_amount     => $cust_bill_pkg->unitsetup,
5020               quantity        => $cust_bill_pkg->quantity,
5021               ext_description => \@d,
5022             };
5023           };
5024
5025         }
5026
5027         if (    ( !$type || $type eq 'R' || $type eq 'U' )
5028              && (
5029                      $cust_bill_pkg->recur != 0
5030                   || $cust_bill_pkg->setup == 0
5031                   || $discount_show_always
5032                   || $cust_bill_pkg->recur_show_zero
5033                 )
5034            )
5035         {
5036
5037           warn "$me _items_cust_bill_pkg adding recur/usage\n"
5038             if $DEBUG > 1;
5039
5040           my $is_summary = $display->summary;
5041           my $description = ($is_summary && $type && $type eq 'U')
5042                             ? "Usage charges" : $desc;
5043
5044           #pry be a bit more efficient to look some of this conf stuff up
5045           # outside the loop
5046           unless (
5047             $conf->exists('disable_line_item_date_ranges')
5048               || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
5049           ) {
5050             my $time_period;
5051             my $date_style = $conf->config( 'cust_bill-line_item-date_style',
5052                                             $cust_main->agentnum
5053                                           );
5054             if ( defined($date_style) && $date_style eq 'month_of' ) {
5055               $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5056             } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5057               my $desc = $conf->config( 'cust_bill-line_item-date_description',
5058                                          $cust_main->agentnum
5059                                       );
5060               $desc .= ' ' unless $desc =~ /\s$/;
5061               $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5062             } else {
5063               $time_period =      time2str($date_format, $cust_bill_pkg->sdate).
5064                            " - ". time2str($date_format, $cust_bill_pkg->edate);
5065             }
5066             $description .= " ($time_period)";
5067           }
5068
5069           my @d = ();
5070           my @seconds = (); # for display of usage info
5071
5072           #at least until cust_bill_pkg has "past" ranges in addition to
5073           #the "future" sdate/edate ones... see #3032
5074           my @dates = ( $self->_date );
5075           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5076           push @dates, $prev->sdate if $prev;
5077           push @dates, undef if !$prev;
5078
5079           unless ( $cust_pkg->part_pkg->hide_svc_detail
5080                 || $cust_bill_pkg->itemdesc
5081                 || $cust_bill_pkg->hidden
5082                 || $is_summary && $type && $type eq 'U' )
5083           {
5084
5085             warn "$me _items_cust_bill_pkg adding service details\n"
5086               if $DEBUG > 1;
5087
5088             push @d, map &{$escape_function}($_),
5089                          $cust_pkg->h_labels_short(@dates, 'I')
5090                                                    #$cust_bill_pkg->edate,
5091                                                    #$cust_bill_pkg->sdate)
5092               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5093
5094             warn "$me _items_cust_bill_pkg done adding service details\n"
5095               if $DEBUG > 1;
5096
5097             if ( $multilocation ) {
5098               my $loc = $cust_pkg->location_label;
5099               $loc = substr($loc, 0, $maxlength). '...'
5100                 if $format eq 'latex' && length($loc) > $maxlength;
5101               push @d, &{$escape_function}($loc);
5102             }
5103
5104             # Display of seconds_since_sqlradacct:
5105             # On the invoice, when processing @detail_items, look for a field
5106             # named 'seconds'.  This will contain total seconds for each 
5107             # service, in the same order as @ext_description.  For services 
5108             # that don't support this it will show undef.
5109             if ( $conf->exists('svc_acct-usage_seconds') 
5110                  and ! $cust_bill_pkg->pkgpart_override ) {
5111               foreach my $cust_svc ( 
5112                   $cust_pkg->h_cust_svc(@dates, 'I') 
5113                 ) {
5114
5115                 # eval because not having any part_export_usage exports 
5116                 # is a fatal error, last_bill/_date because that's how 
5117                 # sqlradius_hour billing does it
5118                 my $sec = eval {
5119                   $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5120                 };
5121                 push @seconds, $sec;
5122               }
5123             } #if svc_acct-usage_seconds
5124
5125           }
5126
5127           unless ( $is_summary ) {
5128             warn "$me _items_cust_bill_pkg adding details\n"
5129               if $DEBUG > 1;
5130
5131             #instead of omitting details entirely in this case (unwanted side
5132             # effects), just omit CDRs
5133             $details_opt{'no_usage'} = 1
5134               if $type && $type eq 'R';
5135
5136             push @d, $cust_bill_pkg->details(%details_opt);
5137           }
5138
5139           warn "$me _items_cust_bill_pkg calculating amount\n"
5140             if $DEBUG > 1;
5141   
5142           my $amount = 0;
5143           if (!$type) {
5144             $amount = $cust_bill_pkg->recur;
5145           } elsif ($type eq 'R') {
5146             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5147           } elsif ($type eq 'U') {
5148             $amount = $cust_bill_pkg->usage;
5149           }
5150   
5151           if ( !$type || $type eq 'R' ) {
5152
5153             warn "$me _items_cust_bill_pkg adding recur\n"
5154               if $DEBUG > 1;
5155
5156             if ( $cust_bill_pkg->hidden ) {
5157               $r->{amount}      += $amount;
5158               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5159               push @{ $r->{ext_description} }, @d;
5160             } else {
5161               $r = {
5162                 description     => $description,
5163                 #pkgpart         => $part_pkg->pkgpart,
5164                 pkgnum          => $cust_bill_pkg->pkgnum,
5165                 amount          => $amount,
5166                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5167                 unit_amount     => $cust_bill_pkg->unitrecur,
5168                 quantity        => $cust_bill_pkg->quantity,
5169                 %item_dates,
5170                 ext_description => \@d,
5171               };
5172               $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5173             }
5174
5175           } else {  # $type eq 'U'
5176
5177             warn "$me _items_cust_bill_pkg adding usage\n"
5178               if $DEBUG > 1;
5179
5180             if ( $cust_bill_pkg->hidden ) {
5181               $u->{amount}      += $amount;
5182               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5183               push @{ $u->{ext_description} }, @d;
5184             } else {
5185               $u = {
5186                 description     => $description,
5187                 #pkgpart         => $part_pkg->pkgpart,
5188                 pkgnum          => $cust_bill_pkg->pkgnum,
5189                 amount          => $amount,
5190                 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5191                 unit_amount     => $cust_bill_pkg->unitrecur,
5192                 quantity        => $cust_bill_pkg->quantity,
5193                 %item_dates,
5194                 ext_description => \@d,
5195               };
5196             }
5197           }
5198
5199         } # recurring or usage with recurring charge
5200
5201       } else { #pkgnum tax or one-shot line item (??)
5202
5203         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5204           if $DEBUG > 1;
5205
5206         if ( $cust_bill_pkg->setup != 0 ) {
5207           push @b, {
5208             'description' => $desc,
5209             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
5210           };
5211         }
5212         if ( $cust_bill_pkg->recur != 0 ) {
5213           push @b, {
5214             'description' => "$desc (".
5215                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5216                              time2str($date_format, $cust_bill_pkg->edate). ')',
5217             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
5218           };
5219         }
5220
5221       }
5222
5223     }
5224
5225     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5226                                 && $conf->exists('discount-show-always'));
5227
5228   }
5229
5230   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5231     if ( $_  ) {
5232       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
5233       $_->{amount}      =~ s/^\-0\.00$/0.00/;
5234       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5235       push @b, { %$_ }
5236         if $_->{amount} != 0
5237         || $discount_show_always
5238         || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5239         || (   $_->{_is_setup} && $_->{setup_show_zero} )
5240     }
5241   }
5242
5243   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5244     if $DEBUG > 1;
5245
5246   @b;
5247
5248 }
5249
5250 sub _items_credits {
5251   my( $self, %opt ) = @_;
5252   my $trim_len = $opt{'trim_len'} || 60;
5253
5254   my @b;
5255   #credits
5256   foreach ( $self->cust_credited ) {
5257
5258     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5259
5260     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5261     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5262     $reason = " ($reason) " if $reason;
5263
5264     push @b, {
5265       #'description' => 'Credit ref\#'. $_->crednum.
5266       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
5267       #                 $reason,
5268       'description' => $self->mt('Credit applied').' '.
5269                        time2str($date_format,$_->cust_credit->_date). $reason,
5270       'amount'      => sprintf("%.2f",$_->amount),
5271     };
5272   }
5273
5274   @b;
5275
5276 }
5277
5278 sub _items_payments {
5279   my $self = shift;
5280
5281   my @b;
5282   #get & print payments
5283   foreach ( $self->cust_bill_pay ) {
5284
5285     #something more elaborate if $_->amount ne ->cust_pay->paid ?
5286
5287     push @b, {
5288       'description' => $self->mt('Payment received').' '.
5289                        time2str($date_format,$_->cust_pay->_date ),
5290       'amount'      => sprintf("%.2f", $_->amount )
5291     };
5292   }
5293
5294   @b;
5295
5296 }
5297
5298 =item _items_discounts_avail
5299
5300 Returns an array of line item hashrefs representing available term discounts
5301 for this invoice.  This makes the same assumptions that apply to term 
5302 discounts in general: that the package is billed monthly, at a flat rate, 
5303 with no usage charges.  A prorated first month will be handled, as will 
5304 a setup fee if the discount is allowed to apply to setup fees.
5305
5306 =cut
5307
5308 sub _items_discounts_avail {
5309   my $self = shift;
5310   my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5311
5312   my %plans = $self->discount_plans;
5313
5314   $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5315
5316   map {
5317     my $months = $_;
5318     my $plan = $plans{$months};
5319
5320     my $term_total = sprintf('%.2f', $plan->discounted_total);
5321     my $percent = sprintf('%.0f', 
5322                           100 * (1 - $term_total / $plan->base_total) );
5323     my $permonth = sprintf('%.2f', $term_total / $months);
5324     my $detail = $self->mt('discount on item'). ' '.
5325                  join(', ', map { "#$_" } $plan->pkgnums)
5326       if $list_pkgnums;
5327
5328     # discounts for non-integer months don't work anyway
5329     $months = sprintf("%d", $months);
5330
5331     +{
5332       description => $self->mt('Save [_1]% by paying for [_2] months',
5333                                 $percent, $months),
5334       amount      => $self->mt('[_1] ([_2] per month)', 
5335                                 $term_total, $money_char.$permonth),
5336       ext_description => ($detail || ''),
5337     }
5338   } #map
5339   sort { $b <=> $a } keys %plans;
5340
5341 }
5342
5343 =item call_details [ OPTION => VALUE ... ]
5344
5345 Returns an array of CSV strings representing the call details for this invoice
5346 The only option available is the boolean prepend_billed_number
5347
5348 =cut
5349
5350 sub call_details {
5351   my ($self, %opt) = @_;
5352
5353   my $format_function = sub { shift };
5354
5355   if ($opt{prepend_billed_number}) {
5356     $format_function = sub {
5357       my $detail = shift;
5358       my $row = shift;
5359
5360       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5361       
5362     };
5363   }
5364
5365   my @details = map { $_->details( 'format_function' => $format_function,
5366                                    'escape_function' => sub{ return() },
5367                                  )
5368                     }
5369                   grep { $_->pkgnum }
5370                   $self->cust_bill_pkg;
5371   my $header = $details[0];
5372   ( $header, grep { $_ ne $header } @details );
5373 }
5374
5375
5376 =back
5377
5378 =head1 SUBROUTINES
5379
5380 =over 4
5381
5382 =item process_reprint
5383
5384 =cut
5385
5386 sub process_reprint {
5387   process_re_X('print', @_);
5388 }
5389
5390 =item process_reemail
5391
5392 =cut
5393
5394 sub process_reemail {
5395   process_re_X('email', @_);
5396 }
5397
5398 =item process_refax
5399
5400 =cut
5401
5402 sub process_refax {
5403   process_re_X('fax', @_);
5404 }
5405
5406 =item process_reftp
5407
5408 =cut
5409
5410 sub process_reftp {
5411   process_re_X('ftp', @_);
5412 }
5413
5414 =item respool
5415
5416 =cut
5417
5418 sub process_respool {
5419   process_re_X('spool', @_);
5420 }
5421
5422 use Storable qw(thaw);
5423 use Data::Dumper;
5424 use MIME::Base64;
5425 sub process_re_X {
5426   my( $method, $job ) = ( shift, shift );
5427   warn "$me process_re_X $method for job $job\n" if $DEBUG;
5428
5429   my $param = thaw(decode_base64(shift));
5430   warn Dumper($param) if $DEBUG;
5431
5432   re_X(
5433     $method,
5434     $job,
5435     %$param,
5436   );
5437
5438 }
5439
5440 sub re_X {
5441   my($method, $job, %param ) = @_;
5442   if ( $DEBUG ) {
5443     warn "re_X $method for job $job with param:\n".
5444          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
5445   }
5446
5447   #some false laziness w/search/cust_bill.html
5448   my $distinct = '';
5449   my $orderby = 'ORDER BY cust_bill._date';
5450
5451   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5452
5453   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5454      
5455   my @cust_bill = qsearch( {
5456     #'select'    => "cust_bill.*",
5457     'table'     => 'cust_bill',
5458     'addl_from' => $addl_from,
5459     'hashref'   => {},
5460     'extra_sql' => $extra_sql,
5461     'order_by'  => $orderby,
5462     'debug' => 1,
5463   } );
5464
5465   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5466
5467   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5468     if $DEBUG;
5469
5470   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5471   foreach my $cust_bill ( @cust_bill ) {
5472     $cust_bill->$method();
5473
5474     if ( $job ) { #progressbar foo
5475       $num++;
5476       if ( time - $min_sec > $last ) {
5477         my $error = $job->update_statustext(
5478           int( 100 * $num / scalar(@cust_bill) )
5479         );
5480         die $error if $error;
5481         $last = time;
5482       }
5483     }
5484
5485   }
5486
5487 }
5488
5489 =back
5490
5491 =head1 CLASS METHODS
5492
5493 =over 4
5494
5495 =item owed_sql
5496
5497 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5498
5499 =cut
5500
5501 sub owed_sql {
5502   my ($class, $start, $end) = @_;
5503   'charged - '. 
5504     $class->paid_sql($start, $end). ' - '. 
5505     $class->credited_sql($start, $end);
5506 }
5507
5508 =item net_sql
5509
5510 Returns an SQL fragment to retreive the net amount (charged minus credited).
5511
5512 =cut
5513
5514 sub net_sql {
5515   my ($class, $start, $end) = @_;
5516   'charged - '. $class->credited_sql($start, $end);
5517 }
5518
5519 =item paid_sql
5520
5521 Returns an SQL fragment to retreive the amount paid against this invoice.
5522
5523 =cut
5524
5525 sub paid_sql {
5526   my ($class, $start, $end) = @_;
5527   $start &&= "AND cust_bill_pay._date <= $start";
5528   $end   &&= "AND cust_bill_pay._date > $end";
5529   $start = '' unless defined($start);
5530   $end   = '' unless defined($end);
5531   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5532        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
5533 }
5534
5535 =item credited_sql
5536
5537 Returns an SQL fragment to retreive the amount credited against this invoice.
5538
5539 =cut
5540
5541 sub credited_sql {
5542   my ($class, $start, $end) = @_;
5543   $start &&= "AND cust_credit_bill._date <= $start";
5544   $end   &&= "AND cust_credit_bill._date >  $end";
5545   $start = '' unless defined($start);
5546   $end   = '' unless defined($end);
5547   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5548        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
5549 }
5550
5551 =item due_date_sql
5552
5553 Returns an SQL fragment to retrieve the due date of an invoice.
5554 Currently only supported on PostgreSQL.
5555
5556 =cut
5557
5558 sub due_date_sql {
5559   my $conf = new FS::Conf;
5560 'COALESCE(
5561   SUBSTRING(
5562     COALESCE(
5563       cust_bill.invoice_terms,
5564       cust_main.invoice_terms,
5565       \''.($conf->config('invoice_default_terms') || '').'\'
5566     ), E\'Net (\\\\d+)\'
5567   )::INTEGER, 0
5568 ) * 86400 + cust_bill._date'
5569 }
5570
5571 =item search_sql_where HASHREF
5572
5573 Class method which returns an SQL WHERE fragment to search for parameters
5574 specified in HASHREF.  Valid parameters are
5575
5576 =over 4
5577
5578 =item _date
5579
5580 List reference of start date, end date, as UNIX timestamps.
5581
5582 =item invnum_min
5583
5584 =item invnum_max
5585
5586 =item agentnum
5587
5588 =item charged
5589
5590 List reference of charged limits (exclusive).
5591
5592 =item owed
5593
5594 List reference of charged limits (exclusive).
5595
5596 =item open
5597
5598 flag, return open invoices only
5599
5600 =item net
5601
5602 flag, return net invoices only
5603
5604 =item days
5605
5606 =item newest_percust
5607
5608 =back
5609
5610 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5611
5612 =cut
5613
5614 sub search_sql_where {
5615   my($class, $param) = @_;
5616   if ( $DEBUG ) {
5617     warn "$me search_sql_where called with params: \n".
5618          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
5619   }
5620
5621   my @search = ();
5622
5623   #agentnum
5624   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5625     push @search, "cust_main.agentnum = $1";
5626   }
5627
5628   #refnum
5629   if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5630     push @search, "cust_main.refnum = $1";
5631   }
5632
5633   #custnum
5634   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5635     push @search, "cust_bill.custnum = $1";
5636   }
5637
5638   #_date
5639   if ( $param->{_date} ) {
5640     my($beginning, $ending) = @{$param->{_date}};
5641
5642     push @search, "cust_bill._date >= $beginning",
5643                   "cust_bill._date <  $ending";
5644   }
5645
5646   #invnum
5647   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5648     push @search, "cust_bill.invnum >= $1";
5649   }
5650   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5651     push @search, "cust_bill.invnum <= $1";
5652   }
5653
5654   #charged
5655   if ( $param->{charged} ) {
5656     my @charged = ref($param->{charged})
5657                     ? @{ $param->{charged} }
5658                     : ($param->{charged});
5659
5660     push @search, map { s/^charged/cust_bill.charged/; $_; }
5661                       @charged;
5662   }
5663
5664   my $owed_sql = FS::cust_bill->owed_sql;
5665
5666   #owed
5667   if ( $param->{owed} ) {
5668     my @owed = ref($param->{owed})
5669                  ? @{ $param->{owed} }
5670                  : ($param->{owed});
5671     push @search, map { s/^owed/$owed_sql/; $_; }
5672                       @owed;
5673   }
5674
5675   #open/net flags
5676   push @search, "0 != $owed_sql"
5677     if $param->{'open'};
5678   push @search, '0 != '. FS::cust_bill->net_sql
5679     if $param->{'net'};
5680
5681   #days
5682   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5683     if $param->{'days'};
5684
5685   #newest_percust
5686   if ( $param->{'newest_percust'} ) {
5687
5688     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5689     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5690
5691     my @newest_where = map { my $x = $_;
5692                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
5693                              $x;
5694                            }
5695                            grep ! /^cust_main./, @search;
5696     my $newest_where = scalar(@newest_where)
5697                          ? ' AND '. join(' AND ', @newest_where)
5698                          : '';
5699
5700
5701     push @search, "cust_bill._date = (
5702       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5703         WHERE newest_cust_bill.custnum = cust_bill.custnum
5704           $newest_where
5705     )";
5706
5707   }
5708
5709   #promised_date - also has an option to accept nulls
5710   if ( $param->{promised_date} ) {
5711     my($beginning, $ending, $null) = @{$param->{promised_date}};
5712
5713     push @search, "(( cust_bill.promised_date >= $beginning AND ".
5714                     "cust_bill.promised_date <  $ending )" .
5715                     ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5716   }
5717
5718   #agent virtualization
5719   my $curuser = $FS::CurrentUser::CurrentUser;
5720   if ( $curuser->username eq 'fs_queue'
5721        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5722     my $username = $1;
5723     my $newuser = qsearchs('access_user', {
5724       'username' => $username,
5725       'disabled' => '',
5726     } );
5727     if ( $newuser ) {
5728       $curuser = $newuser;
5729     } else {
5730       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5731     }
5732   }
5733   push @search, $curuser->agentnums_sql;
5734
5735   join(' AND ', @search );
5736
5737 }
5738
5739 =back
5740
5741 =head1 BUGS
5742
5743 The delete method.
5744
5745 =head1 SEE ALSO
5746
5747 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5748 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
5749 documentation.
5750
5751 =cut
5752
5753 1;
5754