82023f6b5010fb75c601efe2aa19af26ef13465b
[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 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_bill_pkg;
20 use FS::cust_credit;
21 use FS::cust_pay;
22 use FS::cust_pkg;
23 use FS::cust_credit_bill;
24 use FS::pay_batch;
25 use FS::cust_pay_batch;
26 use FS::cust_bill_event;
27 use FS::part_pkg;
28 use FS::cust_bill_pay;
29 use FS::cust_bill_pay_batch;
30 use FS::part_bill_event;
31 use FS::payby;
32
33 @ISA = qw( FS::cust_main_Mixin FS::Record );
34
35 $DEBUG = 0;
36 $me = '[FS::cust_bill]';
37
38 #ask FS::UID to run this stuff for us later
39 FS::UID->install_callback( sub { 
40   $conf = new FS::Conf;
41   $money_char = $conf->config('money_char') || '$';  
42 } );
43
44 =head1 NAME
45
46 FS::cust_bill - Object methods for cust_bill records
47
48 =head1 SYNOPSIS
49
50   use FS::cust_bill;
51
52   $record = new FS::cust_bill \%hash;
53   $record = new FS::cust_bill { 'column' => 'value' };
54
55   $error = $record->insert;
56
57   $error = $new_record->replace($old_record);
58
59   $error = $record->delete;
60
61   $error = $record->check;
62
63   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
64
65   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
66
67   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
68
69   @cust_pay_objects = $cust_bill->cust_pay;
70
71   $tax_amount = $record->tax;
72
73   @lines = $cust_bill->print_text;
74   @lines = $cust_bill->print_text $time;
75
76 =head1 DESCRIPTION
77
78 An FS::cust_bill object represents an invoice; a declaration that a customer
79 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
80 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
81 following fields are currently supported:
82
83 =over 4
84
85 =item invnum - primary key (assigned automatically for new invoices)
86
87 =item custnum - customer (see L<FS::cust_main>)
88
89 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
90 L<Time::Local> and L<Date::Parse> for conversion functions.
91
92 =item charged - amount of this invoice
93
94 =item printed - deprecated
95
96 =item closed - books closed flag, empty or `Y'
97
98 =back
99
100 =head1 METHODS
101
102 =over 4
103
104 =item new HASHREF
105
106 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
107 Invoices are normally created by calling the bill method of a customer object
108 (see L<FS::cust_main>).
109
110 =cut
111
112 sub table { 'cust_bill'; }
113
114 sub cust_linked { $_[0]->cust_main_custnum; } 
115 sub cust_unlinked_msg {
116   my $self = shift;
117   "WARNING: can't find cust_main.custnum ". $self->custnum.
118   ' (cust_bill.invnum '. $self->invnum. ')';
119 }
120
121 =item insert
122
123 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
124 returns the error, otherwise returns false.
125
126 =item delete
127
128 This method now works but you probably shouldn't use it.  Instead, apply a
129 credit against the invoice.
130
131 Using this method to delete invoices outright is really, really bad.  There
132 would be no record you ever posted this invoice, and there are no check to
133 make sure charged = 0 or that there are no associated cust_bill_pkg records.
134
135 Really, don't use it.
136
137 =cut
138
139 sub delete {
140   my $self = shift;
141   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
142   $self->SUPER::delete(@_);
143 }
144
145 =item replace OLD_RECORD
146
147 Replaces the OLD_RECORD with this one in the database.  If there is an error,
148 returns the error, otherwise returns false.
149
150 Only printed may be changed.  printed is normally updated by calling the
151 collect method of a customer object (see L<FS::cust_main>).
152
153 =cut
154
155 #replace can be inherited from Record.pm
156
157 # replace_check is now the preferred way to #implement replace data checks
158 # (so $object->replace() works without an argument)
159
160 sub replace_check {
161   my( $new, $old ) = ( shift, shift );
162   return "Can't change custnum!" unless $old->custnum == $new->custnum;
163   #return "Can't change _date!" unless $old->_date eq $new->_date;
164   return "Can't change _date!" unless $old->_date == $new->_date;
165   return "Can't change charged!" unless $old->charged == $new->charged
166                                      || $old->charged == 0;
167
168   '';
169 }
170
171 =item check
172
173 Checks all fields to make sure this is a valid invoice.  If there is an error,
174 returns the error, otherwise returns false.  Called by the insert and replace
175 methods.
176
177 =cut
178
179 sub check {
180   my $self = shift;
181
182   my $error =
183     $self->ut_numbern('invnum')
184     || $self->ut_number('custnum')
185     || $self->ut_numbern('_date')
186     || $self->ut_money('charged')
187     || $self->ut_numbern('printed')
188     || $self->ut_enum('closed', [ '', 'Y' ])
189   ;
190   return $error if $error;
191
192   return "Unknown customer"
193     unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
194
195   $self->_date(time) unless $self->_date;
196
197   $self->printed(0) if $self->printed eq '';
198
199   $self->SUPER::check;
200 }
201
202 =item previous
203
204 Returns a list consisting of the total previous balance for this customer, 
205 followed by the previous outstanding invoices (as FS::cust_bill objects also).
206
207 =cut
208
209 sub previous {
210   my $self = shift;
211   my $total = 0;
212   my @cust_bill = sort { $a->_date <=> $b->_date }
213     grep { $_->owed != 0 && $_->_date < $self->_date }
214       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
215   ;
216   foreach ( @cust_bill ) { $total += $_->owed; }
217   $total, @cust_bill;
218 }
219
220 =item cust_bill_pkg
221
222 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
223
224 =cut
225
226 sub cust_bill_pkg {
227   my $self = shift;
228   qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
229 }
230
231 =item cust_pkg
232
233 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
234 this invoice.
235
236 =cut
237
238 sub cust_pkg {
239   my $self = shift;
240   my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
241   my %saw = ();
242   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
243 }
244
245 =item open_cust_bill_pkg
246
247 Returns the open line items for this invoice.
248
249 Note that cust_bill_pkg with both setup and recur fees are returned as two
250 separate line items, each with only one fee.
251
252 =cut
253
254 # modeled after cust_main::open_cust_bill
255 sub open_cust_bill_pkg {
256   my $self = shift;
257
258   # grep { $_->owed > 0 } $self->cust_bill_pkg
259
260   my %other = ( 'recur' => 'setup',
261                 'setup' => 'recur', );
262   my @open = ();
263   foreach my $field ( qw( recur setup )) {
264     push @open, map  { $_->set( $other{$field}, 0 ); $_; }
265                 grep { $_->owed($field) > 0 }
266                 $self->cust_bill_pkg;
267   }
268
269   @open;
270 }
271
272 =item cust_bill_event
273
274 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
275 invoice.
276
277 =cut
278
279 sub cust_bill_event {
280   my $self = shift;
281   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
282 }
283
284
285 =item cust_main
286
287 Returns the customer (see L<FS::cust_main>) for this invoice.
288
289 =cut
290
291 sub cust_main {
292   my $self = shift;
293   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
294 }
295
296 =item cust_suspend_if_balance_over AMOUNT
297
298 Suspends the customer associated with this invoice if the total amount owed on
299 this invoice and all older invoices is greater than the specified amount.
300
301 Returns a list: an empty list on success or a list of errors.
302
303 =cut
304
305 sub cust_suspend_if_balance_over {
306   my( $self, $amount ) = ( shift, shift );
307   my $cust_main = $self->cust_main;
308   if ( $cust_main->total_owed_date($self->_date) < $amount ) {
309     return ();
310   } else {
311     $cust_main->suspend(@_);
312   }
313 }
314
315 =item cust_credit
316
317 Depreciated.  See the cust_credited method.
318
319  #Returns a list consisting of the total previous credited (see
320  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
321  #outstanding credits (FS::cust_credit objects).
322
323 =cut
324
325 sub cust_credit {
326   use Carp;
327   croak "FS::cust_bill->cust_credit depreciated; see ".
328         "FS::cust_bill->cust_credit_bill";
329   #my $self = shift;
330   #my $total = 0;
331   #my @cust_credit = sort { $a->_date <=> $b->_date }
332   #  grep { $_->credited != 0 && $_->_date < $self->_date }
333   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
334   #;
335   #foreach (@cust_credit) { $total += $_->credited; }
336   #$total, @cust_credit;
337 }
338
339 =item cust_pay
340
341 Depreciated.  See the cust_bill_pay method.
342
343 #Returns all payments (see L<FS::cust_pay>) for this invoice.
344
345 =cut
346
347 sub cust_pay {
348   use Carp;
349   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
350   #my $self = shift;
351   #sort { $a->_date <=> $b->_date }
352   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
353   #;
354 }
355
356 =item cust_bill_pay
357
358 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
359
360 =cut
361
362 sub cust_bill_pay {
363   my $self = shift;
364   sort { $a->_date <=> $b->_date }
365     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
366 }
367
368 =item cust_credited
369
370 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
371
372 =cut
373
374 sub cust_credited {
375   my $self = shift;
376   sort { $a->_date <=> $b->_date }
377     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
378   ;
379 }
380
381 =item tax
382
383 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
384
385 =cut
386
387 sub tax {
388   my $self = shift;
389   my $total = 0;
390   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
391                                              'pkgnum' => 0 } );
392   foreach (@taxlines) { $total += $_->setup; }
393   $total;
394 }
395
396 =item owed
397
398 Returns the amount owed (still outstanding) on this invoice, which is charged
399 minus all payment applications (see L<FS::cust_bill_pay>) and credit
400 applications (see L<FS::cust_credit_bill>).
401
402 =cut
403
404 sub owed {
405   my $self = shift;
406   my $balance = $self->charged;
407   $balance -= $_->amount foreach ( $self->cust_bill_pay );
408   $balance -= $_->amount foreach ( $self->cust_credited );
409   $balance = sprintf( "%.2f", $balance);
410   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
411   $balance;
412 }
413
414 =item apply_payments_and_credits
415
416 =cut
417
418 sub apply_payments_and_credits {
419   my $self = shift;
420
421   local $SIG{HUP} = 'IGNORE';
422   local $SIG{INT} = 'IGNORE';
423   local $SIG{QUIT} = 'IGNORE';
424   local $SIG{TERM} = 'IGNORE';
425   local $SIG{TSTP} = 'IGNORE';
426   local $SIG{PIPE} = 'IGNORE';
427
428   my $oldAutoCommit = $FS::UID::AutoCommit;
429   local $FS::UID::AutoCommit = 0;
430   my $dbh = dbh;
431
432   $self->select_for_update; #mutex
433
434   my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
435   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
436
437   while ( $self->owed > 0 and ( @payments || @credits ) ) {
438
439     my $app = '';
440     if ( @payments && @credits ) {
441
442       #decide which goes first by weight of top (unapplied) line item
443
444       my @open_lineitems = $self->open_cust_bill_pkg;
445
446       my $max_pay_weight =
447         max( map  { $_->part_pkg->pay_weight || 0 }
448              grep { $_ }
449              map  { $_->cust_pkg }
450                   @open_lineitems
451            );
452       my $max_credit_weight =
453         max( map  { $_->part_pkg->credit_weight || 0 }
454              grep { $_ } 
455              map  { $_->cust_pkg }
456                   @open_lineitems
457            );
458
459       #if both are the same... payments first?  it has to be something
460       if ( $max_pay_weight >= $max_credit_weight ) {
461         $app = 'pay';
462       } else {
463         $app = 'credit';
464       }
465     
466     } elsif ( @payments ) {
467       $app = 'pay';
468     } elsif ( @credits ) {
469       $app = 'credit';
470     } else {
471       die "guru meditation #12 and 35";
472     }
473
474     if ( $app eq 'pay' ) {
475
476       my $payment = shift @payments;
477
478       $app = new FS::cust_bill_pay {
479         'paynum'  => $payment->paynum,
480         'amount'  => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
481       };
482
483     } elsif ( $app eq 'credit' ) {
484
485       my $credit = shift @credits;
486
487       $app = new FS::cust_credit_bill {
488         'crednum' => $credit->crednum,
489         'amount'  => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
490       };
491
492     } else {
493       die "guru meditation #12 and 35";
494     }
495
496     $app->invnum( $self->invnum );
497
498     my $error = $app->insert;
499     if ( $error ) {
500       $dbh->rollback if $oldAutoCommit;
501       return "Error inserting ". $app->table. " record: $error";
502     }
503     die $error if $error;
504
505   }
506
507   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
508   ''; #no error
509
510 }
511
512 =item generate_email PARAMHASH
513
514 PARAMHASH can contain the following:
515
516 =over 4
517
518 =item from       => sender address, required
519
520 =item tempate    => alternate template name, optional
521
522 =item print_text => text attachment arrayref, optional
523
524 =item subject    => email subject, optional
525
526 =back
527
528 Returns an argument list to be passed to L<FS::Misc::send_email>.
529
530 =cut
531
532 use MIME::Entity;
533
534 sub generate_email {
535
536   my $self = shift;
537   my %args = @_;
538
539   my $me = '[FS::cust_bill::generate_email]';
540
541   my %return = (
542     'from'      => $args{'from'},
543     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
544   );
545
546   if (ref($args{'to'}) eq 'ARRAY') {
547     $return{'to'} = $args{'to'};
548   } else {
549     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
550                            $self->cust_main->invoicing_list
551                     ];
552   }
553
554   if ( $conf->exists('invoice_html') ) {
555
556     warn "$me creating HTML/text multipart message"
557       if $DEBUG;
558
559     $return{'nobody'} = 1;
560
561     my $alternative = build MIME::Entity
562       'Type'        => 'multipart/alternative',
563       'Encoding'    => '7bit',
564       'Disposition' => 'inline'
565     ;
566
567     my $data;
568     if ( $conf->exists('invoice_email_pdf')
569          and scalar($conf->config('invoice_email_pdf_note')) ) {
570
571       warn "$me using 'invoice_email_pdf_note' in multipart message"
572         if $DEBUG;
573       $data = [ map { $_ . "\n" }
574                     $conf->config('invoice_email_pdf_note')
575               ];
576
577     } else {
578
579       warn "$me not using 'invoice_email_pdf_note' in multipart message"
580         if $DEBUG;
581       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
582         $data = $args{'print_text'};
583       } else {
584         $data = [ $self->print_text('', $args{'template'}) ];
585       }
586
587     }
588
589     $alternative->attach(
590       'Type'        => 'text/plain',
591       #'Encoding'    => 'quoted-printable',
592       'Encoding'    => '7bit',
593       'Data'        => $data,
594       'Disposition' => 'inline',
595     );
596
597     $args{'from'} =~ /\@([\w\.\-]+)/;
598     my $from = $1 || 'example.com';
599     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
600
601     my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
602     my $file;
603     if ( defined($args{'template'}) && length($args{'template'})
604          && -e "$path/logo_". $args{'template'}. ".png"
605        )
606     {
607       $file = "$path/logo_". $args{'template'}. ".png";
608     } else {
609       $file = "$path/logo.png";
610     }
611
612     my $image = build MIME::Entity
613       'Type'       => 'image/png',
614       'Encoding'   => 'base64',
615       'Path'       => $file,
616       'Filename'   => 'logo.png',
617       'Content-ID' => "<$content_id>",
618     ;
619
620     $alternative->attach(
621       'Type'        => 'text/html',
622       'Encoding'    => 'quoted-printable',
623       'Data'        => [ '<html>',
624                          '  <head>',
625                          '    <title>',
626                          '      '. encode_entities($return{'subject'}), 
627                          '    </title>',
628                          '  </head>',
629                          '  <body bgcolor="#e8e8e8">',
630                          $self->print_html('', $args{'template'}, $content_id),
631                          '  </body>',
632                          '</html>',
633                        ],
634       'Disposition' => 'inline',
635       #'Filename'    => 'invoice.pdf',
636     );
637
638     if ( $conf->exists('invoice_email_pdf') ) {
639
640       #attaching pdf too:
641       # multipart/mixed
642       #   multipart/related
643       #     multipart/alternative
644       #       text/plain
645       #       text/html
646       #     image/png
647       #   application/pdf
648
649       my $related = build MIME::Entity 'Type'     => 'multipart/related',
650                                        'Encoding' => '7bit';
651
652       #false laziness w/Misc::send_email
653       $related->head->replace('Content-type',
654         $related->mime_type.
655         '; boundary="'. $related->head->multipart_boundary. '"'.
656         '; type=multipart/alternative'
657       );
658
659       $related->add_part($alternative);
660
661       $related->add_part($image);
662
663       my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
664
665       $return{'mimeparts'} = [ $related, $pdf ];
666
667     } else {
668
669       #no other attachment:
670       # multipart/related
671       #   multipart/alternative
672       #     text/plain
673       #     text/html
674       #   image/png
675
676       $return{'content-type'} = 'multipart/related';
677       $return{'mimeparts'} = [ $alternative, $image ];
678       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
679       #$return{'disposition'} = 'inline';
680
681     }
682   
683   } else {
684
685     if ( $conf->exists('invoice_email_pdf') ) {
686       warn "$me creating PDF attachment"
687         if $DEBUG;
688
689       #mime parts arguments a la MIME::Entity->build().
690       $return{'mimeparts'} = [
691         { $self->mimebuild_pdf('', $args{'template'}) }
692       ];
693     }
694   
695     if ( $conf->exists('invoice_email_pdf')
696          and scalar($conf->config('invoice_email_pdf_note')) ) {
697
698       warn "$me using 'invoice_email_pdf_note'"
699         if $DEBUG;
700       $return{'body'} = [ map { $_ . "\n" }
701                               $conf->config('invoice_email_pdf_note')
702                         ];
703
704     } else {
705
706       warn "$me not using 'invoice_email_pdf_note'"
707         if $DEBUG;
708       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
709         $return{'body'} = $args{'print_text'};
710       } else {
711         $return{'body'} = [ $self->print_text('', $args{'template'}) ];
712       }
713
714     }
715
716   }
717
718   %return;
719
720 }
721
722 =item mimebuild_pdf
723
724 Returns a list suitable for passing to MIME::Entity->build(), representing
725 this invoice as PDF attachment.
726
727 =cut
728
729 sub mimebuild_pdf {
730   my $self = shift;
731   (
732     'Type'        => 'application/pdf',
733     'Encoding'    => 'base64',
734     'Data'        => [ $self->print_pdf(@_) ],
735     'Disposition' => 'attachment',
736     'Filename'    => 'invoice.pdf',
737   );
738 }
739
740 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
741
742 Sends this invoice to the destinations configured for this customer: sends
743 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
744
745 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
746
747 AGENTNUM, if specified, means that this invoice will only be sent for customers
748 of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
749 single agent) or an arrayref of agentnums.
750
751 INVOICE_FROM, if specified, overrides the default email invoice From: address.
752
753 =cut
754
755 sub queueable_send {
756   my %opt = @_;
757
758   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
759     or die "invalid invoice number: " . $opt{invnum};
760
761   my @args = ( $opt{template}, $opt{agentnum} );
762   push @args, $opt{invoice_from}
763     if exists($opt{invoice_from}) && $opt{invoice_from};
764
765   my $error = $self->send( @args );
766   die $error if $error;
767
768 }
769
770 sub send {
771   my $self = shift;
772   my $template = scalar(@_) ? shift : '';
773   if ( scalar(@_) && $_[0]  ) {
774     my $agentnums = ref($_[0]) ? shift : [ shift ];
775     return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
776   }
777
778   my $invoice_from =
779     scalar(@_)
780       ? shift
781       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
782
783   my @invoicing_list = $self->cust_main->invoicing_list;
784
785   $self->email($template, $invoice_from)
786     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
787
788   $self->print($template)
789     if grep { $_ eq 'POST' } @invoicing_list; #postal
790
791   $self->fax($template)
792     if grep { $_ eq 'FAX' } @invoicing_list; #fax
793
794   '';
795
796 }
797
798 =item email [ TEMPLATENAME  [ , INVOICE_FROM ] ] 
799
800 Emails this invoice.
801
802 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
803
804 INVOICE_FROM, if specified, overrides the default email invoice From: address.
805
806 =cut
807
808 sub queueable_email {
809   my %opt = @_;
810
811   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
812     or die "invalid invoice number: " . $opt{invnum};
813
814   my @args = ( $opt{template} );
815   push @args, $opt{invoice_from}
816     if exists($opt{invoice_from}) && $opt{invoice_from};
817
818   my $error = $self->email( @args );
819   die $error if $error;
820
821 }
822
823 sub email {
824   my $self = shift;
825   my $template = scalar(@_) ? shift : '';
826   my $invoice_from =
827     scalar(@_)
828       ? shift
829       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
830
831   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
832                             $self->cust_main->invoicing_list;
833
834   #better to notify this person than silence
835   @invoicing_list = ($invoice_from) unless @invoicing_list;
836
837   my $error = send_email(
838     $self->generate_email(
839       'from'       => $invoice_from,
840       'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
841       'template'   => $template,
842     )
843   );
844   die "can't email invoice: $error\n" if $error;
845   #die "$error\n" if $error;
846
847 }
848
849 =item lpr_data [ TEMPLATENAME ]
850
851 Returns the postscript or plaintext for this invoice as an arrayref.
852
853 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
854
855 =cut
856
857 sub lpr_data {
858   my( $self, $template) = @_;
859   $conf->exists('invoice_latex')
860     ? [ $self->print_ps('', $template) ]
861     : [ $self->print_text('', $template) ];
862 }
863
864 =item print [ TEMPLATENAME ]
865
866 Prints this invoice.
867
868 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
869
870 =cut
871
872 sub print {
873   my $self = shift;
874   my $template = scalar(@_) ? shift : '';
875
876   do_print $self->lpr_data($template);
877 }
878
879 =item fax [ TEMPLATENAME ] 
880
881 Faxes this invoice.
882
883 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
884
885 =cut
886
887 sub fax {
888   my $self = shift;
889   my $template = scalar(@_) ? shift : '';
890
891   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
892     unless $conf->exists('invoice_latex');
893
894   my $dialstring = $self->cust_main->getfield('fax');
895   #Check $dialstring?
896
897   my $error = send_fax( 'docdata'    => $self->lpr_data($template),
898                         'dialstring' => $dialstring,
899                       );
900   die $error if $error;
901
902 }
903
904 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
905
906 Like B<send>, but only sends the invoice if it is the newest open invoice for
907 this customer.
908
909 =cut
910
911 sub send_if_newest {
912   my $self = shift;
913
914   return ''
915     if scalar(
916                grep { $_->owed > 0 } 
917                     qsearch('cust_bill', {
918                       'custnum' => $self->custnum,
919                       #'_date'   => { op=>'>', value=>$self->_date },
920                       'invnum'  => { op=>'>', value=>$self->invnum },
921                     } )
922              );
923     
924   $self->send(@_);
925 }
926
927 =item send_csv OPTION => VALUE, ...
928
929 Sends invoice as a CSV data-file to a remote host with the specified protocol.
930
931 Options are:
932
933 protocol - currently only "ftp"
934 server
935 username
936 password
937 dir
938
939 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
940 and YYMMDDHHMMSS is a timestamp.
941
942 See L</print_csv> for a description of the output format.
943
944 =cut
945
946 sub send_csv {
947   my($self, %opt) = @_;
948
949   #create file(s)
950
951   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
952   mkdir $spooldir, 0700 unless -d $spooldir;
953
954   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
955   my $file = "$spooldir/$tracctnum.csv";
956   
957   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
958
959   open(CSV, ">$file") or die "can't open $file: $!";
960   print CSV $header;
961
962   print CSV $detail;
963
964   close CSV;
965
966   my $net;
967   if ( $opt{protocol} eq 'ftp' ) {
968     eval "use Net::FTP;";
969     die $@ if $@;
970     $net = Net::FTP->new($opt{server}) or die @$;
971   } else {
972     die "unknown protocol: $opt{protocol}";
973   }
974
975   $net->login( $opt{username}, $opt{password} )
976     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
977
978   $net->binary or die "can't set binary mode";
979
980   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
981
982   $net->put($file) or die "can't put $file: $!";
983
984   $net->quit;
985
986   unlink $file;
987
988 }
989
990 =item spool_csv
991
992 Spools CSV invoice data.
993
994 Options are:
995
996 =over 4
997
998 =item format - 'default' or 'billco'
999
1000 =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>).
1001
1002 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1003
1004 =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.
1005
1006 =back
1007
1008 =cut
1009
1010 sub spool_csv {
1011   my($self, %opt) = @_;
1012
1013   my $cust_main = $self->cust_main;
1014
1015   if ( $opt{'dest'} ) {
1016     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1017                              $cust_main->invoicing_list;
1018     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1019                      || ! keys %invoicing_list;
1020   }
1021
1022   if ( $opt{'balanceover'} ) {
1023     return 'N/A'
1024       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1025   }
1026
1027   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1028   mkdir $spooldir, 0700 unless -d $spooldir;
1029
1030   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1031
1032   my $file =
1033     "$spooldir/".
1034     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1035     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1036     '.csv';
1037   
1038   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1039
1040   open(CSV, ">>$file") or die "can't open $file: $!";
1041   flock(CSV, LOCK_EX);
1042   seek(CSV, 0, 2);
1043
1044   print CSV $header;
1045
1046   if ( lc($opt{'format'}) eq 'billco' ) {
1047
1048     flock(CSV, LOCK_UN);
1049     close CSV;
1050
1051     $file =
1052       "$spooldir/".
1053       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1054       '-detail.csv';
1055
1056     open(CSV,">>$file") or die "can't open $file: $!";
1057     flock(CSV, LOCK_EX);
1058     seek(CSV, 0, 2);
1059   }
1060
1061   print CSV $detail;
1062
1063   flock(CSV, LOCK_UN);
1064   close CSV;
1065
1066   return '';
1067
1068 }
1069
1070 =item print_csv OPTION => VALUE, ...
1071
1072 Returns CSV data for this invoice.
1073
1074 Options are:
1075
1076 format - 'default' or 'billco'
1077
1078 Returns a list consisting of two scalars.  The first is a single line of CSV
1079 header information for this invoice.  The second is one or more lines of CSV
1080 detail information for this invoice.
1081
1082 If I<format> is not specified or "default", the fields of the CSV file are as
1083 follows:
1084
1085 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1086
1087 =over 4
1088
1089 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1090
1091 B<record_type> is C<cust_bill> for the initial header line only.  The
1092 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1093 fields are filled in.
1094
1095 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1096 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1097 are filled in.
1098
1099 =item invnum - invoice number
1100
1101 =item custnum - customer number
1102
1103 =item _date - invoice date
1104
1105 =item charged - total invoice amount
1106
1107 =item first - customer first name
1108
1109 =item last - customer first name
1110
1111 =item company - company name
1112
1113 =item address1 - address line 1
1114
1115 =item address2 - address line 1
1116
1117 =item city
1118
1119 =item state
1120
1121 =item zip
1122
1123 =item country
1124
1125 =item pkg - line item description
1126
1127 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1128
1129 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1130
1131 =item sdate - start date for recurring fee
1132
1133 =item edate - end date for recurring fee
1134
1135 =back
1136
1137 If I<format> is "billco", the fields of the header CSV file are as follows:
1138
1139   +-------------------------------------------------------------------+
1140   |                        FORMAT HEADER FILE                         |
1141   |-------------------------------------------------------------------|
1142   | Field | Description                   | Name       | Type | Width |
1143   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1144   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1145   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1146   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1147   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1148   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1149   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1150   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1151   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1152   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1153   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1154   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1155   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1156   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1157   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1158   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1159   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1160   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1161   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1162   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1163   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1164   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1165   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1166   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1167   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1168   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1169   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1170   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1171   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1172   +-------+-------------------------------+------------+------+-------+
1173
1174 If I<format> is "billco", the fields of the detail CSV file are as follows:
1175
1176                                   FORMAT FOR DETAIL FILE
1177         |                            |           |      |
1178   Field | Description                | Name      | Type | Width
1179   1     | N/A-Leave Empty            | RC        | CHAR |     2
1180   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1181   3     | Account Number             | TRACCTNUM | CHAR |    15
1182   4     | Invoice Number             | TRINVOICE | CHAR |    15
1183   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1184   6     | Transaction Detail         | DETAILS   | CHAR |   100
1185   7     | Amount                     | AMT       | NUM* |     9
1186   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1187   9     | Grouping Code              | GROUP     | CHAR |     2
1188   10    | User Defined               | ACCT CODE | CHAR |    15
1189
1190 =cut
1191
1192 sub print_csv {
1193   my($self, %opt) = @_;
1194   
1195   eval "use Text::CSV_XS";
1196   die $@ if $@;
1197
1198   my $cust_main = $self->cust_main;
1199
1200   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1201
1202   if ( lc($opt{'format'}) eq 'billco' ) {
1203
1204     my $taxtotal = 0;
1205     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1206
1207     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1208
1209     my( $previous_balance, @unused ) = $self->previous; #previous balance
1210
1211     my $pmt_cr_applied = 0;
1212     $pmt_cr_applied += $_->{'amount'}
1213       foreach ( $self->_items_payments, $self->_items_credits ) ;
1214
1215     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1216
1217     $csv->combine(
1218       '',                         #  1 | N/A-Leave Empty               CHAR   2
1219       '',                         #  2 | N/A-Leave Empty               CHAR  15
1220       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1221       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1222       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1223       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1224       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1225       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1226       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1227       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1228       '',                         # 10 | Ancillary Billing Information CHAR  30
1229       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1230       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1231
1232       # XXX ?
1233       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1234
1235       # XXX ?
1236       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1237
1238       $previous_balance,          # 15 | Previous Balance              NUM*   9
1239       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1240       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1241       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1242       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1243       '',                         # 20 | 30 Day Aging                  NUM*   9
1244       '',                         # 21 | 60 Day Aging                  NUM*   9
1245       '',                         # 22 | 90 Day Aging                  NUM*   9
1246       'N',                        # 23 | Y/N                           CHAR   1
1247       '',                         # 24 | Remittance automation         CHAR 100
1248       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1249       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1250       '0',                        # 27 | Federal Tax***                NUM*   9
1251       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1252       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1253     );
1254
1255   } else {
1256   
1257     $csv->combine(
1258       'cust_bill',
1259       $self->invnum,
1260       $self->custnum,
1261       time2str("%x", $self->_date),
1262       sprintf("%.2f", $self->charged),
1263       ( map { $cust_main->getfield($_) }
1264           qw( first last company address1 address2 city state zip country ) ),
1265       map { '' } (1..5),
1266     ) or die "can't create csv";
1267   }
1268
1269   my $header = $csv->string. "\n";
1270
1271   my $detail = '';
1272   if ( lc($opt{'format'}) eq 'billco' ) {
1273
1274     my $lineseq = 0;
1275     foreach my $item ( $self->_items_pkg ) {
1276
1277       $csv->combine(
1278         '',                     #  1 | N/A-Leave Empty            CHAR   2
1279         '',                     #  2 | N/A-Leave Empty            CHAR  15
1280         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1281         $self->invnum,          #  4 | Invoice Number             CHAR  15
1282         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1283         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1284         $item->{'amount'},      #  7 | Amount                     NUM*   9
1285         '',                     #  8 | Line Format Control**      CHAR   2
1286         '',                     #  9 | Grouping Code              CHAR   2
1287         '',                     # 10 | User Defined               CHAR  15
1288       );
1289
1290       $detail .= $csv->string. "\n";
1291
1292     }
1293
1294   } else {
1295
1296     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1297
1298       my($pkg, $setup, $recur, $sdate, $edate);
1299       if ( $cust_bill_pkg->pkgnum ) {
1300       
1301         ($pkg, $setup, $recur, $sdate, $edate) = (
1302           $cust_bill_pkg->cust_pkg->part_pkg->pkg,
1303           ( $cust_bill_pkg->setup != 0
1304             ? sprintf("%.2f", $cust_bill_pkg->setup )
1305             : '' ),
1306           ( $cust_bill_pkg->recur != 0
1307             ? sprintf("%.2f", $cust_bill_pkg->recur )
1308             : '' ),
1309           ( $cust_bill_pkg->sdate 
1310             ? time2str("%x", $cust_bill_pkg->sdate)
1311             : '' ),
1312           ($cust_bill_pkg->edate 
1313             ?time2str("%x", $cust_bill_pkg->edate)
1314             : '' ),
1315         );
1316   
1317       } else { #pkgnum tax
1318         next unless $cust_bill_pkg->setup != 0;
1319         my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1320                          ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1321                          : 'Tax';
1322         ($pkg, $setup, $recur, $sdate, $edate) =
1323           ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
1324       }
1325   
1326       $csv->combine(
1327         'cust_bill_pkg',
1328         $self->invnum,
1329         ( map { '' } (1..11) ),
1330         ($pkg, $setup, $recur, $sdate, $edate)
1331       ) or die "can't create csv";
1332
1333       $detail .= $csv->string. "\n";
1334
1335     }
1336
1337   }
1338
1339   ( $header, $detail );
1340
1341 }
1342
1343 =item comp
1344
1345 Pays this invoice with a compliemntary payment.  If there is an error,
1346 returns the error, otherwise returns false.
1347
1348 =cut
1349
1350 sub comp {
1351   my $self = shift;
1352   my $cust_pay = new FS::cust_pay ( {
1353     'invnum'   => $self->invnum,
1354     'paid'     => $self->owed,
1355     '_date'    => '',
1356     'payby'    => 'COMP',
1357     'payinfo'  => $self->cust_main->payinfo,
1358     'paybatch' => '',
1359   } );
1360   $cust_pay->insert;
1361 }
1362
1363 =item realtime_card
1364
1365 Attempts to pay this invoice with a credit card payment via a
1366 Business::OnlinePayment realtime gateway.  See
1367 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1368 for supported processors.
1369
1370 =cut
1371
1372 sub realtime_card {
1373   my $self = shift;
1374   $self->realtime_bop( 'CC', @_ );
1375 }
1376
1377 =item realtime_ach
1378
1379 Attempts to pay this invoice with an electronic check (ACH) payment via a
1380 Business::OnlinePayment realtime gateway.  See
1381 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1382 for supported processors.
1383
1384 =cut
1385
1386 sub realtime_ach {
1387   my $self = shift;
1388   $self->realtime_bop( 'ECHECK', @_ );
1389 }
1390
1391 =item realtime_lec
1392
1393 Attempts to pay this invoice with phone bill (LEC) payment via a
1394 Business::OnlinePayment realtime gateway.  See
1395 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1396 for supported processors.
1397
1398 =cut
1399
1400 sub realtime_lec {
1401   my $self = shift;
1402   $self->realtime_bop( 'LEC', @_ );
1403 }
1404
1405 sub realtime_bop {
1406   my( $self, $method ) = @_;
1407
1408   my $cust_main = $self->cust_main;
1409   my $balance = $cust_main->balance;
1410   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1411   $amount = sprintf("%.2f", $amount);
1412   return "not run (balance $balance)" unless $amount > 0;
1413
1414   my $description = 'Internet Services';
1415   if ( $conf->exists('business-onlinepayment-description') ) {
1416     my $dtempl = $conf->config('business-onlinepayment-description');
1417
1418     my $agent_obj = $cust_main->agent
1419       or die "can't retreive agent for $cust_main (agentnum ".
1420              $cust_main->agentnum. ")";
1421     my $agent = $agent_obj->agent;
1422     my $pkgs = join(', ',
1423       map { $_->cust_pkg->part_pkg->pkg }
1424         grep { $_->pkgnum } $self->cust_bill_pkg
1425     );
1426     $description = eval qq("$dtempl");
1427   }
1428
1429   $cust_main->realtime_bop($method, $amount,
1430     'description' => $description,
1431     'invnum'      => $self->invnum,
1432   );
1433
1434 }
1435
1436 =item batch_card OPTION => VALUE...
1437
1438 Adds a payment for this invoice to the pending credit card batch (see
1439 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1440 runs the payment using a realtime gateway.
1441
1442 =cut
1443
1444 sub batch_card {
1445   my ($self, %options) = @_;
1446   my $cust_main = $self->cust_main;
1447
1448   $options{invnum} = $self->invnum;
1449   
1450   $cust_main->batch_card(%options);
1451 }
1452
1453 sub _agent_template {
1454   my $self = shift;
1455   $self->cust_main->agent_template;
1456 }
1457
1458 sub _agent_invoice_from {
1459   my $self = shift;
1460   $self->cust_main->agent_invoice_from;
1461 }
1462
1463 =item print_text [ TIME [ , TEMPLATE ] ]
1464
1465 Returns an text invoice, as a list of lines.
1466
1467 TIME an optional value used to control the printing of overdue messages.  The
1468 default is now.  It isn't the date of the invoice; that's the `_date' field.
1469 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1470 L<Time::Local> and L<Date::Parse> for conversion functions.
1471
1472 =cut
1473
1474 #still some false laziness w/_items stuff (and send_csv)
1475 sub print_text {
1476
1477   my( $self, $today, $template ) = @_;
1478   $today ||= time;
1479
1480 #  my $invnum = $self->invnum;
1481   my $cust_main = $self->cust_main;
1482   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1483     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1484
1485   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1486 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1487   #my $balance_due = $self->owed + $pr_total - $cr_total;
1488   my $balance_due = $self->owed + $pr_total;
1489
1490   #my @collect = ();
1491   #my($description,$amount);
1492   @buf = ();
1493
1494   #previous balance
1495   foreach ( @pr_cust_bill ) {
1496     push @buf, [
1497       "Previous Balance, Invoice #". $_->invnum. 
1498                  " (". time2str("%x",$_->_date). ")",
1499       $money_char. sprintf("%10.2f",$_->owed)
1500     ];
1501   }
1502   if (@pr_cust_bill) {
1503     push @buf,['','-----------'];
1504     push @buf,[ 'Total Previous Balance',
1505                 $money_char. sprintf("%10.2f",$pr_total ) ];
1506     push @buf,['',''];
1507   }
1508
1509   #new charges
1510   foreach my $cust_bill_pkg (
1511     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1512     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1513   ) {
1514
1515     my $desc = $cust_bill_pkg->desc;
1516
1517     if ( $cust_bill_pkg->pkgnum > 0 ) {
1518
1519       if ( $cust_bill_pkg->setup != 0 ) {
1520         my $description = $desc;
1521         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1522         push @buf, [ $description,
1523                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1524         push @buf,
1525           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1526               $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1527       }
1528
1529       if ( $cust_bill_pkg->recur != 0 ) {
1530         push @buf, [
1531           $desc .
1532             ( $conf->exists('disable_line_item_date_ranges')
1533               ? ''
1534               : " (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1535                        time2str("%x", $cust_bill_pkg->edate) . ")"
1536             ),
1537           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1538         ];
1539         push @buf,
1540           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1541               $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1542                                                   $cust_bill_pkg->sdate );
1543       }
1544
1545       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1546
1547     } else { #pkgnum tax or one-shot line item
1548
1549       if ( $cust_bill_pkg->setup != 0 ) {
1550         push @buf, [ $desc,
1551                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1552       }
1553       if ( $cust_bill_pkg->recur != 0 ) {
1554         push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1555                               . time2str("%x", $cust_bill_pkg->edate). ")",
1556                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1557                    ];
1558       }
1559
1560     }
1561
1562   }
1563
1564   push @buf,['','-----------'];
1565   push @buf,['Total New Charges',
1566              $money_char. sprintf("%10.2f",$self->charged) ];
1567   push @buf,['',''];
1568
1569   push @buf,['','-----------'];
1570   push @buf,['Total Charges',
1571              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1572   push @buf,['',''];
1573
1574   #credits
1575   foreach ( $self->cust_credited ) {
1576
1577     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1578
1579     my $reason = substr($_->cust_credit->reason,0,32);
1580     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1581     $reason = " ($reason) " if $reason;
1582     push @buf,[
1583       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1584         $reason,
1585       $money_char. sprintf("%10.2f",$_->amount)
1586     ];
1587   }
1588   #foreach ( @cr_cust_credit ) {
1589   #  push @buf,[
1590   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1591   #    $money_char. sprintf("%10.2f",$_->credited)
1592   #  ];
1593   #}
1594
1595   #get & print payments
1596   foreach ( $self->cust_bill_pay ) {
1597
1598     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1599
1600     push @buf,[
1601       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1602       $money_char. sprintf("%10.2f",$_->amount )
1603     ];
1604   }
1605
1606   #balance due
1607   my $balance_due_msg = $self->balance_due_msg;
1608
1609   push @buf,['','-----------'];
1610   push @buf,[$balance_due_msg, $money_char. 
1611     sprintf("%10.2f", $balance_due ) ];
1612
1613   #create the template
1614   $template ||= $self->_agent_template;
1615   my $templatefile = 'invoice_template';
1616   $templatefile .= "_$template" if length($template);
1617   my @invoice_template = $conf->config($templatefile)
1618     or die "cannot load config file $templatefile";
1619   $invoice_lines = 0;
1620   my $wasfunc = 0;
1621   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1622     /invoice_lines\((\d*)\)/;
1623     $invoice_lines += $1 || scalar(@buf);
1624     $wasfunc=1;
1625   }
1626   die "no invoice_lines() functions in template?" unless $wasfunc;
1627   my $invoice_template = new Text::Template (
1628     TYPE   => 'ARRAY',
1629     SOURCE => [ map "$_\n", @invoice_template ],
1630   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1631   $invoice_template->compile()
1632     or die "can't compile template: $Text::Template::ERROR";
1633
1634   #setup template variables
1635   package FS::cust_bill::_template; #!
1636   use vars qw( $custnum $invnum $date $agent @address $overdue
1637                $page $total_pages @buf );
1638
1639   $custnum = $self->custnum;
1640   $invnum = $self->invnum;
1641   $date = $self->_date;
1642   $agent = $self->cust_main->agent->agent;
1643   $page = 1;
1644
1645   if ( $FS::cust_bill::invoice_lines ) {
1646     $total_pages =
1647       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1648     $total_pages++
1649       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1650   } else {
1651     $total_pages = 1;
1652   }
1653
1654   #format address (variable for the template)
1655   my $l = 0;
1656   @address = ( '', '', '', '', '', '' );
1657   package FS::cust_bill; #!
1658   $FS::cust_bill::_template::address[$l++] =
1659     $cust_main->payname.
1660       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1661         ? " (P.O. #". $cust_main->payinfo. ")"
1662         : ''
1663       )
1664   ;
1665   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1666     if $cust_main->company;
1667   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1668   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1669     if $cust_main->address2;
1670   $FS::cust_bill::_template::address[$l++] =
1671     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1672
1673   my $countrydefault = $conf->config('countrydefault') || 'US';
1674   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1675     unless $cust_main->country eq $countrydefault;
1676
1677         #  #overdue? (variable for the template)
1678         #  $FS::cust_bill::_template::overdue = ( 
1679         #    $balance_due > 0
1680         #    && $today > $self->_date 
1681         ##    && $self->printed > 1
1682         #    && $self->printed > 0
1683         #  );
1684
1685   #and subroutine for the template
1686   sub FS::cust_bill::_template::invoice_lines {
1687     my $lines = shift || scalar(@buf);
1688     map { 
1689       scalar(@buf) ? shift @buf : [ '', '' ];
1690     }
1691     ( 1 .. $lines );
1692   }
1693
1694   #and fill it in
1695   $FS::cust_bill::_template::page = 1;
1696   my $lines;
1697   my @collect;
1698   while (@buf) {
1699     push @collect, split("\n",
1700       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1701     );
1702     $FS::cust_bill::_template::page++;
1703   }
1704
1705   map "$_\n", @collect;
1706
1707 }
1708
1709 =item print_latex [ TIME [ , TEMPLATE ] ]
1710
1711 Internal method - returns a filename of a filled-in LaTeX template for this
1712 invoice (Note: add ".tex" to get the actual filename), and a filename of
1713 an associated logo (with the .eps extension included).
1714
1715 See print_ps and print_pdf for methods that return PostScript and PDF output.
1716
1717 TIME an optional value used to control the printing of overdue messages.  The
1718 default is now.  It isn't the date of the invoice; that's the `_date' field.
1719 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1720 L<Time::Local> and L<Date::Parse> for conversion functions.
1721
1722 =cut
1723
1724 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1725 sub print_latex {
1726
1727   my( $self, $today, $template ) = @_;
1728   $today ||= time;
1729   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1730     if $DEBUG;
1731
1732   my $cust_main = $self->cust_main;
1733   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1734     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1735
1736   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1737 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1738   #my $balance_due = $self->owed + $pr_total - $cr_total;
1739   my $balance_due = $self->owed + $pr_total;
1740
1741   #create the template
1742   $template ||= $self->_agent_template;
1743   my $templatefile = 'invoice_latex';
1744   my $suffix = length($template) ? "_$template" : '';
1745   $templatefile .= $suffix;
1746   my @invoice_template = map "$_\n", $conf->config($templatefile)
1747     or die "cannot load config file $templatefile";
1748
1749   my($format, $text_template);
1750   if ( grep { /^%%Detail/ } @invoice_template ) {
1751     #change this to a die when the old code is removed
1752     warn "old-style invoice template $templatefile; ".
1753          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1754     $format = 'old';
1755   } else {
1756     $format = 'Text::Template';
1757     $text_template = new Text::Template(
1758       TYPE => 'ARRAY',
1759       SOURCE => \@invoice_template,
1760       DELIMITERS => [ '[@--', '--@]' ],
1761     );
1762
1763     $text_template->compile()
1764       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1765   }
1766
1767   my $returnaddress;
1768   if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1769     $returnaddress = join("\n",
1770       $conf->config_orbase('invoice_latexreturnaddress', $template)
1771     );
1772   } else {
1773     $returnaddress = '~';
1774   }
1775
1776   my %invoice_data = (
1777     'custnum'      => $self->custnum,
1778     'invnum'       => $self->invnum,
1779     'date'         => time2str('%b %o, %Y', $self->_date),
1780     'today'        => time2str('%b %o, %Y', $today),
1781     'agent'        => _latex_escape($cust_main->agent->agent),
1782     'payname'      => _latex_escape($cust_main->payname),
1783     'company'      => _latex_escape($cust_main->company),
1784     'address1'     => _latex_escape($cust_main->address1),
1785     'address2'     => _latex_escape($cust_main->address2),
1786     'city'         => _latex_escape($cust_main->city),
1787     'state'        => _latex_escape($cust_main->state),
1788     'zip'          => _latex_escape($cust_main->zip),
1789     'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1790     'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1791     'returnaddress' => $returnaddress,
1792     'quantity'     => 1,
1793     'terms'        => $self->terms,
1794     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1795     # better hang on to conf_dir for a while
1796     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1797   );
1798
1799   my $countrydefault = $conf->config('countrydefault') || 'US';
1800   if ( $cust_main->country eq $countrydefault ) {
1801     $invoice_data{'country'} = '';
1802   } else {
1803     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1804   }
1805
1806   $invoice_data{'notes'} =
1807     join("\n",
1808 #  #do variable substitutions in notes
1809 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1810         $conf->config_orbase('invoice_latexnotes', $template)
1811     );
1812   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1813     if $DEBUG;
1814
1815   $invoice_data{'footer'} =~ s/\n+$//;
1816   $invoice_data{'smallfooter'} =~ s/\n+$//;
1817   $invoice_data{'notes'} =~ s/\n+$//;
1818
1819   $invoice_data{'po_line'} =
1820     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1821       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1822       : '~';
1823
1824   my @filled_in = ();
1825   if ( $format eq 'old' ) {
1826   
1827     my @line_item = ();
1828     my @total_item = ();
1829     while ( @invoice_template ) {
1830       my $line = shift @invoice_template;
1831   
1832       if ( $line =~ /^%%Detail\s*$/ ) {
1833   
1834         while ( ( my $line_item_line = shift @invoice_template )
1835                 !~ /^%%EndDetail\s*$/                            ) {
1836           push @line_item, $line_item_line;
1837         }
1838         foreach my $line_item ( $self->_items ) {
1839         #foreach my $line_item ( $self->_items_pkg ) {
1840           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1841           $invoice_data{'description'} =
1842             _latex_escape($line_item->{'description'});
1843           if ( exists $line_item->{'ext_description'} ) {
1844             $invoice_data{'description'} .=
1845               "\\tabularnewline\n~~".
1846               join( "\\tabularnewline\n~~",
1847                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1848                   );
1849           }
1850           $invoice_data{'amount'} = $line_item->{'amount'};
1851           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1852           push @filled_in,
1853             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1854         }
1855   
1856       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1857   
1858         while ( ( my $total_item_line = shift @invoice_template )
1859                 !~ /^%%EndTotalDetails\s*$/                      ) {
1860           push @total_item, $total_item_line;
1861         }
1862   
1863         my @total_fill = ();
1864   
1865         my $taxtotal = 0;
1866         foreach my $tax ( $self->_items_tax ) {
1867           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1868           $taxtotal += $tax->{'amount'};
1869           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1870           push @total_fill,
1871             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1872                 @total_item;
1873         }
1874
1875         if ( $taxtotal ) {
1876           $invoice_data{'total_item'} = 'Sub-total';
1877           $invoice_data{'total_amount'} =
1878             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1879           unshift @total_fill,
1880             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1881                 @total_item;
1882         }
1883   
1884         $invoice_data{'total_item'} = '\textbf{Total}';
1885         $invoice_data{'total_amount'} =
1886           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1887         push @total_fill,
1888           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1889               @total_item;
1890   
1891         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1892   
1893         # credits
1894         foreach my $credit ( $self->_items_credits ) {
1895           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1896           #$credittotal
1897           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1898           push @total_fill, 
1899             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1900                 @total_item;
1901         }
1902   
1903         # payments
1904         foreach my $payment ( $self->_items_payments ) {
1905           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1906           #$paymenttotal
1907           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1908           push @total_fill, 
1909             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1910                 @total_item;
1911         }
1912   
1913         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1914         $invoice_data{'total_amount'} =
1915           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1916         push @total_fill,
1917           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1918               @total_item;
1919   
1920         push @filled_in, @total_fill;
1921   
1922       } else {
1923         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1924         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1925         push @filled_in, $line;
1926       }
1927   
1928     }
1929
1930     sub nounder {
1931       my $var = $1;
1932       $var =~ s/_/\-/g;
1933       $var;
1934     }
1935
1936   } elsif ( $format eq 'Text::Template' ) {
1937
1938     my @detail_items = ();
1939     my @total_items = ();
1940
1941     $invoice_data{'detail_items'} = \@detail_items;
1942     $invoice_data{'total_items'} = \@total_items;
1943   
1944     foreach my $line_item ( $self->_items ) {
1945       my $detail = {
1946         ext_description => [],
1947       };
1948       $detail->{'ref'} = $line_item->{'pkgnum'};
1949       $detail->{'quantity'} = 1;
1950       $detail->{'description'} = _latex_escape($line_item->{'description'});
1951       if ( exists $line_item->{'ext_description'} ) {
1952         @{$detail->{'ext_description'}} = map {
1953           _latex_escape($_);
1954         } @{$line_item->{'ext_description'}};
1955       }
1956       $detail->{'amount'} = $line_item->{'amount'};
1957       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1958   
1959       push @detail_items, $detail;
1960     }
1961   
1962   
1963     my $taxtotal = 0;
1964     foreach my $tax ( $self->_items_tax ) {
1965       my $total = {};
1966       $total->{'total_item'} = _latex_escape($tax->{'description'});
1967       $taxtotal += $tax->{'amount'};
1968       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1969       push @total_items, $total;
1970     }
1971   
1972     if ( $taxtotal ) {
1973       my $total = {};
1974       $total->{'total_item'} = 'Sub-total';
1975       $total->{'total_amount'} =
1976         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1977       unshift @total_items, $total;
1978     }
1979   
1980     {
1981       my $total = {};
1982       $total->{'total_item'} = '\textbf{Total}';
1983       $total->{'total_amount'} =
1984         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1985       push @total_items, $total;
1986     }
1987   
1988     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1989   
1990     # credits
1991     foreach my $credit ( $self->_items_credits ) {
1992       my $total;
1993       $total->{'total_item'} = _latex_escape($credit->{'description'});
1994       #$credittotal
1995       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1996       push @total_items, $total;
1997     }
1998   
1999     # payments
2000     foreach my $payment ( $self->_items_payments ) {
2001       my $total = {};
2002       $total->{'total_item'} = _latex_escape($payment->{'description'});
2003       #$paymenttotal
2004       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
2005       push @total_items, $total;
2006     }
2007   
2008     { 
2009       my $total;
2010       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2011       $total->{'total_amount'} =
2012         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2013       push @total_items, $total;
2014     }
2015
2016   } else {
2017     die "guru meditation #54";
2018   }
2019
2020   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2021   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2022                            DIR      => $dir,
2023                            SUFFIX   => '.eps',
2024                            UNLINK   => 0,
2025                          ) or die "can't open temp file: $!\n";
2026
2027   if ($template && $conf->exists("logo_${template}.eps")) {
2028     print $lh $conf->config_binary("logo_${template}.eps")
2029       or die "can't write temp file: $!\n";
2030   }else{
2031     print $lh $conf->config_binary('logo.eps')
2032       or die "can't write temp file: $!\n";
2033   }
2034   close $lh;
2035   $invoice_data{'logo_file'} = $lh->filename;
2036
2037   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2038                            DIR      => $dir,
2039                            SUFFIX   => '.tex',
2040                            UNLINK   => 0,
2041                          ) or die "can't open temp file: $!\n";
2042   if ( $format eq 'old' ) {
2043     print $fh join('', @filled_in );
2044   } elsif ( $format eq 'Text::Template' ) {
2045     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2046   } else {
2047     die "guru meditation #32";
2048   }
2049   close $fh;
2050
2051   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2052   return ($1, $invoice_data{'logo_file'});
2053
2054 }
2055
2056 =item print_ps [ TIME [ , TEMPLATE ] ]
2057
2058 Returns an postscript invoice, as a scalar.
2059
2060 TIME an optional value used to control the printing of overdue messages.  The
2061 default is now.  It isn't the date of the invoice; that's the `_date' field.
2062 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2063 L<Time::Local> and L<Date::Parse> for conversion functions.
2064
2065 =cut
2066
2067 sub print_ps {
2068   my $self = shift;
2069
2070   my ($file, $lfile) = $self->print_latex(@_);
2071   my $ps = generate_ps($file);
2072   unlink($lfile);
2073   $ps;
2074
2075 }
2076
2077 =item print_pdf [ TIME [ , TEMPLATE ] ]
2078
2079 Returns an PDF invoice, as a scalar.
2080
2081 TIME an optional value used to control the printing of overdue messages.  The
2082 default is now.  It isn't the date of the invoice; that's the `_date' field.
2083 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2084 L<Time::Local> and L<Date::Parse> for conversion functions.
2085
2086 =cut
2087
2088 sub print_pdf {
2089   my $self = shift;
2090
2091   my ($file, $lfile) = $self->print_latex(@_);
2092
2093   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2094   chdir($dir);
2095
2096   #system('pdflatex', "$file.tex");
2097   #system('pdflatex', "$file.tex");
2098   #! LaTeX Error: Unknown graphics extension: .eps.
2099
2100   my $sfile = shell_quote $file;
2101
2102   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2103     or die "pslatex $file.tex failed; see $file.log for details?\n";
2104   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2105     or die "pslatex $file.tex failed; see $file.log for details?\n";
2106
2107   #system('dvipdf', "$file.dvi", "$file.pdf" );
2108   system(
2109     "dvips -q -t letter -f $sfile.dvi ".
2110     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2111     "     -c save pop -"
2112   ) == 0
2113     or die "dvips | gs failed: $!";
2114
2115   open(PDF, "<$file.pdf")
2116     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2117
2118   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2119   unlink("$lfile");
2120
2121   my $pdf = '';
2122   while (<PDF>) {
2123     $pdf .= $_;
2124   }
2125
2126   close PDF;
2127
2128   return $pdf;
2129
2130 }
2131
2132 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2133
2134 Returns an HTML invoice, as a scalar.
2135
2136 TIME an optional value used to control the printing of overdue messages.  The
2137 default is now.  It isn't the date of the invoice; that's the `_date' field.
2138 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2139 L<Time::Local> and L<Date::Parse> for conversion functions.
2140
2141 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2142 when emailing the invoice as part of a multipart/related MIME email.
2143
2144 =cut
2145
2146 #some falze laziness w/print_text and print_latex (and send_csv)
2147 sub print_html {
2148   my( $self, $today, $template, $cid ) = @_;
2149   $today ||= time;
2150
2151   my $cust_main = $self->cust_main;
2152   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2153     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2154
2155   $template ||= $self->_agent_template;
2156   my $templatefile = 'invoice_html';
2157   my $suffix = length($template) ? "_$template" : '';
2158   $templatefile .= $suffix;
2159   my @html_template = map "$_\n", $conf->config($templatefile)
2160     or die "cannot load config file $templatefile";
2161
2162   my $html_template = new Text::Template(
2163     TYPE   => 'ARRAY',
2164     SOURCE => \@html_template,
2165     DELIMITERS => [ '<%=', '%>' ],
2166   );
2167
2168   $html_template->compile()
2169     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2170
2171   my %invoice_data = (
2172     'custnum'      => $self->custnum,
2173     'invnum'       => $self->invnum,
2174     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
2175     'today'        => time2str('%b %o, %Y', $today),
2176     'agent'        => encode_entities($cust_main->agent->agent),
2177     'payname'      => encode_entities($cust_main->payname),
2178     'company'      => encode_entities($cust_main->company),
2179     'address1'     => encode_entities($cust_main->address1),
2180     'address2'     => encode_entities($cust_main->address2),
2181     'city'         => encode_entities($cust_main->city),
2182     'state'        => encode_entities($cust_main->state),
2183     'zip'          => encode_entities($cust_main->zip),
2184     'terms'        => $self->terms,
2185     'cid'          => $cid,
2186     'template'     => $template,
2187 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2188   );
2189
2190   if (
2191          defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2192       && length(  $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2193   ) {
2194     $invoice_data{'returnaddress'} =
2195       join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2196   } else {
2197     $invoice_data{'returnaddress'} =
2198       join("\n", map { 
2199                        s/~/&nbsp;/g;
2200                        s/\\\\\*?\s*$/<BR>/;
2201                        s/\\hyphenation\{[\w\s\-]+\}//;
2202                        $_;
2203                      }
2204                      $conf->config_orbase( 'invoice_latexreturnaddress',
2205                                            $template
2206                                          )
2207           );
2208   }
2209
2210   my $countrydefault = $conf->config('countrydefault') || 'US';
2211   if ( $cust_main->country eq $countrydefault ) {
2212     $invoice_data{'country'} = '';
2213   } else {
2214     $invoice_data{'country'} =
2215       encode_entities(code2country($cust_main->country));
2216   }
2217
2218   if (
2219          defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2220       && length(  $conf->config_orbase('invoice_htmlnotes', $template) )
2221   ) {
2222     $invoice_data{'notes'} =
2223       join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2224   } else {
2225     $invoice_data{'notes'} = 
2226       join("\n", map { 
2227                        s/%%(.*)$/<!-- $1 -->/;
2228                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2229                        s/\\begin\{enumerate\}/<ol>/;
2230                        s/\\item /  <li>/;
2231                        s/\\end\{enumerate\}/<\/ol>/;
2232                        s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2233                        s/\\\\\*/ /;
2234                        $_;
2235                      } 
2236                      $conf->config_orbase('invoice_latexnotes', $template)
2237           );
2238   }
2239
2240 #  #do variable substitutions in notes
2241 #  $invoice_data{'notes'} =
2242 #    join("\n",
2243 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2244 #        $conf->config_orbase('invoice_latexnotes', $suffix)
2245 #    );
2246
2247   if (
2248          defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2249       && length(  $conf->config_orbase('invoice_htmlfooter', $template) )
2250   ) {
2251    $invoice_data{'footer'} =
2252      join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2253   } else {
2254    $invoice_data{'footer'} =
2255        join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
2256                       $conf->config_orbase('invoice_latexfooter', $template)
2257            );
2258   }
2259
2260   $invoice_data{'po_line'} =
2261     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2262       ? encode_entities("Purchase Order #". $cust_main->payinfo)
2263       : '';
2264
2265   my $money_char = $conf->config('money_char') || '$';
2266
2267   foreach my $line_item ( $self->_items ) {
2268     my $detail = {
2269       ext_description => [],
2270     };
2271     $detail->{'ref'} = $line_item->{'pkgnum'};
2272     $detail->{'description'} = encode_entities($line_item->{'description'});
2273     if ( exists $line_item->{'ext_description'} ) {
2274       @{$detail->{'ext_description'}} = map {
2275         encode_entities($_);
2276       } @{$line_item->{'ext_description'}};
2277     }
2278     $detail->{'amount'} = $money_char. $line_item->{'amount'};
2279     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2280
2281     push @{$invoice_data{'detail_items'}}, $detail;
2282   }
2283
2284
2285   my $taxtotal = 0;
2286   foreach my $tax ( $self->_items_tax ) {
2287     my $total = {};
2288     $total->{'total_item'} = encode_entities($tax->{'description'});
2289     $taxtotal += $tax->{'amount'};
2290     $total->{'total_amount'} = $money_char. $tax->{'amount'};
2291     push @{$invoice_data{'total_items'}}, $total;
2292   }
2293
2294   if ( $taxtotal ) {
2295     my $total = {};
2296     $total->{'total_item'} = 'Sub-total';
2297     $total->{'total_amount'} =
2298       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2299     unshift @{$invoice_data{'total_items'}}, $total;
2300   }
2301
2302   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2303   {
2304     my $total = {};
2305     $total->{'total_item'} = '<b>Total</b>';
2306     $total->{'total_amount'} =
2307       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2308     push @{$invoice_data{'total_items'}}, $total;
2309   }
2310
2311   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2312
2313   # credits
2314   foreach my $credit ( $self->_items_credits ) {
2315     my $total;
2316     $total->{'total_item'} = encode_entities($credit->{'description'});
2317     #$credittotal
2318     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2319     push @{$invoice_data{'total_items'}}, $total;
2320   }
2321
2322   # payments
2323   foreach my $payment ( $self->_items_payments ) {
2324     my $total = {};
2325     $total->{'total_item'} = encode_entities($payment->{'description'});
2326     #$paymenttotal
2327     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2328     push @{$invoice_data{'total_items'}}, $total;
2329   }
2330
2331   { 
2332     my $total;
2333     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2334     $total->{'total_amount'} =
2335       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2336     push @{$invoice_data{'total_items'}}, $total;
2337   }
2338
2339   $html_template->fill_in( HASH => \%invoice_data);
2340 }
2341
2342 # quick subroutine for print_latex
2343 #
2344 # There are ten characters that LaTeX treats as special characters, which
2345 # means that they do not simply typeset themselves: 
2346 #      # $ % & ~ _ ^ \ { }
2347 #
2348 # TeX ignores blanks following an escaped character; if you want a blank (as
2349 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2350
2351 sub _latex_escape {
2352   my $value = shift;
2353   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2354   $value =~ s/([<>])/\$$1\$/g;
2355   $value;
2356 }
2357
2358 #utility methods for print_*
2359
2360 sub terms {
2361   my $self = shift;
2362
2363   #check for an invoice- specific override (eventually)
2364   
2365   #check for a customer- specific override
2366   return $self->cust_main->invoice_terms
2367     if $self->cust_main->invoice_terms;
2368
2369   #use configured default or default default
2370   $conf->config('invoice_default_terms') || 'Payable upon receipt';
2371 }
2372
2373 sub due_date {
2374   my $self = shift;
2375   my $duedate = '';
2376   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2377     $duedate = $self->_date() + ( $1 * 86400 );
2378   }
2379   $duedate;
2380 }
2381
2382 sub due_date2str {
2383   my $self = shift;
2384   $self->due_date ? time2str(shift, $self->due_date) : '';
2385 }
2386
2387 sub balance_due_msg {
2388   my $self = shift;
2389   my $msg = 'Balance Due';
2390   return $msg unless $self->terms;
2391   if ( $self->due_date ) {
2392     $msg .= ' - Please pay by '. $self->due_date2str('%x');
2393   } elsif ( $self->terms ) {
2394     $msg .= ' - '. $self->terms;
2395   }
2396   $msg;
2397 }
2398
2399 sub _items {
2400   my $self = shift;
2401   my @display = scalar(@_)
2402                 ? @_
2403                 : qw( _items_previous _items_pkg );
2404                 #: qw( _items_pkg );
2405                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2406   my @b = ();
2407   foreach my $display ( @display ) {
2408     push @b, $self->$display(@_);
2409   }
2410   @b;
2411 }
2412
2413 sub _items_previous {
2414   my $self = shift;
2415   my $cust_main = $self->cust_main;
2416   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2417   my @b = ();
2418   foreach ( @pr_cust_bill ) {
2419     push @b, {
2420       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2421                        ' ('. time2str('%x',$_->_date). ')',
2422       #'pkgpart'     => 'N/A',
2423       'pkgnum'      => 'N/A',
2424       'amount'      => sprintf("%.2f", $_->owed),
2425     };
2426   }
2427   @b;
2428
2429   #{
2430   #    'description'     => 'Previous Balance',
2431   #    #'pkgpart'         => 'N/A',
2432   #    'pkgnum'          => 'N/A',
2433   #    'amount'          => sprintf("%10.2f", $pr_total ),
2434   #    'ext_description' => [ map {
2435   #                                 "Invoice ". $_->invnum.
2436   #                                 " (". time2str("%x",$_->_date). ") ".
2437   #                                 sprintf("%10.2f", $_->owed)
2438   #                         } @pr_cust_bill ],
2439
2440   #};
2441 }
2442
2443 sub _items_pkg {
2444   my $self = shift;
2445   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2446   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2447 }
2448
2449 sub _items_tax {
2450   my $self = shift;
2451   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2452   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2453 }
2454
2455 sub _items_cust_bill_pkg {
2456   my $self = shift;
2457   my $cust_bill_pkg = shift;
2458
2459   my @b = ();
2460   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2461
2462     my $desc = $cust_bill_pkg->desc;
2463
2464     if ( $cust_bill_pkg->pkgnum > 0 ) {
2465
2466       if ( $cust_bill_pkg->setup != 0 ) {
2467         my $description = $desc;
2468         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2469         my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2470         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2471         push @b, {
2472           description     => $description,
2473           #pkgpart         => $part_pkg->pkgpart,
2474           pkgnum          => $cust_bill_pkg->pkgnum,
2475           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2476           ext_description => \@d,
2477         };
2478       }
2479
2480       if ( $cust_bill_pkg->recur != 0 ) {
2481         push @b, {
2482           description     => $desc .
2483                              ( $conf->exists('disable_line_item_date_ranges')
2484                                ? ''
2485                                : " (" .time2str("%x", $cust_bill_pkg->sdate).
2486                                  " - ".time2str("%x", $cust_bill_pkg->edate).")"
2487                              ),
2488           #pkgpart         => $part_pkg->pkgpart,
2489           pkgnum          => $cust_bill_pkg->pkgnum,
2490           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2491           ext_description =>
2492             [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2493                                                         $cust_bill_pkg->sdate),
2494               $cust_bill_pkg->details,
2495             ],
2496         };
2497       }
2498
2499     } else { #pkgnum tax or one-shot line item (??)
2500
2501       if ( $cust_bill_pkg->setup != 0 ) {
2502         push @b, {
2503           'description' => $desc,
2504           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2505         };
2506       }
2507       if ( $cust_bill_pkg->recur != 0 ) {
2508         push @b, {
2509           'description' => "$desc (".
2510                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2511                            time2str("%x", $cust_bill_pkg->edate). ')',
2512           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2513         };
2514       }
2515
2516     }
2517
2518   }
2519
2520   @b;
2521
2522 }
2523
2524 sub _items_credits {
2525   my $self = shift;
2526
2527   my @b;
2528   #credits
2529   foreach ( $self->cust_credited ) {
2530
2531     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2532
2533     my $reason = $_->cust_credit->reason;
2534     #my $reason = substr($_->cust_credit->reason,0,32);
2535     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2536     $reason = " ($reason) " if $reason;
2537     push @b, {
2538       #'description' => 'Credit ref\#'. $_->crednum.
2539       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2540       #                 $reason,
2541       'description' => 'Credit applied '.
2542                        time2str("%x",$_->cust_credit->_date). $reason,
2543       'amount'      => sprintf("%.2f",$_->amount),
2544     };
2545   }
2546   #foreach ( @cr_cust_credit ) {
2547   #  push @buf,[
2548   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2549   #    $money_char. sprintf("%10.2f",$_->credited)
2550   #  ];
2551   #}
2552
2553   @b;
2554
2555 }
2556
2557 sub _items_payments {
2558   my $self = shift;
2559
2560   my @b;
2561   #get & print payments
2562   foreach ( $self->cust_bill_pay ) {
2563
2564     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2565
2566     push @b, {
2567       'description' => "Payment received ".
2568                        time2str("%x",$_->cust_pay->_date ),
2569       'amount'      => sprintf("%.2f", $_->amount )
2570     };
2571   }
2572
2573   @b;
2574
2575 }
2576
2577
2578 =back
2579
2580 =head1 SUBROUTINES
2581
2582 =over 4
2583
2584 =item reprint
2585
2586 =cut
2587
2588 sub process_reprint {
2589   process_re_X('print', @_);
2590 }
2591
2592 =item reemail
2593
2594 =cut
2595
2596 sub process_reemail {
2597   process_re_X('email', @_);
2598 }
2599
2600 =item refax
2601
2602 =cut
2603
2604 sub process_refax {
2605   process_re_X('fax', @_);
2606 }
2607
2608 use Storable qw(thaw);
2609 use Data::Dumper;
2610 use MIME::Base64;
2611 sub process_re_X {
2612   my( $method, $job ) = ( shift, shift );
2613   warn "process_re_X $method for job $job\n" if $DEBUG;
2614
2615   my $param = thaw(decode_base64(shift));
2616   warn Dumper($param) if $DEBUG;
2617
2618   re_X(
2619     $method,
2620     $job,
2621     %$param,
2622   );
2623
2624 }
2625
2626 sub re_X {
2627   my($method, $job, %param ) = @_;
2628 #              [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2629   if ( $DEBUG ) {
2630     warn "re_X $method for job $job with param:\n".
2631          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
2632   }
2633
2634   #some false laziness w/search/cust_bill.html
2635   my $distinct = '';
2636   my $orderby = 'ORDER BY cust_bill._date';
2637
2638   my @where;
2639
2640   if ( $param{'begin'} =~ /^(\d+)$/ ) {
2641     push @where, "cust_bill._date >= $1";
2642   }
2643   if ( $param{'end'} =~ /^(\d+)$/ ) {
2644     push @where, "cust_bill._date < $1";
2645   }
2646   if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2647     push @where, "cust_main.agentnum = $1";
2648   }
2649
2650   my $owed =
2651     "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2652                  WHERE cust_bill_pay.invnum = cust_bill.invnum )
2653              - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2654                  WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2655
2656   push @where, "0 != $owed"
2657     if $param{'open'};
2658
2659   push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2660     if $param{'days'};
2661
2662   my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2663
2664   my $addl_from = 'left join cust_main using ( custnum )';
2665
2666   if ( $param{'newest_percust'} ) {
2667     $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2668     $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2669     #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2670   }
2671      
2672   my @cust_bill = qsearch( 'cust_bill',
2673                            {},
2674                            "$distinct cust_bill.*",
2675                            $extra_sql,
2676                            '',
2677                            $addl_from
2678                          );
2679
2680   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2681   foreach my $cust_bill ( @cust_bill ) {
2682     $cust_bill->$method();
2683
2684     if ( $job ) { #progressbar foo
2685       $num++;
2686       if ( time - $min_sec > $last ) {
2687         my $error = $job->update_statustext(
2688           int( 100 * $num / scalar(@cust_bill) )
2689         );
2690         die $error if $error;
2691         $last = time;
2692       }
2693     }
2694
2695   }
2696
2697 }
2698
2699 =back
2700
2701 =head1 BUGS
2702
2703 The delete method.
2704
2705 print_text formatting (and some logic :/) is in source, but needs to be
2706 slurped in from a file.  Also number of lines ($=).
2707
2708 =head1 SEE ALSO
2709
2710 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2711 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2712 documentation.
2713
2714 =cut
2715
2716 1;
2717