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