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