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