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