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