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