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