9fbe34e942d5edbc77f7e7de4672301b0784fa8a
[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   my $min_sdate = 999999999999;
2520   my $max_edate = 0;
2521   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2522     next unless $cust_bill_pkg->pkgnum > 0;
2523     $min_sdate = $cust_bill_pkg->sdate if $cust_bill_pkg->sdate < $min_sdate;
2524     $max_edate = $cust_bill_pkg->edate if $cust_bill_pkg->edate > $max_edate;
2525   }
2526
2527   $invoice_data{'bill_period'} = '';
2528   $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate) 
2529     . " to " . time2str('%e %h', $max_edate)
2530     if ($max_edate != 0 && $min_sdate != 999999999999);
2531
2532   $invoice_data{finance_section} = '';
2533   if ( $conf->config('finance_pkgclass') ) {
2534     my $pkg_class =
2535       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2536     $invoice_data{finance_section} = $pkg_class->categoryname;
2537   } 
2538   $invoice_data{finance_amount} = '0.00';
2539   $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2540
2541   my $countrydefault = $conf->config('countrydefault') || 'US';
2542   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2543   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2544     my $method = $prefix.$_;
2545     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2546   }
2547   $invoice_data{'ship_country'} = ''
2548     if ( $invoice_data{'ship_country'} eq $countrydefault );
2549   
2550   $invoice_data{'cid'} = $params{'cid'}
2551     if $params{'cid'};
2552
2553   if ( $cust_main->country eq $countrydefault ) {
2554     $invoice_data{'country'} = '';
2555   } else {
2556     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2557   }
2558
2559   my @address = ();
2560   $invoice_data{'address'} = \@address;
2561   push @address,
2562     $cust_main->payname.
2563       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2564         ? " (P.O. #". $cust_main->payinfo. ")"
2565         : ''
2566       )
2567   ;
2568   push @address, $cust_main->company
2569     if $cust_main->company;
2570   push @address, $cust_main->address1;
2571   push @address, $cust_main->address2
2572     if $cust_main->address2;
2573   push @address,
2574     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
2575   push @address, $invoice_data{'country'}
2576     if $invoice_data{'country'};
2577   push @address, ''
2578     while (scalar(@address) < 5);
2579
2580   $invoice_data{'logo_file'} = $params{'logo_file'}
2581     if $params{'logo_file'};
2582   $invoice_data{'barcode_file'} = $params{'barcode_file'}
2583     if $params{'barcode_file'};
2584   $invoice_data{'barcode_img'} = $params{'barcode_img'}
2585     if $params{'barcode_img'};
2586   $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2587     if $params{'barcode_cid'};
2588
2589   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2590 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2591   #my $balance_due = $self->owed + $pr_total - $cr_total;
2592   my $balance_due = $self->owed + $pr_total;
2593   $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2594   $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2595   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2596   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2597
2598   my $summarypage = '';
2599   if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2600     $summarypage = 1;
2601   }
2602   $invoice_data{'summarypage'} = $summarypage;
2603
2604   warn "$me substituting variables in notes, footer, smallfooter\n"
2605     if $DEBUG > 1;
2606
2607   foreach my $include (qw( notes footer smallfooter coupon )) {
2608
2609     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2610     my @inc_src;
2611
2612     if ( $conf->exists($inc_file, $agentnum)
2613          && length( $conf->config($inc_file, $agentnum) ) ) {
2614
2615       @inc_src = $conf->config($inc_file, $agentnum);
2616
2617     } else {
2618
2619       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2620
2621       my $convert_map = $convert_maps{$format}{$include};
2622
2623       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2624                        s/--\@\]/$delimiters{$format}[1]/g;
2625                        $_;
2626                      } 
2627                  &$convert_map( $conf->config($inc_file, $agentnum) );
2628
2629     }
2630
2631     my $inc_tt = new Text::Template (
2632       TYPE       => 'ARRAY',
2633       SOURCE     => [ map "$_\n", @inc_src ],
2634       DELIMITERS => $delimiters{$format},
2635     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2636
2637     unless ( $inc_tt->compile() ) {
2638       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2639       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2640       die $error;
2641     }
2642
2643     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2644
2645     $invoice_data{$include} =~ s/\n+$//
2646       if ($format eq 'latex');
2647   }
2648
2649   $invoice_data{'po_line'} =
2650     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2651       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2652       : $nbsp;
2653
2654   my %money_chars = ( 'latex'    => '',
2655                       'html'     => $conf->config('money_char') || '$',
2656                       'template' => '',
2657                     );
2658   my $money_char = $money_chars{$format};
2659
2660   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2661                             'html'     => $conf->config('money_char') || '$',
2662                             'template' => '',
2663                           );
2664   my $other_money_char = $other_money_chars{$format};
2665   $invoice_data{'dollar'} = $other_money_char;
2666
2667   my @detail_items = ();
2668   my @total_items = ();
2669   my @buf = ();
2670   my @sections = ();
2671
2672   $invoice_data{'detail_items'} = \@detail_items;
2673   $invoice_data{'total_items'} = \@total_items;
2674   $invoice_data{'buf'} = \@buf;
2675   $invoice_data{'sections'} = \@sections;
2676
2677   warn "$me generating sections\n"
2678     if $DEBUG > 1;
2679
2680   my $previous_section = { 'description' => 'Previous Charges',
2681                            'subtotal'    => $other_money_char.
2682                                             sprintf('%.2f', $pr_total),
2683                            'summarized'  => $summarypage ? 'Y' : '',
2684                          };
2685   $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '. 
2686     join(' / ', map { $cust_main->balance_date_range(@$_) }
2687                 $self->_prior_month30s
2688         )
2689     if $conf->exists('invoice_include_aging');
2690
2691   my $taxtotal = 0;
2692   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2693                       'subtotal'    => $taxtotal,   # adjusted below
2694                       'summarized'  => $summarypage ? 'Y' : '',
2695                     };
2696   my $tax_weight = _pkg_category($tax_section->{description})
2697                         ? _pkg_category($tax_section->{description})->weight
2698                         : 0;
2699   $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2700   $tax_section->{'sort_weight'} = $tax_weight;
2701
2702
2703   my $adjusttotal = 0;
2704   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2705                          'subtotal'    => 0,   # adjusted below
2706                          'summarized'  => $summarypage ? 'Y' : '',
2707                        };
2708   my $adjust_weight = _pkg_category($adjust_section->{description})
2709                         ? _pkg_category($adjust_section->{description})->weight
2710                         : 0;
2711   $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2712   $adjust_section->{'sort_weight'} = $adjust_weight;
2713
2714   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2715   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2716   $invoice_data{'multisection'} = $multisection;
2717   my $late_sections = [];
2718   my $extra_sections = [];
2719   my $extra_lines = ();
2720   if ( $multisection ) {
2721     ($extra_sections, $extra_lines) =
2722       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2723       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2724
2725     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2726
2727     push @detail_items, @$extra_lines if $extra_lines;
2728     push @sections,
2729       $self->_items_sections( $late_sections,      # this could stand a refactor
2730                               $summarypage,
2731                               $escape_function_nonbsp,
2732                               $extra_sections,
2733                               $format,             #bah
2734                             );
2735     if ($conf->exists('svc_phone_sections')) {
2736       my ($phone_sections, $phone_lines) =
2737         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2738       push @{$late_sections}, @$phone_sections;
2739       push @detail_items, @$phone_lines;
2740     }
2741   }else{
2742     push @sections, { 'description' => '', 'subtotal' => '' };
2743   }
2744
2745   unless (    $conf->exists('disable_previous_balance')
2746            || $conf->exists('previous_balance-summary_only')
2747          )
2748   {
2749
2750     warn "$me adding previous balances\n"
2751       if $DEBUG > 1;
2752
2753     foreach my $line_item ( $self->_items_previous ) {
2754
2755       my $detail = {
2756         ext_description => [],
2757       };
2758       $detail->{'ref'} = $line_item->{'pkgnum'};
2759       $detail->{'quantity'} = 1;
2760       $detail->{'section'} = $previous_section;
2761       $detail->{'description'} = &$escape_function($line_item->{'description'});
2762       if ( exists $line_item->{'ext_description'} ) {
2763         @{$detail->{'ext_description'}} = map {
2764           &$escape_function($_);
2765         } @{$line_item->{'ext_description'}};
2766       }
2767       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2768                             $line_item->{'amount'};
2769       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2770
2771       push @detail_items, $detail;
2772       push @buf, [ $detail->{'description'},
2773                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2774                  ];
2775     }
2776
2777   }
2778
2779   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2780     push @buf, ['','-----------'];
2781     push @buf, [ 'Total Previous Balance',
2782                  $money_char. sprintf("%10.2f", $pr_total) ];
2783     push @buf, ['',''];
2784   }
2785  
2786   if ( $conf->exists('svc_phone-did-summary') ) {
2787       warn "$me adding DID summary\n"
2788         if $DEBUG > 1;
2789
2790       my ($didsummary,$minutes) = $self->_did_summary;
2791       my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2792       push @detail_items, 
2793         { 'description' => $didsummary_desc,
2794             'ext_description' => [ $didsummary, $minutes ],
2795         }
2796         if !$multisection;
2797   }
2798
2799   foreach my $section (@sections, @$late_sections) {
2800
2801     warn "$me adding section \n". Dumper($section)
2802       if $DEBUG > 1;
2803
2804     # begin some normalization
2805     $section->{'subtotal'} = $section->{'amount'}
2806       if $multisection
2807          && !exists($section->{subtotal})
2808          && exists($section->{amount});
2809
2810     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2811       if ( $invoice_data{finance_section} &&
2812            $section->{'description'} eq $invoice_data{finance_section} );
2813
2814     $section->{'subtotal'} = $other_money_char.
2815                              sprintf('%.2f', $section->{'subtotal'})
2816       if $multisection;
2817
2818     # continue some normalization
2819     $section->{'amount'}   = $section->{'subtotal'}
2820       if $multisection;
2821
2822
2823     if ( $section->{'description'} ) {
2824       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2825                    [ '', '' ],
2826                  );
2827     }
2828
2829     warn "$me   setting options\n"
2830       if $DEBUG > 1;
2831
2832     my $multilocation = scalar($cust_main->cust_location); #too expensive?
2833     my %options = ();
2834     $options{'section'} = $section if $multisection;
2835     $options{'format'} = $format;
2836     $options{'escape_function'} = $escape_function;
2837     $options{'format_function'} = sub { () } unless $unsquelched;
2838     $options{'unsquelched'} = $unsquelched;
2839     $options{'summary_page'} = $summarypage;
2840     $options{'skip_usage'} =
2841       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2842     $options{'multilocation'} = $multilocation;
2843     $options{'multisection'} = $multisection;
2844
2845     warn "$me   searching for line items\n"
2846       if $DEBUG > 1;
2847
2848     foreach my $line_item ( $self->_items_pkg(%options) ) {
2849
2850       warn "$me     adding line item $line_item\n"
2851         if $DEBUG > 1;
2852
2853       my $detail = {
2854         ext_description => [],
2855       };
2856       $detail->{'ref'} = $line_item->{'pkgnum'};
2857       $detail->{'quantity'} = $line_item->{'quantity'};
2858       $detail->{'section'} = $section;
2859       $detail->{'description'} = &$escape_function($line_item->{'description'});
2860       if ( exists $line_item->{'ext_description'} ) {
2861         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2862       }
2863       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2864                               $line_item->{'amount'};
2865       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2866                                  $line_item->{'unit_amount'};
2867       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2868   
2869       push @detail_items, $detail;
2870       push @buf, ( [ $detail->{'description'},
2871                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2872                    ],
2873                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2874                  );
2875     }
2876
2877     if ( $section->{'description'} ) {
2878       push @buf, ( ['','-----------'],
2879                    [ $section->{'description'}. ' sub-total',
2880                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2881                    ],
2882                    [ '', '' ],
2883                    [ '', '' ],
2884                  );
2885     }
2886   
2887   }
2888   
2889   $invoice_data{current_less_finance} =
2890     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2891
2892   if ( $multisection && !$conf->exists('disable_previous_balance')
2893     || $conf->exists('previous_balance-summary_only') )
2894   {
2895     unshift @sections, $previous_section if $pr_total;
2896   }
2897
2898   warn "$me adding taxes\n"
2899     if $DEBUG > 1;
2900
2901   foreach my $tax ( $self->_items_tax ) {
2902
2903     $taxtotal += $tax->{'amount'};
2904
2905     my $description = &$escape_function( $tax->{'description'} );
2906     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2907
2908     if ( $multisection ) {
2909
2910       my $money = $old_latex ? '' : $money_char;
2911       push @detail_items, {
2912         ext_description => [],
2913         ref          => '',
2914         quantity     => '',
2915         description  => $description,
2916         amount       => $money. $amount,
2917         product_code => '',
2918         section      => $tax_section,
2919       };
2920
2921     } else {
2922
2923       push @total_items, {
2924         'total_item'   => $description,
2925         'total_amount' => $other_money_char. $amount,
2926       };
2927
2928     }
2929
2930     push @buf,[ $description,
2931                 $money_char. $amount,
2932               ];
2933
2934   }
2935   
2936   if ( $taxtotal ) {
2937     my $total = {};
2938     $total->{'total_item'} = 'Sub-total';
2939     $total->{'total_amount'} =
2940       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2941
2942     if ( $multisection ) {
2943       $tax_section->{'subtotal'} = $other_money_char.
2944                                    sprintf('%.2f', $taxtotal);
2945       $tax_section->{'pretotal'} = 'New charges sub-total '.
2946                                    $total->{'total_amount'};
2947       push @sections, $tax_section if $taxtotal;
2948     }else{
2949       unshift @total_items, $total;
2950     }
2951   }
2952   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2953
2954   push @buf,['','-----------'];
2955   push @buf,[( $conf->exists('disable_previous_balance') 
2956                ? 'Total Charges'
2957                : 'Total New Charges'
2958              ),
2959              $money_char. sprintf("%10.2f",$self->charged) ];
2960   push @buf,['',''];
2961
2962   {
2963     my $total = {};
2964     my $item = 'Total';
2965     $item = $conf->config('previous_balance-exclude_from_total')
2966          || 'Total New Charges'
2967       if $conf->exists('previous_balance-exclude_from_total');
2968     my $amount = $self->charged +
2969                    ( $conf->exists('disable_previous_balance') ||
2970                      $conf->exists('previous_balance-exclude_from_total')
2971                      ? 0
2972                      : $pr_total
2973                    );
2974     $total->{'total_item'} = &$embolden_function($item);
2975     $total->{'total_amount'} =
2976       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
2977     if ( $multisection ) {
2978       if ( $adjust_section->{'sort_weight'} ) {
2979         $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2980           sprintf("%.2f", ($self->billing_balance || 0) );
2981       } else {
2982         $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2983                                         sprintf('%.2f', $self->charged );
2984       } 
2985     }else{
2986       push @total_items, $total;
2987     }
2988     push @buf,['','-----------'];
2989     push @buf,[$item,
2990                $money_char.
2991                sprintf( '%10.2f', $amount )
2992               ];
2993     push @buf,['',''];
2994   }
2995   
2996   unless ( $conf->exists('disable_previous_balance') ) {
2997     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2998   
2999     # credits
3000     my $credittotal = 0;
3001     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3002
3003       my $total;
3004       $total->{'total_item'} = &$escape_function($credit->{'description'});
3005       $credittotal += $credit->{'amount'};
3006       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3007       $adjusttotal += $credit->{'amount'};
3008       if ( $multisection ) {
3009         my $money = $old_latex ? '' : $money_char;
3010         push @detail_items, {
3011           ext_description => [],
3012           ref          => '',
3013           quantity     => '',
3014           description  => &$escape_function($credit->{'description'}),
3015           amount       => $money. $credit->{'amount'},
3016           product_code => '',
3017           section      => $adjust_section,
3018         };
3019       } else {
3020         push @total_items, $total;
3021       }
3022
3023     }
3024     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3025
3026     #credits (again)
3027     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3028       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3029     }
3030
3031     # payments
3032     my $paymenttotal = 0;
3033     foreach my $payment ( $self->_items_payments ) {
3034       my $total = {};
3035       $total->{'total_item'} = &$escape_function($payment->{'description'});
3036       $paymenttotal += $payment->{'amount'};
3037       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3038       $adjusttotal += $payment->{'amount'};
3039       if ( $multisection ) {
3040         my $money = $old_latex ? '' : $money_char;
3041         push @detail_items, {
3042           ext_description => [],
3043           ref          => '',
3044           quantity     => '',
3045           description  => &$escape_function($payment->{'description'}),
3046           amount       => $money. $payment->{'amount'},
3047           product_code => '',
3048           section      => $adjust_section,
3049         };
3050       }else{
3051         push @total_items, $total;
3052       }
3053       push @buf, [ $payment->{'description'},
3054                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
3055                  ];
3056     }
3057     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3058   
3059     if ( $multisection ) {
3060       $adjust_section->{'subtotal'} = $other_money_char.
3061                                       sprintf('%.2f', $adjusttotal);
3062       push @sections, $adjust_section
3063         unless $adjust_section->{sort_weight};
3064     }
3065
3066     { 
3067       my $total;
3068       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3069       $total->{'total_amount'} =
3070         &$embolden_function(
3071           $other_money_char. sprintf('%.2f', $summarypage 
3072                                                ? $self->charged +
3073                                                  $self->billing_balance
3074                                                : $self->owed + $pr_total
3075                                     )
3076         );
3077       if ( $multisection && !$adjust_section->{sort_weight} ) {
3078         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3079                                          $total->{'total_amount'};
3080       }else{
3081         push @total_items, $total;
3082       }
3083       push @buf,['','-----------'];
3084       push @buf,[$self->balance_due_msg, $money_char. 
3085         sprintf("%10.2f", $balance_due ) ];
3086     }
3087   }
3088
3089   if ( $multisection ) {
3090     if ($conf->exists('svc_phone_sections')) {
3091       my $total;
3092       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3093       $total->{'total_amount'} =
3094         &$embolden_function(
3095           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3096         );
3097       my $last_section = pop @sections;
3098       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3099                                      $total->{'total_amount'};
3100       push @sections, $last_section;
3101     }
3102     push @sections, @$late_sections
3103       if $unsquelched;
3104   }
3105
3106   my @includelist = ();
3107   push @includelist, 'summary' if $summarypage;
3108   foreach my $include ( @includelist ) {
3109
3110     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3111     my @inc_src;
3112
3113     if ( length( $conf->config($inc_file, $agentnum) ) ) {
3114
3115       @inc_src = $conf->config($inc_file, $agentnum);
3116
3117     } else {
3118
3119       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3120
3121       my $convert_map = $convert_maps{$format}{$include};
3122
3123       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3124                        s/--\@\]/$delimiters{$format}[1]/g;
3125                        $_;
3126                      } 
3127                  &$convert_map( $conf->config($inc_file, $agentnum) );
3128
3129     }
3130
3131     my $inc_tt = new Text::Template (
3132       TYPE       => 'ARRAY',
3133       SOURCE     => [ map "$_\n", @inc_src ],
3134       DELIMITERS => $delimiters{$format},
3135     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3136
3137     unless ( $inc_tt->compile() ) {
3138       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3139       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3140       die $error;
3141     }
3142
3143     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3144
3145     $invoice_data{$include} =~ s/\n+$//
3146       if ($format eq 'latex');
3147   }
3148
3149   $invoice_lines = 0;
3150   my $wasfunc = 0;
3151   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3152     /invoice_lines\((\d*)\)/;
3153     $invoice_lines += $1 || scalar(@buf);
3154     $wasfunc=1;
3155   }
3156   die "no invoice_lines() functions in template?"
3157     if ( $format eq 'template' && !$wasfunc );
3158
3159   if ($format eq 'template') {
3160
3161     if ( $invoice_lines ) {
3162       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3163       $invoice_data{'total_pages'}++
3164         if scalar(@buf) % $invoice_lines;
3165     }
3166
3167     #setup subroutine for the template
3168     sub FS::cust_bill::_template::invoice_lines {
3169       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3170       map { 
3171         scalar(@FS::cust_bill::_template::buf)
3172           ? shift @FS::cust_bill::_template::buf
3173           : [ '', '' ];
3174       }
3175       ( 1 .. $lines );
3176     }
3177
3178     my $lines;
3179     my @collect;
3180     while (@buf) {
3181       push @collect, split("\n",
3182         $text_template->fill_in( HASH => \%invoice_data,
3183                                  PACKAGE => 'FS::cust_bill::_template'
3184                                )
3185       );
3186       $FS::cust_bill::_template::page++;
3187     }
3188     map "$_\n", @collect;
3189   }else{
3190     warn "filling in template for invoice ". $self->invnum. "\n"
3191       if $DEBUG;
3192     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3193       if $DEBUG > 1;
3194
3195     $text_template->fill_in(HASH => \%invoice_data);
3196   }
3197 }
3198
3199 # helper routine for generating date ranges
3200 sub _prior_month30s {
3201   my $self = shift;
3202   my @ranges = (
3203    [ 1,       2592000 ], # 0-30 days ago
3204    [ 2592000, 5184000 ], # 30-60 days ago
3205    [ 5184000, 7776000 ], # 60-90 days ago
3206    [ 7776000, 0       ], # 90+   days ago
3207   );
3208
3209   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3210           $_->[1] ? $self->_date - $_->[1] - 1 : '',
3211       ] }
3212   @ranges;
3213 }
3214
3215 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3216
3217 Returns an postscript invoice, as a scalar.
3218
3219 Options can be passed as a hashref (recommended) or as a list of time, template
3220 and then any key/value pairs for any other options.
3221
3222 I<time> an optional value used to control the printing of overdue messages.  The
3223 default is now.  It isn't the date of the invoice; that's the `_date' field.
3224 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3225 L<Time::Local> and L<Date::Parse> for conversion functions.
3226
3227 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3228
3229 =cut
3230
3231 sub print_ps {
3232   my $self = shift;
3233
3234   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3235   my $ps = generate_ps($file);
3236   unlink($logofile);
3237   unlink($barcodefile);
3238
3239   $ps;
3240 }
3241
3242 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3243
3244 Returns an PDF invoice, as a scalar.
3245
3246 Options can be passed as a hashref (recommended) or as a list of time, template
3247 and then any key/value pairs for any other options.
3248
3249 I<time> an optional value used to control the printing of overdue messages.  The
3250 default is now.  It isn't the date of the invoice; that's the `_date' field.
3251 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3252 L<Time::Local> and L<Date::Parse> for conversion functions.
3253
3254 I<template>, if specified, is the name of a suffix for alternate invoices.
3255
3256 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3257
3258 =cut
3259
3260 sub print_pdf {
3261   my $self = shift;
3262
3263   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3264   my $pdf = generate_pdf($file);
3265   unlink($logofile);
3266   unlink($barcodefile);
3267
3268   $pdf;
3269 }
3270
3271 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3272
3273 Returns an HTML invoice, as a scalar.
3274
3275 I<time> an optional value used to control the printing of overdue messages.  The
3276 default is now.  It isn't the date of the invoice; that's the `_date' field.
3277 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3278 L<Time::Local> and L<Date::Parse> for conversion functions.
3279
3280 I<template>, if specified, is the name of a suffix for alternate invoices.
3281
3282 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3283
3284 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3285 when emailing the invoice as part of a multipart/related MIME email.
3286
3287 =cut
3288
3289 sub print_html {
3290   my $self = shift;
3291   my %params;
3292   if ( ref($_[0]) ) {
3293     %params = %{ shift() }; 
3294   }else{
3295     $params{'time'} = shift;
3296     $params{'template'} = shift;
3297     $params{'cid'} = shift;
3298   }
3299
3300   $params{'format'} = 'html';
3301   
3302   $self->print_generic( %params );
3303 }
3304
3305 # quick subroutine for print_latex
3306 #
3307 # There are ten characters that LaTeX treats as special characters, which
3308 # means that they do not simply typeset themselves: 
3309 #      # $ % & ~ _ ^ \ { }
3310 #
3311 # TeX ignores blanks following an escaped character; if you want a blank (as
3312 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3313
3314 sub _latex_escape {
3315   my $value = shift;
3316   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3317   $value =~ s/([<>])/\$$1\$/g;
3318   $value;
3319 }
3320
3321 sub _html_escape {
3322   my $value = shift;
3323   encode_entities($value);
3324   $value;
3325 }
3326
3327 sub _html_escape_nbsp {
3328   my $value = _html_escape(shift);
3329   $value =~ s/ +/&nbsp;/g;
3330   $value;
3331 }
3332
3333 #utility methods for print_*
3334
3335 sub _translate_old_latex_format {
3336   warn "_translate_old_latex_format called\n"
3337     if $DEBUG; 
3338
3339   my @template = ();
3340   while ( @_ ) {
3341     my $line = shift;
3342   
3343     if ( $line =~ /^%%Detail\s*$/ ) {
3344   
3345       push @template, q![@--!,
3346                       q!  foreach my $_tr_line (@detail_items) {!,
3347                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3348                       q!      $_tr_line->{'description'} .= !, 
3349                       q!        "\\tabularnewline\n~~".!,
3350                       q!        join( "\\tabularnewline\n~~",!,
3351                       q!          @{$_tr_line->{'ext_description'}}!,
3352                       q!        );!,
3353                       q!    }!;
3354
3355       while ( ( my $line_item_line = shift )
3356               !~ /^%%EndDetail\s*$/                            ) {
3357         $line_item_line =~ s/'/\\'/g;    # nice LTS
3358         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3359         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3360         push @template, "    \$OUT .= '$line_item_line';";
3361       }
3362
3363       push @template, '}',
3364                       '--@]';
3365       #' doh, gvim
3366     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3367
3368       push @template, '[@--',
3369                       '  foreach my $_tr_line (@total_items) {';
3370
3371       while ( ( my $total_item_line = shift )
3372               !~ /^%%EndTotalDetails\s*$/                      ) {
3373         $total_item_line =~ s/'/\\'/g;    # nice LTS
3374         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3375         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3376         push @template, "    \$OUT .= '$total_item_line';";
3377       }
3378
3379       push @template, '}',
3380                       '--@]';
3381
3382     } else {
3383       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3384       push @template, $line;  
3385     }
3386   
3387   }
3388
3389   if ($DEBUG) {
3390     warn "$_\n" foreach @template;
3391   }
3392
3393   (@template);
3394 }
3395
3396 sub terms {
3397   my $self = shift;
3398
3399   #check for an invoice-specific override
3400   return $self->invoice_terms if $self->invoice_terms;
3401   
3402   #check for a customer- specific override
3403   my $cust_main = $self->cust_main;
3404   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3405
3406   #use configured default
3407   $conf->config('invoice_default_terms') || '';
3408 }
3409
3410 sub due_date {
3411   my $self = shift;
3412   my $duedate = '';
3413   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3414     $duedate = $self->_date() + ( $1 * 86400 );
3415   }
3416   $duedate;
3417 }
3418
3419 sub due_date2str {
3420   my $self = shift;
3421   $self->due_date ? time2str(shift, $self->due_date) : '';
3422 }
3423
3424 sub balance_due_msg {
3425   my $self = shift;
3426   my $msg = 'Balance Due';
3427   return $msg unless $self->terms;
3428   if ( $self->due_date ) {
3429     $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3430   } elsif ( $self->terms ) {
3431     $msg .= ' - '. $self->terms;
3432   }
3433   $msg;
3434 }
3435
3436 sub balance_due_date {
3437   my $self = shift;
3438   my $duedate = '';
3439   if (    $conf->exists('invoice_default_terms') 
3440        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3441     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3442   }
3443   $duedate;
3444 }
3445
3446 =item invnum_date_pretty
3447
3448 Returns a string with the invoice number and date, for example:
3449 "Invoice #54 (3/20/2008)"
3450
3451 =cut
3452
3453 sub invnum_date_pretty {
3454   my $self = shift;
3455   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3456 }
3457
3458 =item _date_pretty
3459
3460 Returns a string with the date, for example: "3/20/2008"
3461
3462 =cut
3463
3464 sub _date_pretty {
3465   my $self = shift;
3466   time2str($date_format, $self->_date);
3467 }
3468
3469 use vars qw(%pkg_category_cache);
3470 sub _items_sections {
3471   my $self = shift;
3472   my $late = shift;
3473   my $summarypage = shift;
3474   my $escape = shift;
3475   my $extra_sections = shift;
3476   my $format = shift;
3477
3478   my %subtotal = ();
3479   my %late_subtotal = ();
3480   my %not_tax = ();
3481
3482   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3483   {
3484
3485       my $usage = $cust_bill_pkg->usage;
3486
3487       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3488         next if ( $display->summary && $summarypage );
3489
3490         my $section = $display->section;
3491         my $type    = $display->type;
3492
3493         $not_tax{$section} = 1
3494           unless $cust_bill_pkg->pkgnum == 0;
3495
3496         if ( $display->post_total && !$summarypage ) {
3497           if (! $type || $type eq 'S') {
3498             $late_subtotal{$section} += $cust_bill_pkg->setup
3499               if $cust_bill_pkg->setup != 0;
3500           }
3501
3502           if (! $type) {
3503             $late_subtotal{$section} += $cust_bill_pkg->recur
3504               if $cust_bill_pkg->recur != 0;
3505           }
3506
3507           if ($type && $type eq 'R') {
3508             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3509               if $cust_bill_pkg->recur != 0;
3510           }
3511           
3512           if ($type && $type eq 'U') {
3513             $late_subtotal{$section} += $usage
3514               unless scalar(@$extra_sections);
3515           }
3516
3517         } else {
3518
3519           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3520
3521           if (! $type || $type eq 'S') {
3522             $subtotal{$section} += $cust_bill_pkg->setup
3523               if $cust_bill_pkg->setup != 0;
3524           }
3525
3526           if (! $type) {
3527             $subtotal{$section} += $cust_bill_pkg->recur
3528               if $cust_bill_pkg->recur != 0;
3529           }
3530
3531           if ($type && $type eq 'R') {
3532             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3533               if $cust_bill_pkg->recur != 0;
3534           }
3535           
3536           if ($type && $type eq 'U') {
3537             $subtotal{$section} += $usage
3538               unless scalar(@$extra_sections);
3539           }
3540
3541         }
3542
3543       }
3544
3545   }
3546
3547   %pkg_category_cache = ();
3548
3549   push @$late, map { { 'description' => &{$escape}($_),
3550                        'subtotal'    => $late_subtotal{$_},
3551                        'post_total'  => 1,
3552                        'sort_weight' => ( _pkg_category($_)
3553                                             ? _pkg_category($_)->weight
3554                                             : 0
3555                                        ),
3556                        ((_pkg_category($_) && _pkg_category($_)->condense)
3557                                            ? $self->_condense_section($format)
3558                                            : ()
3559                        ),
3560                    } }
3561                  sort _sectionsort keys %late_subtotal;
3562
3563   my @sections;
3564   if ( $summarypage ) {
3565     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3566                 map { $_->categoryname } qsearch('pkg_category', {});
3567     push @sections, '' if exists($subtotal{''});
3568   } else {
3569     @sections = keys %subtotal;
3570   }
3571
3572   my @early = map { { 'description' => &{$escape}($_),
3573                       'subtotal'    => $subtotal{$_},
3574                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3575                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3576                       'sort_weight' => ( _pkg_category($_)
3577                                            ? _pkg_category($_)->weight
3578                                            : 0
3579                                        ),
3580                        ((_pkg_category($_) && _pkg_category($_)->condense)
3581                                            ? $self->_condense_section($format)
3582                                            : ()
3583                        ),
3584                     }
3585                   } @sections;
3586   push @early, @$extra_sections if $extra_sections;
3587  
3588   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3589
3590 }
3591
3592 #helper subs for above
3593
3594 sub _sectionsort {
3595   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3596 }
3597
3598 sub _pkg_category {
3599   my $categoryname = shift;
3600   $pkg_category_cache{$categoryname} ||=
3601     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3602 }
3603
3604 my %condensed_format = (
3605   'label' => [ qw( Description Qty Amount ) ],
3606   'fields' => [
3607                 sub { shift->{description} },
3608                 sub { shift->{quantity} },
3609                 sub { my($href, %opt) = @_;
3610                       ($opt{dollar} || ''). $href->{amount};
3611                     },
3612               ],
3613   'align'  => [ qw( l r r ) ],
3614   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
3615   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
3616 );
3617
3618 sub _condense_section {
3619   my ( $self, $format ) = ( shift, shift );
3620   ( 'condensed' => 1,
3621     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3622       qw( description_generator
3623           header_generator
3624           total_generator
3625           total_line_generator
3626         )
3627   );
3628 }
3629
3630 sub _condensed_generator_defaults {
3631   my ( $self, $format ) = ( shift, shift );
3632   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3633 }
3634
3635 my %html_align = (
3636   'c' => 'center',
3637   'l' => 'left',
3638   'r' => 'right',
3639 );
3640
3641 sub _condensed_header_generator {
3642   my ( $self, $format ) = ( shift, shift );
3643
3644   my ( $f, $prefix, $suffix, $separator, $column ) =
3645     _condensed_generator_defaults($format);
3646
3647   if ($format eq 'latex') {
3648     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3649     $suffix = "\\\\\n\\hline";
3650     $separator = "&\n";
3651     $column =
3652       sub { my ($d,$a,$s,$w) = @_;
3653             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3654           };
3655   } elsif ( $format eq 'html' ) {
3656     $prefix = '<th></th>';
3657     $suffix = '';
3658     $separator = '';
3659     $column =
3660       sub { my ($d,$a,$s,$w) = @_;
3661             return qq!<th align="$html_align{$a}">$d</th>!;
3662       };
3663   }
3664
3665   sub {
3666     my @args = @_;
3667     my @result = ();
3668
3669     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3670       push @result,
3671         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3672     }
3673
3674     $prefix. join($separator, @result). $suffix;
3675   };
3676
3677 }
3678
3679 sub _condensed_description_generator {
3680   my ( $self, $format ) = ( shift, shift );
3681
3682   my ( $f, $prefix, $suffix, $separator, $column ) =
3683     _condensed_generator_defaults($format);
3684
3685   my $money_char = '$';
3686   if ($format eq 'latex') {
3687     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3688     $suffix = '\\\\';
3689     $separator = " & \n";
3690     $column =
3691       sub { my ($d,$a,$s,$w) = @_;
3692             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3693           };
3694     $money_char = '\\dollar';
3695   }elsif ( $format eq 'html' ) {
3696     $prefix = '"><td align="center"></td>';
3697     $suffix = '';
3698     $separator = '';
3699     $column =
3700       sub { my ($d,$a,$s,$w) = @_;
3701             return qq!<td align="$html_align{$a}">$d</td>!;
3702       };
3703     #$money_char = $conf->config('money_char') || '$';
3704     $money_char = '';  # this is madness
3705   }
3706
3707   sub {
3708     #my @args = @_;
3709     my $href = shift;
3710     my @result = ();
3711
3712     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3713       my $dollar = '';
3714       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3715       push @result,
3716         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3717                     map { $f->{$_}->[$i] } qw(align span width)
3718                   );
3719     }
3720
3721     $prefix. join( $separator, @result ). $suffix;
3722   };
3723
3724 }
3725
3726 sub _condensed_total_generator {
3727   my ( $self, $format ) = ( shift, shift );
3728
3729   my ( $f, $prefix, $suffix, $separator, $column ) =
3730     _condensed_generator_defaults($format);
3731   my $style = '';
3732
3733   if ($format eq 'latex') {
3734     $prefix = "& ";
3735     $suffix = "\\\\\n";
3736     $separator = " & \n";
3737     $column =
3738       sub { my ($d,$a,$s,$w) = @_;
3739             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3740           };
3741   }elsif ( $format eq 'html' ) {
3742     $prefix = '';
3743     $suffix = '';
3744     $separator = '';
3745     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3746     $column =
3747       sub { my ($d,$a,$s,$w) = @_;
3748             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3749       };
3750   }
3751
3752
3753   sub {
3754     my @args = @_;
3755     my @result = ();
3756
3757     #  my $r = &{$f->{fields}->[$i]}(@args);
3758     #  $r .= ' Total' unless $i;
3759
3760     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3761       push @result,
3762         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3763                     map { $f->{$_}->[$i] } qw(align span width)
3764                   );
3765     }
3766
3767     $prefix. join( $separator, @result ). $suffix;
3768   };
3769
3770 }
3771
3772 =item total_line_generator FORMAT
3773
3774 Returns a coderef used for generation of invoice total line items for this
3775 usage_class.  FORMAT is either html or latex
3776
3777 =cut
3778
3779 # should not be used: will have issues with hash element names (description vs
3780 # total_item and amount vs total_amount -- another array of functions?
3781
3782 sub _condensed_total_line_generator {
3783   my ( $self, $format ) = ( shift, shift );
3784
3785   my ( $f, $prefix, $suffix, $separator, $column ) =
3786     _condensed_generator_defaults($format);
3787   my $style = '';
3788
3789   if ($format eq 'latex') {
3790     $prefix = "& ";
3791     $suffix = "\\\\\n";
3792     $separator = " & \n";
3793     $column =
3794       sub { my ($d,$a,$s,$w) = @_;
3795             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3796           };
3797   }elsif ( $format eq 'html' ) {
3798     $prefix = '';
3799     $suffix = '';
3800     $separator = '';
3801     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3802     $column =
3803       sub { my ($d,$a,$s,$w) = @_;
3804             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3805       };
3806   }
3807
3808
3809   sub {
3810     my @args = @_;
3811     my @result = ();
3812
3813     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3814       push @result,
3815         &{$column}( &{$f->{fields}->[$i]}(@args),
3816                     map { $f->{$_}->[$i] } qw(align span width)
3817                   );
3818     }
3819
3820     $prefix. join( $separator, @result ). $suffix;
3821   };
3822
3823 }
3824
3825 #sub _items_extra_usage_sections {
3826 #  my $self = shift;
3827 #  my $escape = shift;
3828 #
3829 #  my %sections = ();
3830 #
3831 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
3832 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3833 #  {
3834 #    next unless $cust_bill_pkg->pkgnum > 0;
3835 #
3836 #    foreach my $section ( keys %usage_class ) {
3837 #
3838 #      my $usage = $cust_bill_pkg->usage($section);
3839 #
3840 #      next unless $usage && $usage > 0;
3841 #
3842 #      $sections{$section} ||= 0;
3843 #      $sections{$section} += $usage;
3844 #
3845 #    }
3846 #
3847 #  }
3848 #
3849 #  map { { 'description' => &{$escape}($_),
3850 #          'subtotal'    => $sections{$_},
3851 #          'summarized'  => '',
3852 #          'tax_section' => '',
3853 #        }
3854 #      }
3855 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3856 #
3857 #}
3858
3859 sub _items_extra_usage_sections {
3860   my $self = shift;
3861   my $escape = shift;
3862   my $format = shift;
3863
3864   my %sections = ();
3865   my %classnums = ();
3866   my %lines = ();
3867
3868   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3869   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3870     next unless $cust_bill_pkg->pkgnum > 0;
3871
3872     foreach my $classnum ( keys %usage_class ) {
3873       my $section = $usage_class{$classnum}->classname;
3874       $classnums{$section} = $classnum;
3875
3876       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3877         my $amount = $detail->amount;
3878         next unless $amount && $amount > 0;
3879  
3880         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3881         $sections{$section}{amount} += $amount;  #subtotal
3882         $sections{$section}{calls}++;
3883         $sections{$section}{duration} += $detail->duration;
3884
3885         my $desc = $detail->regionname; 
3886         my $description = $desc;
3887         $description = substr($desc, 0, 50). '...'
3888           if $format eq 'latex' && length($desc) > 50;
3889
3890         $lines{$section}{$desc} ||= {
3891           description     => &{$escape}($description),
3892           #pkgpart         => $part_pkg->pkgpart,
3893           pkgnum          => $cust_bill_pkg->pkgnum,
3894           ref             => '',
3895           amount          => 0,
3896           calls           => 0,
3897           duration        => 0,
3898           #unit_amount     => $cust_bill_pkg->unitrecur,
3899           quantity        => $cust_bill_pkg->quantity,
3900           product_code    => 'N/A',
3901           ext_description => [],
3902         };
3903
3904         $lines{$section}{$desc}{amount} += $amount;
3905         $lines{$section}{$desc}{calls}++;
3906         $lines{$section}{$desc}{duration} += $detail->duration;
3907
3908       }
3909     }
3910   }
3911
3912   my %sectionmap = ();
3913   foreach (keys %sections) {
3914     my $usage_class = $usage_class{$classnums{$_}};
3915     $sectionmap{$_} = { 'description' => &{$escape}($_),
3916                         'amount'    => $sections{$_}{amount},    #subtotal
3917                         'calls'       => $sections{$_}{calls},
3918                         'duration'    => $sections{$_}{duration},
3919                         'summarized'  => '',
3920                         'tax_section' => '',
3921                         'sort_weight' => $usage_class->weight,
3922                         ( $usage_class->format
3923                           ? ( map { $_ => $usage_class->$_($format) }
3924                               qw( description_generator header_generator total_generator total_line_generator )
3925                             )
3926                           : ()
3927                         ), 
3928                       };
3929   }
3930
3931   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3932                  values %sectionmap;
3933
3934   my @lines = ();
3935   foreach my $section ( keys %lines ) {
3936     foreach my $line ( keys %{$lines{$section}} ) {
3937       my $l = $lines{$section}{$line};
3938       $l->{section}     = $sectionmap{$section};
3939       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
3940       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3941       push @lines, $l;
3942     }
3943   }
3944
3945   return(\@sections, \@lines);
3946
3947 }
3948
3949 sub _did_summary {
3950     my $self = shift;
3951     my $end = $self->_date;
3952     my $start = $end - 2592000; # 30 days
3953     my $cust_main = $self->cust_main;
3954     my @pkgs = $cust_main->all_pkgs;
3955     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3956         = (0,0,0,0,0);
3957     my @seen = ();
3958     foreach my $pkg ( @pkgs ) {
3959         my @h_cust_svc = $pkg->h_cust_svc($end);
3960         foreach my $h_cust_svc ( @h_cust_svc ) {
3961             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3962             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3963
3964             my $inserted = $h_cust_svc->date_inserted;
3965             my $deleted = $h_cust_svc->date_deleted;
3966             my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3967             my $phone_deleted;
3968             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
3969             
3970 # DID either activated or ported in; cannot be both for same DID simultaneously
3971             if ($inserted >= $start && $inserted <= $end && $phone_inserted
3972                 && (!$phone_inserted->lnp_status 
3973                     || $phone_inserted->lnp_status eq ''
3974                     || $phone_inserted->lnp_status eq 'native')) {
3975                 $num_activated++;
3976             }
3977             else { # this one not so clean, should probably move to (h_)svc_phone
3978                  my $phone_portedin = qsearchs( 'h_svc_phone',
3979                       { 'svcnum' => $h_cust_svc->svcnum, 
3980                         'lnp_status' => 'portedin' },  
3981                       FS::h_svc_phone->sql_h_searchs($end),  
3982                     );
3983                  $num_portedin++ if $phone_portedin;
3984             }
3985
3986 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3987             if($deleted >= $start && $deleted <= $end && $phone_deleted
3988                 && (!$phone_deleted->lnp_status 
3989                     || $phone_deleted->lnp_status ne 'portingout')) {
3990                 $num_deactivated++;
3991             } 
3992             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
3993                 && $phone_deleted->lnp_status 
3994                 && $phone_deleted->lnp_status eq 'portingout') {
3995                 $num_portedout++;
3996             }
3997
3998             # increment usage minutes
3999             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4000             foreach my $cdr ( @cdrs ) {
4001                 $minutes += $cdr->billsec/60;
4002             }
4003
4004             # don't look at this service again
4005             push @seen, $h_cust_svc->svcnum;
4006         }
4007     }
4008
4009     $minutes = sprintf("%d", $minutes);
4010     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
4011         . "$num_deactivated  Ported-Out: $num_portedout ",
4012             "Total Minutes: $minutes");
4013 }
4014
4015 sub _items_svc_phone_sections {
4016   my $self = shift;
4017   my $escape = shift;
4018   my $format = shift;
4019
4020   my %sections = ();
4021   my %classnums = ();
4022   my %lines = ();
4023
4024   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4025   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4026
4027   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4028     next unless $cust_bill_pkg->pkgnum > 0;
4029
4030     my @header = $cust_bill_pkg->details_header;
4031     next unless scalar(@header);
4032
4033     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4034
4035       my $phonenum = $detail->phonenum;
4036       next unless $phonenum;
4037
4038       my $amount = $detail->amount;
4039       next unless $amount && $amount > 0;
4040
4041       $sections{$phonenum} ||= { 'amount'      => 0,
4042                                  'calls'       => 0,
4043                                  'duration'    => 0,
4044                                  'sort_weight' => -1,
4045                                  'phonenum'    => $phonenum,
4046                                 };
4047       $sections{$phonenum}{amount} += $amount;  #subtotal
4048       $sections{$phonenum}{calls}++;
4049       $sections{$phonenum}{duration} += $detail->duration;
4050
4051       my $desc = $detail->regionname; 
4052       my $description = $desc;
4053       $description = substr($desc, 0, 50). '...'
4054         if $format eq 'latex' && length($desc) > 50;
4055
4056       $lines{$phonenum}{$desc} ||= {
4057         description     => &{$escape}($description),
4058         #pkgpart         => $part_pkg->pkgpart,
4059         pkgnum          => '',
4060         ref             => '',
4061         amount          => 0,
4062         calls           => 0,
4063         duration        => 0,
4064         #unit_amount     => '',
4065         quantity        => '',
4066         product_code    => 'N/A',
4067         ext_description => [],
4068       };
4069
4070       $lines{$phonenum}{$desc}{amount} += $amount;
4071       $lines{$phonenum}{$desc}{calls}++;
4072       $lines{$phonenum}{$desc}{duration} += $detail->duration;
4073
4074       my $line = $usage_class{$detail->classnum}->classname;
4075       $sections{"$phonenum $line"} ||=
4076         { 'amount' => 0,
4077           'calls' => 0,
4078           'duration' => 0,
4079           'sort_weight' => $usage_class{$detail->classnum}->weight,
4080           'phonenum' => $phonenum,
4081           'header'  => [ @header ],
4082         };
4083       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
4084       $sections{"$phonenum $line"}{calls}++;
4085       $sections{"$phonenum $line"}{duration} += $detail->duration;
4086
4087       $lines{"$phonenum $line"}{$desc} ||= {
4088         description     => &{$escape}($description),
4089         #pkgpart         => $part_pkg->pkgpart,
4090         pkgnum          => '',
4091         ref             => '',
4092         amount          => 0,
4093         calls           => 0,
4094         duration        => 0,
4095         #unit_amount     => '',
4096         quantity        => '',
4097         product_code    => 'N/A',
4098         ext_description => [],
4099       };
4100
4101       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4102       $lines{"$phonenum $line"}{$desc}{calls}++;
4103       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4104       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4105            $detail->formatted('format' => $format);
4106
4107     }
4108   }
4109
4110   my %sectionmap = ();
4111   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4112   foreach ( keys %sections ) {
4113     my @header = @{ $sections{$_}{header} || [] };
4114     my $usage_simple =
4115       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4116     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4117     my $usage_class = $summary ? $simple : $usage_simple;
4118     my $ending = $summary ? ' usage charges' : '';
4119     my %gen_opt = ();
4120     unless ($summary) {
4121       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4122     }
4123     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4124                         'amount'    => $sections{$_}{amount},    #subtotal
4125                         'calls'       => $sections{$_}{calls},
4126                         'duration'    => $sections{$_}{duration},
4127                         'summarized'  => '',
4128                         'tax_section' => '',
4129                         'phonenum'    => $sections{$_}{phonenum},
4130                         'sort_weight' => $sections{$_}{sort_weight},
4131                         'post_total'  => $summary, #inspire pagebreak
4132                         (
4133                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4134                             qw( description_generator
4135                                 header_generator
4136                                 total_generator
4137                                 total_line_generator
4138                               )
4139                           )
4140                         ), 
4141                       };
4142   }
4143
4144   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4145                         $a->{sort_weight} <=> $b->{sort_weight}
4146                       }
4147                  values %sectionmap;
4148
4149   my @lines = ();
4150   foreach my $section ( keys %lines ) {
4151     foreach my $line ( keys %{$lines{$section}} ) {
4152       my $l = $lines{$section}{$line};
4153       $l->{section}     = $sectionmap{$section};
4154       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4155       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4156       push @lines, $l;
4157     }
4158   }
4159   
4160   if($conf->exists('phone_usage_class_summary')) { 
4161       # this only works with Latex
4162       my @newlines;
4163       my @newsections;
4164
4165       # after this, we'll have only two sections per DID:
4166       # Calls Summary and Calls Detail
4167       foreach my $section ( @sections ) {
4168         if($section->{'post_total'}) {
4169             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4170             $section->{'total_line_generator'} = sub { '' };
4171             $section->{'total_generator'} = sub { '' };
4172             $section->{'header_generator'} = sub { '' };
4173             $section->{'description_generator'} = '';
4174             push @newsections, $section;
4175             my %calls_detail = %$section;
4176             $calls_detail{'post_total'} = '';
4177             $calls_detail{'sort_weight'} = '';
4178             $calls_detail{'description_generator'} = sub { '' };
4179             $calls_detail{'header_generator'} = sub {
4180                 return ' & Date/Time & Called Number & Duration & Price'
4181                     if $format eq 'latex';
4182                 '';
4183             };
4184             $calls_detail{'description'} = 'Calls Detail: '
4185                                                     . $section->{'phonenum'};
4186             push @newsections, \%calls_detail;  
4187         }
4188       }
4189
4190       # after this, each usage class is collapsed/summarized into a single
4191       # line under the Calls Summary section
4192       foreach my $newsection ( @newsections ) {
4193         if($newsection->{'post_total'}) { # this means Calls Summary
4194             foreach my $section ( @sections ) {
4195                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
4196                                 && !$section->{'post_total'});
4197                 my $newdesc = $section->{'description'};
4198                 my $tn = $section->{'phonenum'};
4199                 $newdesc =~ s/$tn//g;
4200                 my $line = {  ext_description => [],
4201                               pkgnum => '',
4202                               ref => '',
4203                               quantity => '',
4204                               calls => $section->{'calls'},
4205                               section => $newsection,
4206                               duration => $section->{'duration'},
4207                               description => $newdesc,
4208                               amount => sprintf("%.2f",$section->{'amount'}),
4209                               product_code => 'N/A',
4210                             };
4211                 push @newlines, $line;
4212             }
4213         }
4214       }
4215
4216       # after this, Calls Details is populated with all CDRs
4217       foreach my $newsection ( @newsections ) {
4218         if(!$newsection->{'post_total'}) { # this means Calls Details
4219             foreach my $line ( @lines ) {
4220                 next unless (scalar(@{$line->{'ext_description'}}) &&
4221                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4222                             );
4223                 my @extdesc = @{$line->{'ext_description'}};
4224                 my @newextdesc;
4225                 foreach my $extdesc ( @extdesc ) {
4226                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4227                     push @newextdesc, $extdesc;
4228                 }
4229                 $line->{'ext_description'} = \@newextdesc;
4230                 $line->{'section'} = $newsection;
4231                 push @newlines, $line;
4232             }
4233         }
4234       }
4235
4236       return(\@newsections, \@newlines);
4237   }
4238
4239   return(\@sections, \@lines);
4240
4241 }
4242
4243 sub _items {
4244   my $self = shift;
4245
4246   #my @display = scalar(@_)
4247   #              ? @_
4248   #              : qw( _items_previous _items_pkg );
4249   #              #: qw( _items_pkg );
4250   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4251   my @display = qw( _items_previous _items_pkg );
4252
4253   my @b = ();
4254   foreach my $display ( @display ) {
4255     push @b, $self->$display(@_);
4256   }
4257   @b;
4258 }
4259
4260 sub _items_previous {
4261   my $self = shift;
4262   my $cust_main = $self->cust_main;
4263   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4264   my @b = ();
4265   foreach ( @pr_cust_bill ) {
4266     my $date = $conf->exists('invoice_show_prior_due_date')
4267                ? 'due '. $_->due_date2str($date_format)
4268                : time2str($date_format, $_->_date);
4269     push @b, {
4270       'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4271       #'pkgpart'     => 'N/A',
4272       'pkgnum'      => 'N/A',
4273       'amount'      => sprintf("%.2f", $_->owed),
4274     };
4275   }
4276   @b;
4277
4278   #{
4279   #    'description'     => 'Previous Balance',
4280   #    #'pkgpart'         => 'N/A',
4281   #    'pkgnum'          => 'N/A',
4282   #    'amount'          => sprintf("%10.2f", $pr_total ),
4283   #    'ext_description' => [ map {
4284   #                                 "Invoice ". $_->invnum.
4285   #                                 " (". time2str("%x",$_->_date). ") ".
4286   #                                 sprintf("%10.2f", $_->owed)
4287   #                         } @pr_cust_bill ],
4288
4289   #};
4290 }
4291
4292 sub _items_pkg {
4293   my $self = shift;
4294   my %options = @_;
4295
4296   warn "$me _items_pkg searching for all package line items\n"
4297     if $DEBUG > 1;
4298
4299   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4300
4301   warn "$me _items_pkg filtering line items\n"
4302     if $DEBUG > 1;
4303   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4304
4305   if ($options{section} && $options{section}->{condensed}) {
4306
4307     warn "$me _items_pkg condensing section\n"
4308       if $DEBUG > 1;
4309
4310     my %itemshash = ();
4311     local $Storable::canonical = 1;
4312     foreach ( @items ) {
4313       my $item = { %$_ };
4314       delete $item->{ref};
4315       delete $item->{ext_description};
4316       my $key = freeze($item);
4317       $itemshash{$key} ||= 0;
4318       $itemshash{$key} ++; # += $item->{quantity};
4319     }
4320     @items = sort { $a->{description} cmp $b->{description} }
4321              map { my $i = thaw($_);
4322                    $i->{quantity} = $itemshash{$_};
4323                    $i->{amount} =
4324                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4325                    $i;
4326                  }
4327              keys %itemshash;
4328   }
4329
4330   warn "$me _items_pkg returning ". scalar(@items). " items\n"
4331     if $DEBUG > 1;
4332
4333   @items;
4334 }
4335
4336 sub _taxsort {
4337   return 0 unless $a->itemdesc cmp $b->itemdesc;
4338   return -1 if $b->itemdesc eq 'Tax';
4339   return 1 if $a->itemdesc eq 'Tax';
4340   return -1 if $b->itemdesc eq 'Other surcharges';
4341   return 1 if $a->itemdesc eq 'Other surcharges';
4342   $a->itemdesc cmp $b->itemdesc;
4343 }
4344
4345 sub _items_tax {
4346   my $self = shift;
4347   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4348   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4349 }
4350
4351 sub _items_cust_bill_pkg {
4352   my $self = shift;
4353   my $cust_bill_pkgs = shift;
4354   my %opt = @_;
4355
4356   my $format = $opt{format} || '';
4357   my $escape_function = $opt{escape_function} || sub { shift };
4358   my $format_function = $opt{format_function} || '';
4359   my $unsquelched = $opt{unsquelched} || '';
4360   my $section = $opt{section}->{description} if $opt{section};
4361   my $summary_page = $opt{summary_page} || '';
4362   my $multilocation = $opt{multilocation} || '';
4363   my $multisection = $opt{multisection} || '';
4364   my $discount_show_always = 0;
4365
4366   my @b = ();
4367   my ($s, $r, $u) = ( undef, undef, undef );
4368   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4369   {
4370
4371     warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4372       if $DEBUG > 1;
4373
4374     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4375                                 && $conf->exists('discount-show-always'));
4376
4377     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4378       if ( $_ && !$cust_bill_pkg->hidden ) {
4379         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4380         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4381         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4382         push @b, { %$_ }
4383           unless ( $_->{amount} == 0 && !$discount_show_always );
4384         $_ = undef;
4385       }
4386     }
4387
4388     foreach my $display ( grep { defined($section)
4389                                  ? $_->section eq $section
4390                                  : 1
4391                                }
4392                           #grep { !$_->summary || !$summary_page } # bunk!
4393                           grep { !$_->summary || $multisection }
4394                           $cust_bill_pkg->cust_bill_pkg_display
4395                         )
4396     {
4397
4398       warn "$me _items_cust_bill_pkg considering display item $display\n"
4399         if $DEBUG > 1;
4400
4401       my $type = $display->type;
4402
4403       my $desc = $cust_bill_pkg->desc;
4404       $desc = substr($desc, 0, 50). '...'
4405         if $format eq 'latex' && length($desc) > 50;
4406
4407       my %details_opt = ( 'format'          => $format,
4408                           'escape_function' => $escape_function,
4409                           'format_function' => $format_function,
4410                         );
4411
4412       if ( $cust_bill_pkg->pkgnum > 0 ) {
4413
4414         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4415           if $DEBUG > 1;
4416  
4417         my $cust_pkg = $cust_bill_pkg->cust_pkg;
4418
4419         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4420
4421           warn "$me _items_cust_bill_pkg adding setup\n"
4422             if $DEBUG > 1;
4423
4424           my $description = $desc;
4425           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4426
4427           my @d = ();
4428           unless ( $cust_pkg->part_pkg->hide_svc_detail
4429                 || $cust_bill_pkg->hidden )
4430           {
4431
4432             push @d, map &{$escape_function}($_),
4433                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
4434               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4435
4436             if ( $multilocation ) {
4437               my $loc = $cust_pkg->location_label;
4438               $loc = substr($loc, 0, 50). '...'
4439                 if $format eq 'latex' && length($loc) > 50;
4440               push @d, &{$escape_function}($loc);
4441             }
4442
4443           }
4444
4445           push @d, $cust_bill_pkg->details(%details_opt)
4446             if $cust_bill_pkg->recur == 0;
4447
4448           if ( $cust_bill_pkg->hidden ) {
4449             $s->{amount}      += $cust_bill_pkg->setup;
4450             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4451             push @{ $s->{ext_description} }, @d;
4452           } else {
4453             $s = {
4454               description     => $description,
4455               #pkgpart         => $part_pkg->pkgpart,
4456               pkgnum          => $cust_bill_pkg->pkgnum,
4457               amount          => $cust_bill_pkg->setup,
4458               unit_amount     => $cust_bill_pkg->unitsetup,
4459               quantity        => $cust_bill_pkg->quantity,
4460               ext_description => \@d,
4461             };
4462           };
4463
4464         }
4465
4466         if ( ( $cust_bill_pkg->recur != 0  || $cust_bill_pkg->setup == 0 || 
4467                 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4468              ( !$type || $type eq 'R' || $type eq 'U' )
4469            )
4470         {
4471
4472           warn "$me _items_cust_bill_pkg adding recur/usage\n"
4473             if $DEBUG > 1;
4474
4475           my $is_summary = $display->summary;
4476           my $description = ($is_summary && $type && $type eq 'U')
4477                             ? "Usage charges" : $desc;
4478
4479           $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4480                           " - ". time2str($date_format, $cust_bill_pkg->edate).
4481                           ")"
4482             unless $conf->exists('disable_line_item_date_ranges');
4483
4484           my @d = ();
4485
4486           #at least until cust_bill_pkg has "past" ranges in addition to
4487           #the "future" sdate/edate ones... see #3032
4488           my @dates = ( $self->_date );
4489           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4490           push @dates, $prev->sdate if $prev;
4491           push @dates, undef if !$prev;
4492
4493           unless ( $cust_pkg->part_pkg->hide_svc_detail
4494                 || $cust_bill_pkg->itemdesc
4495                 || $cust_bill_pkg->hidden
4496                 || $is_summary && $type && $type eq 'U' )
4497           {
4498
4499             warn "$me _items_cust_bill_pkg adding service details\n"
4500               if $DEBUG > 1;
4501
4502             push @d, map &{$escape_function}($_),
4503                          $cust_pkg->h_labels_short(@dates, 'I')
4504                                                    #$cust_bill_pkg->edate,
4505                                                    #$cust_bill_pkg->sdate)
4506               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4507
4508             warn "$me _items_cust_bill_pkg done adding service details\n"
4509               if $DEBUG > 1;
4510
4511             if ( $multilocation ) {
4512               my $loc = $cust_pkg->location_label;
4513               $loc = substr($loc, 0, 50). '...'
4514                 if $format eq 'latex' && length($loc) > 50;
4515               push @d, &{$escape_function}($loc);
4516             }
4517
4518           }
4519
4520           warn "$me _items_cust_bill_pkg adding details\n"
4521             if $DEBUG > 1;
4522
4523           push @d, $cust_bill_pkg->details(%details_opt)
4524             unless ($is_summary || $type && $type eq 'R');
4525
4526           warn "$me _items_cust_bill_pkg calculating amount\n"
4527             if $DEBUG > 1;
4528   
4529           my $amount = 0;
4530           if (!$type) {
4531             $amount = $cust_bill_pkg->recur;
4532           }elsif($type eq 'R') {
4533             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4534           }elsif($type eq 'U') {
4535             $amount = $cust_bill_pkg->usage;
4536           }
4537   
4538           if ( !$type || $type eq 'R' ) {
4539
4540             warn "$me _items_cust_bill_pkg adding recur\n"
4541               if $DEBUG > 1;
4542
4543             if ( $cust_bill_pkg->hidden ) {
4544               $r->{amount}      += $amount;
4545               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4546               push @{ $r->{ext_description} }, @d;
4547             } else {
4548               $r = {
4549                 description     => $description,
4550                 #pkgpart         => $part_pkg->pkgpart,
4551                 pkgnum          => $cust_bill_pkg->pkgnum,
4552                 amount          => $amount,
4553                 unit_amount     => $cust_bill_pkg->unitrecur,
4554                 quantity        => $cust_bill_pkg->quantity,
4555                 ext_description => \@d,
4556               };
4557             }
4558
4559           } else {  # $type eq 'U'
4560
4561             warn "$me _items_cust_bill_pkg adding usage\n"
4562               if $DEBUG > 1;
4563
4564             if ( $cust_bill_pkg->hidden ) {
4565               $u->{amount}      += $amount;
4566               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4567               push @{ $u->{ext_description} }, @d;
4568             } else {
4569               $u = {
4570                 description     => $description,
4571                 #pkgpart         => $part_pkg->pkgpart,
4572                 pkgnum          => $cust_bill_pkg->pkgnum,
4573                 amount          => $amount,
4574                 unit_amount     => $cust_bill_pkg->unitrecur,
4575                 quantity        => $cust_bill_pkg->quantity,
4576                 ext_description => \@d,
4577               };
4578             }
4579
4580           }
4581
4582         } # recurring or usage with recurring charge
4583
4584       } else { #pkgnum tax or one-shot line item (??)
4585
4586         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4587           if $DEBUG > 1;
4588
4589         if ( $cust_bill_pkg->setup != 0 ) {
4590           push @b, {
4591             'description' => $desc,
4592             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
4593           };
4594         }
4595         if ( $cust_bill_pkg->recur != 0 ) {
4596           push @b, {
4597             'description' => "$desc (".
4598                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4599                              time2str($date_format, $cust_bill_pkg->edate). ')',
4600             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
4601           };
4602         }
4603
4604       }
4605
4606     }
4607
4608   }
4609
4610   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4611     if $DEBUG > 1;
4612
4613   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4614     if ( $_  ) {
4615       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4616       $_->{amount}      =~ s/^\-0\.00$/0.00/;
4617       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4618       push @b, { %$_ }
4619         unless ( $_->{amount} == 0 && !$discount_show_always );
4620     }
4621   }
4622
4623   @b;
4624
4625 }
4626
4627 sub _items_credits {
4628   my( $self, %opt ) = @_;
4629   my $trim_len = $opt{'trim_len'} || 60;
4630
4631   my @b;
4632   #credits
4633   foreach ( $self->cust_credited ) {
4634
4635     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4636
4637     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4638     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4639     $reason = " ($reason) " if $reason;
4640
4641     push @b, {
4642       #'description' => 'Credit ref\#'. $_->crednum.
4643       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
4644       #                 $reason,
4645       'description' => 'Credit applied '.
4646                        time2str($date_format,$_->cust_credit->_date). $reason,
4647       'amount'      => sprintf("%.2f",$_->amount),
4648     };
4649   }
4650
4651   @b;
4652
4653 }
4654
4655 sub _items_payments {
4656   my $self = shift;
4657
4658   my @b;
4659   #get & print payments
4660   foreach ( $self->cust_bill_pay ) {
4661
4662     #something more elaborate if $_->amount ne ->cust_pay->paid ?
4663
4664     push @b, {
4665       'description' => "Payment received ".
4666                        time2str($date_format,$_->cust_pay->_date ),
4667       'amount'      => sprintf("%.2f", $_->amount )
4668     };
4669   }
4670
4671   @b;
4672
4673 }
4674
4675 =item call_details [ OPTION => VALUE ... ]
4676
4677 Returns an array of CSV strings representing the call details for this invoice
4678 The only option available is the boolean prepend_billed_number
4679
4680 =cut
4681
4682 sub call_details {
4683   my ($self, %opt) = @_;
4684
4685   my $format_function = sub { shift };
4686
4687   if ($opt{prepend_billed_number}) {
4688     $format_function = sub {
4689       my $detail = shift;
4690       my $row = shift;
4691
4692       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4693       
4694     };
4695   }
4696
4697   my @details = map { $_->details( 'format_function' => $format_function,
4698                                    'escape_function' => sub{ return() },
4699                                  )
4700                     }
4701                   grep { $_->pkgnum }
4702                   $self->cust_bill_pkg;
4703   my $header = $details[0];
4704   ( $header, grep { $_ ne $header } @details );
4705 }
4706
4707
4708 =back
4709
4710 =head1 SUBROUTINES
4711
4712 =over 4
4713
4714 =item process_reprint
4715
4716 =cut
4717
4718 sub process_reprint {
4719   process_re_X('print', @_);
4720 }
4721
4722 =item process_reemail
4723
4724 =cut
4725
4726 sub process_reemail {
4727   process_re_X('email', @_);
4728 }
4729
4730 =item process_refax
4731
4732 =cut
4733
4734 sub process_refax {
4735   process_re_X('fax', @_);
4736 }
4737
4738 =item process_reftp
4739
4740 =cut
4741
4742 sub process_reftp {
4743   process_re_X('ftp', @_);
4744 }
4745
4746 =item respool
4747
4748 =cut
4749
4750 sub process_respool {
4751   process_re_X('spool', @_);
4752 }
4753
4754 use Storable qw(thaw);
4755 use Data::Dumper;
4756 use MIME::Base64;
4757 sub process_re_X {
4758   my( $method, $job ) = ( shift, shift );
4759   warn "$me process_re_X $method for job $job\n" if $DEBUG;
4760
4761   my $param = thaw(decode_base64(shift));
4762   warn Dumper($param) if $DEBUG;
4763
4764   re_X(
4765     $method,
4766     $job,
4767     %$param,
4768   );
4769
4770 }
4771
4772 sub re_X {
4773   my($method, $job, %param ) = @_;
4774   if ( $DEBUG ) {
4775     warn "re_X $method for job $job with param:\n".
4776          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
4777   }
4778
4779   #some false laziness w/search/cust_bill.html
4780   my $distinct = '';
4781   my $orderby = 'ORDER BY cust_bill._date';
4782
4783   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4784
4785   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4786      
4787   my @cust_bill = qsearch( {
4788     #'select'    => "cust_bill.*",
4789     'table'     => 'cust_bill',
4790     'addl_from' => $addl_from,
4791     'hashref'   => {},
4792     'extra_sql' => $extra_sql,
4793     'order_by'  => $orderby,
4794     'debug' => 1,
4795   } );
4796
4797   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4798
4799   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4800     if $DEBUG;
4801
4802   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4803   foreach my $cust_bill ( @cust_bill ) {
4804     $cust_bill->$method();
4805
4806     if ( $job ) { #progressbar foo
4807       $num++;
4808       if ( time - $min_sec > $last ) {
4809         my $error = $job->update_statustext(
4810           int( 100 * $num / scalar(@cust_bill) )
4811         );
4812         die $error if $error;
4813         $last = time;
4814       }
4815     }
4816
4817   }
4818
4819 }
4820
4821 =back
4822
4823 =head1 CLASS METHODS
4824
4825 =over 4
4826
4827 =item owed_sql
4828
4829 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4830
4831 =cut
4832
4833 sub owed_sql {
4834   my ($class, $start, $end) = @_;
4835   'charged - '. 
4836     $class->paid_sql($start, $end). ' - '. 
4837     $class->credited_sql($start, $end);
4838 }
4839
4840 =item net_sql
4841
4842 Returns an SQL fragment to retreive the net amount (charged minus credited).
4843
4844 =cut
4845
4846 sub net_sql {
4847   my ($class, $start, $end) = @_;
4848   'charged - '. $class->credited_sql($start, $end);
4849 }
4850
4851 =item paid_sql
4852
4853 Returns an SQL fragment to retreive the amount paid against this invoice.
4854
4855 =cut
4856
4857 sub paid_sql {
4858   my ($class, $start, $end) = @_;
4859   $start &&= "AND cust_bill_pay._date <= $start";
4860   $end   &&= "AND cust_bill_pay._date > $end";
4861   $start = '' unless defined($start);
4862   $end   = '' unless defined($end);
4863   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4864        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
4865 }
4866
4867 =item credited_sql
4868
4869 Returns an SQL fragment to retreive the amount credited against this invoice.
4870
4871 =cut
4872
4873 sub credited_sql {
4874   my ($class, $start, $end) = @_;
4875   $start &&= "AND cust_credit_bill._date <= $start";
4876   $end   &&= "AND cust_credit_bill._date >  $end";
4877   $start = '' unless defined($start);
4878   $end   = '' unless defined($end);
4879   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4880        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
4881 }
4882
4883 =item due_date_sql
4884
4885 Returns an SQL fragment to retrieve the due date of an invoice.
4886 Currently only supported on PostgreSQL.
4887
4888 =cut
4889
4890 sub due_date_sql {
4891 'COALESCE(
4892   SUBSTRING(
4893     COALESCE(
4894       cust_bill.invoice_terms,
4895       cust_main.invoice_terms,
4896       \''.($conf->config('invoice_default_terms') || '').'\'
4897     ), E\'Net (\\\\d+)\'
4898   )::INTEGER, 0
4899 ) * 86400 + cust_bill._date'
4900 }
4901
4902 =item search_sql_where HASHREF
4903
4904 Class method which returns an SQL WHERE fragment to search for parameters
4905 specified in HASHREF.  Valid parameters are
4906
4907 =over 4
4908
4909 =item _date
4910
4911 List reference of start date, end date, as UNIX timestamps.
4912
4913 =item invnum_min
4914
4915 =item invnum_max
4916
4917 =item agentnum
4918
4919 =item charged
4920
4921 List reference of charged limits (exclusive).
4922
4923 =item owed
4924
4925 List reference of charged limits (exclusive).
4926
4927 =item open
4928
4929 flag, return open invoices only
4930
4931 =item net
4932
4933 flag, return net invoices only
4934
4935 =item days
4936
4937 =item newest_percust
4938
4939 =back
4940
4941 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4942
4943 =cut
4944
4945 sub search_sql_where {
4946   my($class, $param) = @_;
4947   if ( $DEBUG ) {
4948     warn "$me search_sql_where called with params: \n".
4949          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
4950   }
4951
4952   my @search = ();
4953
4954   #agentnum
4955   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4956     push @search, "cust_main.agentnum = $1";
4957   }
4958
4959   #_date
4960   if ( $param->{_date} ) {
4961     my($beginning, $ending) = @{$param->{_date}};
4962
4963     push @search, "cust_bill._date >= $beginning",
4964                   "cust_bill._date <  $ending";
4965   }
4966
4967   #invnum
4968   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4969     push @search, "cust_bill.invnum >= $1";
4970   }
4971   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4972     push @search, "cust_bill.invnum <= $1";
4973   }
4974
4975   #charged
4976   if ( $param->{charged} ) {
4977     my @charged = ref($param->{charged})
4978                     ? @{ $param->{charged} }
4979                     : ($param->{charged});
4980
4981     push @search, map { s/^charged/cust_bill.charged/; $_; }
4982                       @charged;
4983   }
4984
4985   my $owed_sql = FS::cust_bill->owed_sql;
4986
4987   #owed
4988   if ( $param->{owed} ) {
4989     my @owed = ref($param->{owed})
4990                  ? @{ $param->{owed} }
4991                  : ($param->{owed});
4992     push @search, map { s/^owed/$owed_sql/; $_; }
4993                       @owed;
4994   }
4995
4996   #open/net flags
4997   push @search, "0 != $owed_sql"
4998     if $param->{'open'};
4999   push @search, '0 != '. FS::cust_bill->net_sql
5000     if $param->{'net'};
5001
5002   #days
5003   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5004     if $param->{'days'};
5005
5006   #newest_percust
5007   if ( $param->{'newest_percust'} ) {
5008
5009     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5010     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5011
5012     my @newest_where = map { my $x = $_;
5013                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
5014                              $x;
5015                            }
5016                            grep ! /^cust_main./, @search;
5017     my $newest_where = scalar(@newest_where)
5018                          ? ' AND '. join(' AND ', @newest_where)
5019                          : '';
5020
5021
5022     push @search, "cust_bill._date = (
5023       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5024         WHERE newest_cust_bill.custnum = cust_bill.custnum
5025           $newest_where
5026     )";
5027
5028   }
5029
5030   #agent virtualization
5031   my $curuser = $FS::CurrentUser::CurrentUser;
5032   if ( $curuser->username eq 'fs_queue'
5033        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5034     my $username = $1;
5035     my $newuser = qsearchs('access_user', {
5036       'username' => $username,
5037       'disabled' => '',
5038     } );
5039     if ( $newuser ) {
5040       $curuser = $newuser;
5041     } else {
5042       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5043     }
5044   }
5045   push @search, $curuser->agentnums_sql;
5046
5047   join(' AND ', @search );
5048
5049 }
5050
5051 =back
5052
5053 =head1 BUGS
5054
5055 The delete method.
5056
5057 =head1 SEE ALSO
5058
5059 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5060 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
5061 documentation.
5062
5063 =cut
5064
5065 1;
5066