naming follow-up notices from the event rather than creting a slew of separate templa...
[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     'company_name'    => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2256     'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2257     'custnum'         => $cust_main->display_custnum,
2258     'invnum'          => $self->invnum,
2259     'date'            => time2str($date_format, $self->_date),
2260     'today'           => time2str('%b %o, %Y', $today),
2261     'agent'           => &$escape_function($cust_main->agent->agent),
2262     'agent_custid'    => &$escape_function($cust_main->agent_custid),
2263     'payname'         => &$escape_function($cust_main->payname),
2264     'company'         => &$escape_function($cust_main->company),
2265     'address1'        => &$escape_function($cust_main->address1),
2266     'address2'        => &$escape_function($cust_main->address2),
2267     'city'            => &$escape_function($cust_main->city),
2268     'state'           => &$escape_function($cust_main->state),
2269     'zip'             => &$escape_function($cust_main->zip),
2270     'fax'             => &$escape_function($cust_main->fax),
2271     'returnaddress'   => $returnaddress,
2272     #'quantity'        => 1,
2273     'terms'           => $self->terms,
2274     'template'        => $template, #params{'template'},
2275     #'notes'           => join("\n", $conf->config('invoice_latexnotes') ),
2276     # better hang on to conf_dir for a while
2277     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2278     'page'            => 1,
2279     'total_pages'     => 1,
2280     'current_charges' => sprintf("%.2f", $self->charged),
2281     'duedate'         => $self->due_date2str('%m/%d/%Y'), #date_format?
2282     'ship_enable'     => $conf->exists('invoice-ship_address'),
2283     'unitprices'      => $conf->exists('invoice-unitprice'),
2284     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
2285   );
2286
2287   $invoice_data{finance_section} = '';
2288   if ( $conf->config('finance_pkgclass') ) {
2289     my $pkg_class =
2290       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2291     $invoice_data{finance_section} = $pkg_class->categoryname;
2292   } 
2293  $invoice_data{finance_amount} = '0.00';
2294
2295   my $countrydefault = $conf->config('countrydefault') || 'US';
2296   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2297   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2298     my $method = $prefix.$_;
2299     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2300   }
2301   $invoice_data{'ship_country'} = ''
2302     if ( $invoice_data{'ship_country'} eq $countrydefault );
2303   
2304   $invoice_data{'cid'} = $params{'cid'}
2305     if $params{'cid'};
2306
2307   if ( $cust_main->country eq $countrydefault ) {
2308     $invoice_data{'country'} = '';
2309   } else {
2310     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2311   }
2312
2313   my @address = ();
2314   $invoice_data{'address'} = \@address;
2315   push @address,
2316     $cust_main->payname.
2317       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2318         ? " (P.O. #". $cust_main->payinfo. ")"
2319         : ''
2320       )
2321   ;
2322   push @address, $cust_main->company
2323     if $cust_main->company;
2324   push @address, $cust_main->address1;
2325   push @address, $cust_main->address2
2326     if $cust_main->address2;
2327   push @address,
2328     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
2329   push @address, $invoice_data{'country'}
2330     if $invoice_data{'country'};
2331   push @address, ''
2332     while (scalar(@address) < 5);
2333
2334   $invoice_data{'logo_file'} = $params{'logo_file'}
2335     if $params{'logo_file'};
2336
2337   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2338 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2339   #my $balance_due = $self->owed + $pr_total - $cr_total;
2340   my $balance_due = $self->owed + $pr_total;
2341   $invoice_data{'true_previous_balance'} = sprintf("%.2f", $self->previous_balance);
2342   $invoice_data{'balance_adjustments'} = sprintf("%.2f", $self->previous_balance - $self->billing_balance);
2343   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2344   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2345
2346   my $agentnum = $self->cust_main->agentnum;
2347
2348   my $summarypage = '';
2349   if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2350     $summarypage = 1;
2351   }
2352   $invoice_data{'summarypage'} = $summarypage;
2353
2354   #do variable substitution in notes, footer, smallfooter
2355   foreach my $include (qw( notes footer smallfooter coupon )) {
2356
2357     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2358     my @inc_src;
2359
2360     if ( $conf->exists($inc_file, $agentnum)
2361          && length( $conf->config($inc_file, $agentnum) ) ) {
2362
2363       @inc_src = $conf->config($inc_file, $agentnum);
2364
2365     } else {
2366
2367       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2368
2369       my $convert_map = $convert_maps{$format}{$include};
2370
2371       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2372                        s/--\@\]/$delimiters{$format}[1]/g;
2373                        $_;
2374                      } 
2375                  &$convert_map( $conf->config($inc_file, $agentnum) );
2376
2377     }
2378
2379     my $inc_tt = new Text::Template (
2380       TYPE       => 'ARRAY',
2381       SOURCE     => [ map "$_\n", @inc_src ],
2382       DELIMITERS => $delimiters{$format},
2383     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2384
2385     unless ( $inc_tt->compile() ) {
2386       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2387       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2388       die $error;
2389     }
2390
2391     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2392
2393     $invoice_data{$include} =~ s/\n+$//
2394       if ($format eq 'latex');
2395   }
2396
2397   $invoice_data{'po_line'} =
2398     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2399       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2400       : $nbsp;
2401
2402   my %money_chars = ( 'latex'    => '',
2403                       'html'     => $conf->config('money_char') || '$',
2404                       'template' => '',
2405                     );
2406   my $money_char = $money_chars{$format};
2407
2408   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2409                             'html'     => $conf->config('money_char') || '$',
2410                             'template' => '',
2411                           );
2412   my $other_money_char = $other_money_chars{$format};
2413   $invoice_data{'dollar'} = $other_money_char;
2414
2415   my @detail_items = ();
2416   my @total_items = ();
2417   my @buf = ();
2418   my @sections = ();
2419
2420   $invoice_data{'detail_items'} = \@detail_items;
2421   $invoice_data{'total_items'} = \@total_items;
2422   $invoice_data{'buf'} = \@buf;
2423   $invoice_data{'sections'} = \@sections;
2424
2425   my $previous_section = { 'description' => 'Previous Charges',
2426                            'subtotal'    => $other_money_char.
2427                                             sprintf('%.2f', $pr_total),
2428                            'summarized'  => $summarypage ? 'Y' : '',
2429                          };
2430
2431   my $taxtotal = 0;
2432   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2433                       'subtotal'    => $taxtotal,   # adjusted below
2434                       'summarized'  => $summarypage ? 'Y' : '',
2435                     };
2436
2437   my $adjusttotal = 0;
2438   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2439                          'subtotal'    => 0,   # adjusted below
2440                          'summarized'  => $summarypage ? 'Y' : '',
2441                        };
2442
2443   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2444   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2445   my $late_sections = [];
2446   if ( $multisection ) {
2447     push @sections,
2448       $self->_items_sections( $late_sections, $summarypage, $escape_function );
2449   }else{
2450     push @sections, { 'description' => '', 'subtotal' => '' };
2451   }
2452
2453   unless (    $conf->exists('disable_previous_balance')
2454            || $conf->exists('previous_balance-summary_only')
2455          )
2456   {
2457
2458     foreach my $line_item ( $self->_items_previous ) {
2459
2460       my $detail = {
2461         ext_description => [],
2462       };
2463       $detail->{'ref'} = $line_item->{'pkgnum'};
2464       $detail->{'quantity'} = 1;
2465       $detail->{'section'} = $previous_section;
2466       $detail->{'description'} = &$escape_function($line_item->{'description'});
2467       if ( exists $line_item->{'ext_description'} ) {
2468         @{$detail->{'ext_description'}} = map {
2469           &$escape_function($_);
2470         } @{$line_item->{'ext_description'}};
2471       }
2472       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2473                             $line_item->{'amount'};
2474       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2475
2476       push @detail_items, $detail;
2477       push @buf, [ $detail->{'description'},
2478                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2479                  ];
2480     }
2481
2482   }
2483
2484   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2485     push @buf, ['','-----------'];
2486     push @buf, [ 'Total Previous Balance',
2487                  $money_char. sprintf("%10.2f", $pr_total) ];
2488     push @buf, ['',''];
2489   }
2490
2491   foreach my $section (@sections, @$late_sections) {
2492
2493     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2494       if ( $invoice_data{finance_section} &&
2495            $section->{'description'} eq $invoice_data{finance_section} );
2496
2497     $section->{'subtotal'} = $other_money_char.
2498                              sprintf('%.2f', $section->{'subtotal'})
2499       if $multisection;
2500
2501     if ( $section->{'description'} ) {
2502       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2503                    [ '', '' ],
2504                  );
2505     }
2506
2507     my %options = ();
2508     $options{'section'} = $section if $multisection;
2509     $options{'format'} = $format;
2510     $options{'escape_function'} = $escape_function;
2511     $options{'format_function'} = sub { () } unless $unsquelched;
2512     $options{'unsquelched'} = $unsquelched;
2513     $options{'summary_page'} = $summarypage;
2514
2515     foreach my $line_item ( $self->_items_pkg(%options) ) {
2516       my $detail = {
2517         ext_description => [],
2518       };
2519       $detail->{'ref'} = $line_item->{'pkgnum'};
2520       $detail->{'quantity'} = $line_item->{'quantity'};
2521       $detail->{'section'} = $section;
2522       $detail->{'description'} = &$escape_function($line_item->{'description'});
2523       if ( exists $line_item->{'ext_description'} ) {
2524         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2525       }
2526       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2527                               $line_item->{'amount'};
2528       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2529                                  $line_item->{'unit_amount'};
2530       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2531   
2532       push @detail_items, $detail;
2533       push @buf, ( [ $detail->{'description'},
2534                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2535                    ],
2536                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2537                  );
2538     }
2539
2540     if ( $section->{'description'} ) {
2541       push @buf, ( ['','-----------'],
2542                    [ $section->{'description'}. ' sub-total',
2543                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2544                    ],
2545                    [ '', '' ],
2546                    [ '', '' ],
2547                  );
2548     }
2549   
2550   }
2551   
2552   $invoice_data{current_less_finance} =
2553     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2554
2555   if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2556     unshift @sections, $previous_section if $pr_total;
2557   }
2558
2559   foreach my $tax ( $self->_items_tax ) {
2560
2561     $taxtotal += $tax->{'amount'};
2562
2563     my $description = &$escape_function( $tax->{'description'} );
2564     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2565
2566     if ( $multisection ) {
2567
2568       my $money = $old_latex ? '' : $money_char;
2569       push @detail_items, {
2570         ext_description => [],
2571         ref          => '',
2572         quantity     => '',
2573         description  => $description,
2574         amount       => $money. $amount,
2575         product_code => '',
2576         section      => $tax_section,
2577       };
2578
2579     } else {
2580
2581       push @total_items, {
2582         'total_item'   => $description,
2583         'total_amount' => $other_money_char. $amount,
2584       };
2585
2586     }
2587
2588     push @buf,[ $description,
2589                 $money_char. $amount,
2590               ];
2591
2592   }
2593   
2594   if ( $taxtotal ) {
2595     my $total = {};
2596     $total->{'total_item'} = 'Sub-total';
2597     $total->{'total_amount'} =
2598       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2599
2600     if ( $multisection ) {
2601       $tax_section->{'subtotal'} = $other_money_char.
2602                                    sprintf('%.2f', $taxtotal);
2603       $tax_section->{'pretotal'} = 'New charges sub-total '.
2604                                    $total->{'total_amount'};
2605       push @sections, $tax_section if $taxtotal;
2606     }else{
2607       unshift @total_items, $total;
2608     }
2609   }
2610   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2611
2612   push @buf,['','-----------'];
2613   push @buf,[( $conf->exists('disable_previous_balance') 
2614                ? 'Total Charges'
2615                : 'Total New Charges'
2616              ),
2617              $money_char. sprintf("%10.2f",$self->charged) ];
2618   push @buf,['',''];
2619
2620   {
2621     my $total = {};
2622     $total->{'total_item'} = &$embolden_function('Total');
2623     $total->{'total_amount'} =
2624       &$embolden_function(
2625         $other_money_char.
2626         sprintf( '%.2f',
2627                  $self->charged + ( $conf->exists('disable_previous_balance')
2628                                     ? 0
2629                                     : $pr_total
2630                                   )
2631                )
2632       );
2633     if ( $multisection ) {
2634       $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2635                                       sprintf('%.2f', $self->charged );
2636     }else{
2637       push @total_items, $total;
2638     }
2639     push @buf,['','-----------'];
2640     push @buf,['Total Charges',
2641                $money_char.
2642                sprintf( '%10.2f', $self->charged +
2643                                     ( $conf->exists('disable_previous_balance')
2644                                         ? 0
2645                                         : $pr_total
2646                                     )
2647                       )
2648               ];
2649     push @buf,['',''];
2650   }
2651   
2652   unless ( $conf->exists('disable_previous_balance') ) {
2653     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2654   
2655     # credits
2656     my $credittotal = 0;
2657     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2658
2659       my $total;
2660       $total->{'total_item'} = &$escape_function($credit->{'description'});
2661       $credittotal += $credit->{'amount'};
2662       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2663       $adjusttotal += $credit->{'amount'};
2664       if ( $multisection ) {
2665         my $money = $old_latex ? '' : $money_char;
2666         push @detail_items, {
2667           ext_description => [],
2668           ref          => '',
2669           quantity     => '',
2670           description  => &$escape_function($credit->{'description'}),
2671           amount       => $money. $credit->{'amount'},
2672           product_code => '',
2673           section      => $adjust_section,
2674         };
2675       } else {
2676         push @total_items, $total;
2677       }
2678
2679     }
2680     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2681
2682     #credits (again)
2683     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2684       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2685     }
2686
2687     # payments
2688     my $paymenttotal = 0;
2689     foreach my $payment ( $self->_items_payments ) {
2690       my $total = {};
2691       $total->{'total_item'} = &$escape_function($payment->{'description'});
2692       $paymenttotal += $payment->{'amount'};
2693       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2694       $adjusttotal += $payment->{'amount'};
2695       if ( $multisection ) {
2696         my $money = $old_latex ? '' : $money_char;
2697         push @detail_items, {
2698           ext_description => [],
2699           ref          => '',
2700           quantity     => '',
2701           description  => &$escape_function($payment->{'description'}),
2702           amount       => $money. $payment->{'amount'},
2703           product_code => '',
2704           section      => $adjust_section,
2705         };
2706       }else{
2707         push @total_items, $total;
2708       }
2709       push @buf, [ $payment->{'description'},
2710                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
2711                  ];
2712     }
2713     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2714   
2715     if ( $multisection ) {
2716       $adjust_section->{'subtotal'} = $other_money_char.
2717                                       sprintf('%.2f', $adjusttotal);
2718       push @sections, $adjust_section;
2719     }
2720
2721     { 
2722       my $total;
2723       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2724       $total->{'total_amount'} =
2725         &$embolden_function(
2726           $other_money_char. sprintf('%.2f', $summarypage 
2727                                                ? $self->charged +
2728                                                  $self->billing_balance
2729                                                : $self->owed + $pr_total
2730                                     )
2731         );
2732       if ( $multisection ) {
2733         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2734                                          $total->{'total_amount'};
2735       }else{
2736         push @total_items, $total;
2737       }
2738       push @buf,['','-----------'];
2739       push @buf,[$self->balance_due_msg, $money_char. 
2740         sprintf("%10.2f", $balance_due ) ];
2741     }
2742   }
2743
2744   if ( $multisection ) {
2745     push @sections, @$late_sections
2746       if $unsquelched;
2747   }
2748
2749   my @includelist = ();
2750   push @includelist, 'summary' if $summarypage;
2751   foreach my $include ( @includelist ) {
2752
2753     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2754     my @inc_src;
2755
2756     if ( length( $conf->config($inc_file, $agentnum) ) ) {
2757
2758       @inc_src = $conf->config($inc_file, $agentnum);
2759
2760     } else {
2761
2762       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2763
2764       my $convert_map = $convert_maps{$format}{$include};
2765
2766       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2767                        s/--\@\]/$delimiters{$format}[1]/g;
2768                        $_;
2769                      } 
2770                  &$convert_map( $conf->config($inc_file, $agentnum) );
2771
2772     }
2773
2774     my $inc_tt = new Text::Template (
2775       TYPE       => 'ARRAY',
2776       SOURCE     => [ map "$_\n", @inc_src ],
2777       DELIMITERS => $delimiters{$format},
2778     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2779
2780     unless ( $inc_tt->compile() ) {
2781       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2782       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2783       die $error;
2784     }
2785
2786     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2787
2788     $invoice_data{$include} =~ s/\n+$//
2789       if ($format eq 'latex');
2790   }
2791
2792   $invoice_lines = 0;
2793   my $wasfunc = 0;
2794   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2795     /invoice_lines\((\d*)\)/;
2796     $invoice_lines += $1 || scalar(@buf);
2797     $wasfunc=1;
2798   }
2799   die "no invoice_lines() functions in template?"
2800     if ( $format eq 'template' && !$wasfunc );
2801
2802   if ($format eq 'template') {
2803
2804     if ( $invoice_lines ) {
2805       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2806       $invoice_data{'total_pages'}++
2807         if scalar(@buf) % $invoice_lines;
2808     }
2809
2810     #setup subroutine for the template
2811     sub FS::cust_bill::_template::invoice_lines {
2812       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2813       map { 
2814         scalar(@FS::cust_bill::_template::buf)
2815           ? shift @FS::cust_bill::_template::buf
2816           : [ '', '' ];
2817       }
2818       ( 1 .. $lines );
2819     }
2820
2821     my $lines;
2822     my @collect;
2823     while (@buf) {
2824       push @collect, split("\n",
2825         $text_template->fill_in( HASH => \%invoice_data,
2826                                  PACKAGE => 'FS::cust_bill::_template'
2827                                )
2828       );
2829       $FS::cust_bill::_template::page++;
2830     }
2831     map "$_\n", @collect;
2832   }else{
2833     warn "filling in template for invoice ". $self->invnum. "\n"
2834       if $DEBUG;
2835     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2836       if $DEBUG > 1;
2837
2838     $text_template->fill_in(HASH => \%invoice_data);
2839   }
2840 }
2841
2842 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
2843
2844 Returns an postscript invoice, as a scalar.
2845
2846 Options can be passed as a hashref (recommended) or as a list of time, template
2847 and then any key/value pairs for any other options.
2848
2849 I<time> an optional value used to control the printing of overdue messages.  The
2850 default is now.  It isn't the date of the invoice; that's the `_date' field.
2851 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2852 L<Time::Local> and L<Date::Parse> for conversion functions.
2853
2854 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2855
2856 =cut
2857
2858 sub print_ps {
2859   my $self = shift;
2860
2861   my ($file, $lfile) = $self->print_latex(@_);
2862   my $ps = generate_ps($file);
2863   unlink($lfile);
2864
2865   $ps;
2866 }
2867
2868 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
2869
2870 Returns an PDF invoice, as a scalar.
2871
2872 Options can be passed as a hashref (recommended) or as a list of time, template
2873 and then any key/value pairs for any other options.
2874
2875 I<time> an optional value used to control the printing of overdue messages.  The
2876 default is now.  It isn't the date of the invoice; that's the `_date' field.
2877 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2878 L<Time::Local> and L<Date::Parse> for conversion functions.
2879
2880 I<template>, if specified, is the name of a suffix for alternate invoices.
2881
2882 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2883
2884 =cut
2885
2886 sub print_pdf {
2887   my $self = shift;
2888
2889   my ($file, $lfile) = $self->print_latex(@_);
2890   my $pdf = generate_pdf($file);
2891   unlink($lfile);
2892
2893   $pdf;
2894 }
2895
2896 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
2897
2898 Returns an HTML invoice, as a scalar.
2899
2900 I<time> an optional value used to control the printing of overdue messages.  The
2901 default is now.  It isn't the date of the invoice; that's the `_date' field.
2902 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2903 L<Time::Local> and L<Date::Parse> for conversion functions.
2904
2905 I<template>, if specified, is the name of a suffix for alternate invoices.
2906
2907 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2908
2909 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2910 when emailing the invoice as part of a multipart/related MIME email.
2911
2912 =cut
2913
2914 sub print_html {
2915   my $self = shift;
2916   my %params;
2917   if ( ref($_[0]) ) {
2918     %params = %{ shift() }; 
2919   }else{
2920     $params{'time'} = shift;
2921     $params{'template'} = shift;
2922     $params{'cid'} = shift;
2923   }
2924
2925   $params{'format'} = 'html';
2926
2927   $self->print_generic( %params );
2928 }
2929
2930 # quick subroutine for print_latex
2931 #
2932 # There are ten characters that LaTeX treats as special characters, which
2933 # means that they do not simply typeset themselves: 
2934 #      # $ % & ~ _ ^ \ { }
2935 #
2936 # TeX ignores blanks following an escaped character; if you want a blank (as
2937 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2938
2939 sub _latex_escape {
2940   my $value = shift;
2941   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2942   $value =~ s/([<>])/\$$1\$/g;
2943   $value;
2944 }
2945
2946 #utility methods for print_*
2947
2948 sub _translate_old_latex_format {
2949   warn "_translate_old_latex_format called\n"
2950     if $DEBUG; 
2951
2952   my @template = ();
2953   while ( @_ ) {
2954     my $line = shift;
2955   
2956     if ( $line =~ /^%%Detail\s*$/ ) {
2957   
2958       push @template, q![@--!,
2959                       q!  foreach my $_tr_line (@detail_items) {!,
2960                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2961                       q!      $_tr_line->{'description'} .= !, 
2962                       q!        "\\tabularnewline\n~~".!,
2963                       q!        join( "\\tabularnewline\n~~",!,
2964                       q!          @{$_tr_line->{'ext_description'}}!,
2965                       q!        );!,
2966                       q!    }!;
2967
2968       while ( ( my $line_item_line = shift )
2969               !~ /^%%EndDetail\s*$/                            ) {
2970         $line_item_line =~ s/'/\\'/g;    # nice LTS
2971         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2972         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2973         push @template, "    \$OUT .= '$line_item_line';";
2974       }
2975   
2976       push @template, '}',
2977                       '--@]';
2978
2979     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2980
2981       push @template, '[@--',
2982                       '  foreach my $_tr_line (@total_items) {';
2983
2984       while ( ( my $total_item_line = shift )
2985               !~ /^%%EndTotalDetails\s*$/                      ) {
2986         $total_item_line =~ s/'/\\'/g;    # nice LTS
2987         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2988         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2989         push @template, "    \$OUT .= '$total_item_line';";
2990       }
2991
2992       push @template, '}',
2993                       '--@]';
2994
2995     } else {
2996       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2997       push @template, $line;  
2998     }
2999   
3000   }
3001
3002   if ($DEBUG) {
3003     warn "$_\n" foreach @template;
3004   }
3005
3006   (@template);
3007 }
3008
3009 sub terms {
3010   my $self = shift;
3011
3012   #check for an invoice- specific override (eventually)
3013   
3014   #check for a customer- specific override
3015   return $self->cust_main->invoice_terms
3016     if $self->cust_main->invoice_terms;
3017
3018   #use configured default
3019   $conf->config('invoice_default_terms') || '';
3020 }
3021
3022 sub due_date {
3023   my $self = shift;
3024   my $duedate = '';
3025   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3026     $duedate = $self->_date() + ( $1 * 86400 );
3027   }
3028   $duedate;
3029 }
3030
3031 sub due_date2str {
3032   my $self = shift;
3033   $self->due_date ? time2str(shift, $self->due_date) : '';
3034 }
3035
3036 sub balance_due_msg {
3037   my $self = shift;
3038   my $msg = 'Balance Due';
3039   return $msg unless $self->terms;
3040   if ( $self->due_date ) {
3041     $msg .= ' - Please pay by '. $self->due_date2str('%x');
3042   } elsif ( $self->terms ) {
3043     $msg .= ' - '. $self->terms;
3044   }
3045   $msg;
3046 }
3047
3048 sub balance_due_date {
3049   my $self = shift;
3050   my $duedate = '';
3051   if (    $conf->exists('invoice_default_terms') 
3052        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3053     $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
3054   }
3055   $duedate;
3056 }
3057
3058 =item invnum_date_pretty
3059
3060 Returns a string with the invoice number and date, for example:
3061 "Invoice #54 (3/20/2008)"
3062
3063 =cut
3064
3065 sub invnum_date_pretty {
3066   my $self = shift;
3067   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3068 }
3069
3070 =item _date_pretty
3071
3072 Returns a string with the date, for example: "3/20/2008"
3073
3074 =cut
3075
3076 sub _date_pretty {
3077   my $self = shift;
3078   time2str('%x', $self->_date);
3079 }
3080
3081 sub _items_sections {
3082   my $self = shift;
3083   my $late = shift;
3084   my $summarypage = shift;
3085   my $escape = shift;
3086
3087   my %s = ();
3088   my %l = ();
3089   my %not_tax = ();
3090
3091   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3092   {
3093
3094
3095       my $usage = $cust_bill_pkg->usage;
3096
3097       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3098         next if ( $display->summary && $summarypage );
3099
3100         my $desc = $display->section;
3101         my $type = $display->type;
3102
3103         if ( $cust_bill_pkg->pkgnum > 0 ) {
3104           $not_tax{$desc} = 1;
3105         }
3106
3107         if ( $display->post_total && !$summarypage ) {
3108           if (! $type || $type eq 'S') {
3109             $l{$desc} += $cust_bill_pkg->setup
3110               if ( $cust_bill_pkg->setup != 0 );
3111           }
3112
3113           if (! $type) {
3114             $l{$desc} += $cust_bill_pkg->recur
3115               if ( $cust_bill_pkg->recur != 0 );
3116           }
3117
3118           if ($type && $type eq 'R') {
3119             $l{$desc} += $cust_bill_pkg->recur - $usage
3120               if ( $cust_bill_pkg->recur != 0 );
3121           }
3122           
3123           if ($type && $type eq 'U') {
3124             $l{$desc} += $usage;
3125           }
3126
3127         } else {
3128           if (! $type || $type eq 'S') {
3129             $s{$desc} += $cust_bill_pkg->setup
3130               if ( $cust_bill_pkg->setup != 0 );
3131           }
3132
3133           if (! $type) {
3134             $s{$desc} += $cust_bill_pkg->recur
3135               if ( $cust_bill_pkg->recur != 0 );
3136           }
3137
3138           if ($type && $type eq 'R') {
3139             $s{$desc} += $cust_bill_pkg->recur - $usage
3140               if ( $cust_bill_pkg->recur != 0 );
3141           }
3142           
3143           if ($type && $type eq 'U') {
3144             $s{$desc} += $usage;
3145           }
3146
3147         }
3148
3149       }
3150
3151   }
3152
3153   my %cache = map { $_->categoryname => $_ }
3154               qsearch( 'pkg_category', {disabled => 'Y'} );
3155   $cache{$_->categoryname} = $_
3156     foreach qsearch( 'pkg_category', {disabled => ''} );
3157
3158   push @$late, map { { 'description' => &{$escape}($_),
3159                        'subtotal'    => $l{$_},
3160                        'post_total'  => 1,
3161                    } }
3162                  sort { $cache{$a}->weight <=> $cache{$b}->weight } keys %l;
3163
3164   map { { 'description' => &{$escape}($_),
3165           'subtotal'    => $s{$_},
3166           'summarized'  => $not_tax{$_} ? '' : 'Y',
3167           'tax_section' => $not_tax{$_} ? '' : 'Y',
3168       } }
3169     sort { $cache{$a}->weight <=> $cache{$b}->weight }
3170     ( $summarypage
3171         ? ( grep { exists($s{$_}) || !$cache{$_}->disabled } keys %cache )
3172         : ( keys %s )
3173     );
3174
3175 }
3176
3177 sub _items {
3178   my $self = shift;
3179
3180   #my @display = scalar(@_)
3181   #              ? @_
3182   #              : qw( _items_previous _items_pkg );
3183   #              #: qw( _items_pkg );
3184   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
3185   my @display = qw( _items_previous _items_pkg );
3186
3187   my @b = ();
3188   foreach my $display ( @display ) {
3189     push @b, $self->$display(@_);
3190   }
3191   @b;
3192 }
3193
3194 sub _items_previous {
3195   my $self = shift;
3196   my $cust_main = $self->cust_main;
3197   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3198   my @b = ();
3199   foreach ( @pr_cust_bill ) {
3200     push @b, {
3201       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
3202                        ' ('. time2str('%x',$_->_date). ')',
3203       #'pkgpart'     => 'N/A',
3204       'pkgnum'      => 'N/A',
3205       'amount'      => sprintf("%.2f", $_->owed),
3206     };
3207   }
3208   @b;
3209
3210   #{
3211   #    'description'     => 'Previous Balance',
3212   #    #'pkgpart'         => 'N/A',
3213   #    'pkgnum'          => 'N/A',
3214   #    'amount'          => sprintf("%10.2f", $pr_total ),
3215   #    'ext_description' => [ map {
3216   #                                 "Invoice ". $_->invnum.
3217   #                                 " (". time2str("%x",$_->_date). ") ".
3218   #                                 sprintf("%10.2f", $_->owed)
3219   #                         } @pr_cust_bill ],
3220
3221   #};
3222 }
3223
3224 sub _items_pkg {
3225   my $self = shift;
3226   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
3227   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3228 }
3229
3230 sub _taxsort {
3231   return 0 unless $a cmp $b;
3232   return -1 if $b eq 'Tax';
3233   return 1 if $a eq 'Tax';
3234   return -1 if $b eq 'Other surcharges';
3235   return 1 if $a eq 'Other surcharges';
3236   $a cmp $b;
3237 }
3238
3239 sub _items_tax {
3240   my $self = shift;
3241   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
3242   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
3243 }
3244
3245 sub _items_cust_bill_pkg {
3246   my $self = shift;
3247   my $cust_bill_pkg = shift;
3248   my %opt = @_;
3249
3250   my $format = $opt{format} || '';
3251   my $escape_function = $opt{escape_function} || sub { shift };
3252   my $format_function = $opt{format_function} || '';
3253   my $unsquelched = $opt{unsquelched} || '';
3254   my $section = $opt{section}->{description} if $opt{section};
3255   my $summary_page = $opt{summary_page} || '';
3256
3257   my @b = ();
3258   my ($s, $r, $u) = ( undef, undef, undef );
3259   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
3260   {
3261
3262     foreach ( $s, $r, $u ) {
3263       if ( $_ && !$cust_bill_pkg->hidden ) {
3264         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3265         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3266         push @b, { %$_ };
3267         $_ = undef;
3268       }
3269     }
3270
3271     foreach my $display ( grep { defined($section)
3272                                  ? $_->section eq $section
3273                                  : 1
3274                                }
3275                           grep { $_->summary || !$summary_page }
3276                           $cust_bill_pkg->cust_bill_pkg_display
3277                         )
3278     {
3279
3280       my $type = $display->type;
3281
3282       my $desc = $cust_bill_pkg->desc;
3283       $desc = substr($desc, 0, 50). '...'
3284         if $format eq 'latex' && length($desc) > 50;
3285
3286       my %details_opt = ( 'format'          => $format,
3287                           'escape_function' => $escape_function,
3288                           'format_function' => $format_function,
3289                         );
3290
3291       if ( $cust_bill_pkg->pkgnum > 0 ) {
3292
3293         my $cust_pkg = $cust_bill_pkg->cust_pkg;
3294
3295         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
3296
3297           my $description = $desc;
3298           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
3299
3300           my @d = ();
3301           push @d, map &{$escape_function}($_),
3302                        $cust_pkg->h_labels_short($self->_date)
3303             unless $cust_pkg->part_pkg->hide_svc_detail
3304                 || $cust_bill_pkg->hidden;
3305           push @d, $cust_bill_pkg->details(%details_opt)
3306             if $cust_bill_pkg->recur == 0;
3307
3308           if ( $cust_bill_pkg->hidden ) {
3309             $s->{amount}      += $cust_bill_pkg->setup;
3310             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
3311             push @{ $s->{ext_description} }, @d;
3312           } else {
3313             $s = {
3314               description     => $description,
3315               #pkgpart         => $part_pkg->pkgpart,
3316               pkgnum          => $cust_bill_pkg->pkgnum,
3317               amount          => $cust_bill_pkg->setup,
3318               unit_amount     => $cust_bill_pkg->unitsetup,
3319               quantity        => $cust_bill_pkg->quantity,
3320               ext_description => \@d,
3321             };
3322           };
3323
3324         }
3325
3326         if ( $cust_bill_pkg->recur != 0 &&
3327              ( !$type || $type eq 'R' || $type eq 'U' )
3328            )
3329         {
3330
3331           my $is_summary = $display->summary;
3332           my $description = ($is_summary && $type && $type eq 'U')
3333                             ? "Usage charges" : $desc;
3334
3335           unless ( $conf->exists('disable_line_item_date_ranges') ) {
3336             $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
3337                             " - ". time2str("%x", $cust_bill_pkg->edate). ")";
3338           }
3339
3340           my @d = ();
3341
3342           #at least until cust_bill_pkg has "past" ranges in addition to
3343           #the "future" sdate/edate ones... see #3032
3344           my @dates = ( $self->_date );
3345           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
3346           push @dates, $prev->sdate if $prev;
3347
3348           push @d, map &{$escape_function}($_),
3349                        $cust_pkg->h_labels_short(@dates)
3350                                                  #$cust_bill_pkg->edate,
3351                                                  #$cust_bill_pkg->sdate)
3352             unless $cust_pkg->part_pkg->hide_svc_detail
3353                 || $cust_bill_pkg->itemdesc
3354                 || $cust_bill_pkg->hidden
3355                 || $is_summary && $type && $type eq 'U';
3356
3357           push @d, $cust_bill_pkg->details(%details_opt)
3358             unless ($is_summary || $type && $type eq 'R');
3359   
3360           my $amount = 0;
3361           if (!$type) {
3362             $amount = $cust_bill_pkg->recur;
3363           }elsif($type eq 'R') {
3364             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3365           }elsif($type eq 'U') {
3366             $amount = $cust_bill_pkg->usage;
3367           }
3368   
3369           if ( !$type || $type eq 'R' ) {
3370
3371             if ( $cust_bill_pkg->hidden ) {
3372               $r->{amount}      += $amount;
3373               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3374               push @{ $r->{ext_description} }, @d;
3375             } else {
3376               $r = {
3377                 description     => $description,
3378                 #pkgpart         => $part_pkg->pkgpart,
3379                 pkgnum          => $cust_bill_pkg->pkgnum,
3380                 amount          => $amount,
3381                 unit_amount     => $cust_bill_pkg->unitrecur,
3382                 quantity        => $cust_bill_pkg->quantity,
3383                 ext_description => \@d,
3384               };
3385             }
3386
3387           } elsif ( $amount ) {  # && $type eq 'U'
3388
3389             if ( $cust_bill_pkg->hidden ) {
3390               $u->{amount}      += $amount;
3391               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3392               push @{ $u->{ext_description} }, @d;
3393             } else {
3394               $u = {
3395                 description     => $description,
3396                 #pkgpart         => $part_pkg->pkgpart,
3397                 pkgnum          => $cust_bill_pkg->pkgnum,
3398                 amount          => $amount,
3399                 unit_amount     => $cust_bill_pkg->unitrecur,
3400                 quantity        => $cust_bill_pkg->quantity,
3401                 ext_description => \@d,
3402               };
3403             }
3404
3405           }
3406
3407         } # recurring or usage with recurring charge
3408
3409       } else { #pkgnum tax or one-shot line item (??)
3410
3411         if ( $cust_bill_pkg->setup != 0 ) {
3412           push @b, {
3413             'description' => $desc,
3414             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
3415           };
3416         }
3417         if ( $cust_bill_pkg->recur != 0 ) {
3418           push @b, {
3419             'description' => "$desc (".
3420                              time2str("%x", $cust_bill_pkg->sdate). ' - '.
3421                              time2str("%x", $cust_bill_pkg->edate). ')',
3422             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
3423           };
3424         }
3425
3426       }
3427
3428     }
3429
3430   }
3431
3432   foreach ( $s, $r, $u ) {
3433     if ( $_ ) {
3434       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3435       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3436       push @b, { %$_ };
3437     }
3438   }
3439
3440   @b;
3441
3442 }
3443
3444 sub _items_credits {
3445   my( $self, %opt ) = @_;
3446   my $trim_len = $opt{'trim_len'} || 60;
3447
3448   my @b;
3449   #credits
3450   foreach ( $self->cust_credited ) {
3451
3452     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3453
3454     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3455     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3456     $reason = " ($reason) " if $reason;
3457
3458     push @b, {
3459       #'description' => 'Credit ref\#'. $_->crednum.
3460       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
3461       #                 $reason,
3462       'description' => 'Credit applied '.
3463                        time2str("%x",$_->cust_credit->_date). $reason,
3464       'amount'      => sprintf("%.2f",$_->amount),
3465     };
3466   }
3467
3468   @b;
3469
3470 }
3471
3472 sub _items_payments {
3473   my $self = shift;
3474
3475   my @b;
3476   #get & print payments
3477   foreach ( $self->cust_bill_pay ) {
3478
3479     #something more elaborate if $_->amount ne ->cust_pay->paid ?
3480
3481     push @b, {
3482       'description' => "Payment received ".
3483                        time2str("%x",$_->cust_pay->_date ),
3484       'amount'      => sprintf("%.2f", $_->amount )
3485     };
3486   }
3487
3488   @b;
3489
3490 }
3491
3492 =item call_details [ OPTION => VALUE ... ]
3493
3494 Returns an array of CSV strings representing the call details for this invoice
3495 The only option available is the boolean prepend_billed_number
3496
3497 =cut
3498
3499 sub call_details {
3500   my ($self, %opt) = @_;
3501
3502   my $format_function = sub { shift };
3503
3504   if ($opt{prepend_billed_number}) {
3505     $format_function = sub {
3506       my $detail = shift;
3507       my $row = shift;
3508
3509       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3510       
3511     };
3512   }
3513
3514   my @details = map { $_->details( 'format_function' => $format_function,
3515                                    'escape_function' => sub{ return() },
3516                                  )
3517                     }
3518                   grep { $_->pkgnum }
3519                   $self->cust_bill_pkg;
3520   my $header = $details[0];
3521   ( $header, grep { $_ ne $header } @details );
3522 }
3523
3524
3525 =back
3526
3527 =head1 SUBROUTINES
3528
3529 =over 4
3530
3531 =item process_reprint
3532
3533 =cut
3534
3535 sub process_reprint {
3536   process_re_X('print', @_);
3537 }
3538
3539 =item process_reemail
3540
3541 =cut
3542
3543 sub process_reemail {
3544   process_re_X('email', @_);
3545 }
3546
3547 =item process_refax
3548
3549 =cut
3550
3551 sub process_refax {
3552   process_re_X('fax', @_);
3553 }
3554
3555 =item process_reftp
3556
3557 =cut
3558
3559 sub process_reftp {
3560   process_re_X('ftp', @_);
3561 }
3562
3563 =item respool
3564
3565 =cut
3566
3567 sub process_respool {
3568   process_re_X('spool', @_);
3569 }
3570
3571 use Storable qw(thaw);
3572 use Data::Dumper;
3573 use MIME::Base64;
3574 sub process_re_X {
3575   my( $method, $job ) = ( shift, shift );
3576   warn "$me process_re_X $method for job $job\n" if $DEBUG;
3577
3578   my $param = thaw(decode_base64(shift));
3579   warn Dumper($param) if $DEBUG;
3580
3581   re_X(
3582     $method,
3583     $job,
3584     %$param,
3585   );
3586
3587 }
3588
3589 sub re_X {
3590   my($method, $job, %param ) = @_;
3591   if ( $DEBUG ) {
3592     warn "re_X $method for job $job with param:\n".
3593          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
3594   }
3595
3596   #some false laziness w/search/cust_bill.html
3597   my $distinct = '';
3598   my $orderby = 'ORDER BY cust_bill._date';
3599
3600   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3601
3602   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3603      
3604   my @cust_bill = qsearch( {
3605     #'select'    => "cust_bill.*",
3606     'table'     => 'cust_bill',
3607     'addl_from' => $addl_from,
3608     'hashref'   => {},
3609     'extra_sql' => $extra_sql,
3610     'order_by'  => $orderby,
3611     'debug' => 1,
3612   } );
3613
3614   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3615
3616   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3617     if $DEBUG;
3618
3619   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3620   foreach my $cust_bill ( @cust_bill ) {
3621     $cust_bill->$method();
3622
3623     if ( $job ) { #progressbar foo
3624       $num++;
3625       if ( time - $min_sec > $last ) {
3626         my $error = $job->update_statustext(
3627           int( 100 * $num / scalar(@cust_bill) )
3628         );
3629         die $error if $error;
3630         $last = time;
3631       }
3632     }
3633
3634   }
3635
3636 }
3637
3638 =back
3639
3640 =head1 CLASS METHODS
3641
3642 =over 4
3643
3644 =item owed_sql
3645
3646 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3647
3648 =cut
3649
3650 sub owed_sql {
3651   my $class = shift;
3652   'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3653 }
3654
3655 =item net_sql
3656
3657 Returns an SQL fragment to retreive the net amount (charged minus credited).
3658
3659 =cut
3660
3661 sub net_sql {
3662   my $class = shift;
3663   'charged - '. $class->credited_sql;
3664 }
3665
3666 =item paid_sql
3667
3668 Returns an SQL fragment to retreive the amount paid against this invoice.
3669
3670 =cut
3671
3672 sub paid_sql {
3673   #my $class = shift;
3674   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3675        WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
3676 }
3677
3678 =item credited_sql
3679
3680 Returns an SQL fragment to retreive the amount credited against this invoice.
3681
3682 =cut
3683
3684 sub credited_sql {
3685   #my $class = shift;
3686   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3687        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
3688 }
3689
3690 =item search_sql HASHREF
3691
3692 Class method which returns an SQL WHERE fragment to search for parameters
3693 specified in HASHREF.  Valid parameters are
3694
3695 =over 4
3696
3697 =item begin
3698
3699 Epoch date (UNIX timestamp) setting a lower bound for _date values
3700
3701 =item end
3702
3703 Epoch date (UNIX timestamp) setting an upper bound for _date values
3704
3705 =item invnum_min
3706
3707 =item invnum_max
3708
3709 =item agentnum
3710
3711 =item owed
3712
3713 =item net
3714
3715 =item days
3716
3717 =item newest_percust
3718
3719 =back
3720
3721 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3722
3723 =cut
3724
3725 sub search_sql {
3726   my($class, $param) = @_;
3727   if ( $DEBUG ) {
3728     warn "$me search_sql called with params: \n".
3729          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
3730   }
3731
3732   my @search = ();
3733
3734   if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3735     push @search, "cust_bill._date >= $1";
3736   }
3737   if ( $param->{'end'} =~ /^(\d+)$/ ) {
3738     push @search, "cust_bill._date < $1";
3739   }
3740   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3741     push @search, "cust_bill.invnum >= $1";
3742   }
3743   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3744     push @search, "cust_bill.invnum <= $1";
3745   }
3746   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3747     push @search, "cust_main.agentnum = $1";
3748   }
3749
3750   push @search, '0 != '. FS::cust_bill->owed_sql
3751     if $param->{'open'};
3752
3753   push @search, '0 != '. FS::cust_bill->net_sql
3754     if $param->{'net'};
3755
3756   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3757     if $param->{'days'};
3758
3759   if ( $param->{'newest_percust'} ) {
3760
3761     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3762     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3763
3764     my @newest_where = map { my $x = $_;
3765                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
3766                              $x;
3767                            }
3768                            grep ! /^cust_main./, @search;
3769     my $newest_where = scalar(@newest_where)
3770                          ? ' AND '. join(' AND ', @newest_where)
3771                          : '';
3772
3773
3774     push @search, "cust_bill._date = (
3775       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3776         WHERE newest_cust_bill.custnum = cust_bill.custnum
3777           $newest_where
3778     )";
3779
3780   }
3781
3782   my $curuser = $FS::CurrentUser::CurrentUser;
3783   if ( $curuser->username eq 'fs_queue'
3784        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3785     my $username = $1;
3786     my $newuser = qsearchs('access_user', {
3787       'username' => $username,
3788       'disabled' => '',
3789     } );
3790     if ( $newuser ) {
3791       $curuser = $newuser;
3792     } else {
3793       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3794     }
3795   }
3796
3797   push @search, $curuser->agentnums_sql;
3798
3799   join(' AND ', @search );
3800
3801 }
3802
3803 =back
3804
3805 =head1 BUGS
3806
3807 The delete method.
3808
3809 =head1 SEE ALSO
3810
3811 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3812 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3813 documentation.
3814
3815 =cut
3816
3817 1;
3818