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