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