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