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