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