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