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