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