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