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