This commit was generated by cvs2svn to compensate for changes in r4888,
[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 OPTION => VALUE...
1307
1308 Adds a payment for this invoice to the pending credit card batch (see
1309 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1310 runs the payment using a realtime gateway.
1311
1312 =cut
1313
1314 sub batch_card {
1315   my ($self, %options) = @_;
1316   my $cust_main = $self->cust_main;
1317
1318   my $amount = sprintf("%.2f", $cust_main->balance - $cust_main->in_transit_payments);
1319   return '' unless $amount > 0;
1320   
1321   if ($options{'realtime'}) {
1322     return $cust_main->realtime_bop( FS::payby->payby2bop($cust_main->payby),
1323                                      $amount,
1324                                      %options,
1325                                    );
1326   }
1327
1328   my $oldAutoCommit = $FS::UID::AutoCommit;
1329   local $FS::UID::AutoCommit = 0;
1330   my $dbh = dbh;
1331
1332   $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
1333     or return "Cannot lock pay_batch: " . $dbh->errstr;
1334
1335   my %pay_batch = (
1336     'status' => 'O',
1337     'payby'  => FS::payby->payby2payment($cust_main->payby),
1338   );
1339
1340   my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
1341
1342   unless ( $pay_batch ) {
1343     $pay_batch = new FS::pay_batch \%pay_batch;
1344     my $error = $pay_batch->insert;
1345     if ( $error ) {
1346       $dbh->rollback if $oldAutoCommit;
1347       die "error creating new batch: $error\n";
1348     }
1349   }
1350
1351   my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
1352       'batchnum' => $pay_batch->batchnum,
1353       'custnum'  => $cust_main->custnum,
1354   } );
1355
1356   my $cust_pay_batch = new FS::cust_pay_batch ( {
1357     'batchnum' => $pay_batch->batchnum,
1358     'invnum'   => $self->getfield('invnum'),       # is there a better value?
1359                                                    # this field should be
1360                                                    # removed...
1361                                                    # cust_bill_pay_batch now
1362     'custnum'  => $cust_main->custnum,
1363     'last'     => $cust_main->getfield('last'),
1364     'first'    => $cust_main->getfield('first'),
1365     'address1' => $cust_main->address1,
1366     'address2' => $cust_main->address2,
1367     'city'     => $cust_main->city,
1368     'state'    => $cust_main->state,
1369     'zip'      => $cust_main->zip,
1370     'country'  => $cust_main->country,
1371     'payby'    => $cust_main->payby,
1372     'payinfo'  => $cust_main->payinfo,
1373     'exp'      => $cust_main->paydate,
1374     'payname'  => $cust_main->payname,
1375     'amount'   => $amount,                          # consolidating
1376   } );
1377   
1378   $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
1379     if $old_cust_pay_batch;
1380
1381   my $error;
1382   if ($old_cust_pay_batch) {
1383     $error = $cust_pay_batch->replace($old_cust_pay_batch)
1384   } else {
1385     $error = $cust_pay_batch->insert;
1386   }
1387
1388   if ( $error ) {
1389     $dbh->rollback if $oldAutoCommit;
1390     die $error;
1391   }
1392
1393   my $unapplied = $cust_main->total_credited + $cust_main->total_unapplied_payments + $cust_main->in_transit_payments;
1394   foreach my $cust_bill ($cust_main->open_cust_bill) {
1395     #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
1396     my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
1397       'invnum' => $cust_bill->invnum,
1398       'paybatchnum' => $cust_pay_batch->paybatchnum,
1399       'amount' => $cust_bill->owed,
1400       '_date' => time,
1401     };
1402     if ($unapplied >= $cust_bill_pay_batch->amount){
1403       $unapplied -= $cust_bill_pay_batch->amount;
1404       next;
1405     }else{
1406       $cust_bill_pay_batch->amount(sprintf ( "%.2f", 
1407                                    $cust_bill_pay_batch->amount - $unapplied ));
1408       $unapplied = 0;
1409     }
1410     $error = $cust_bill_pay_batch->insert;
1411     if ( $error ) {
1412       $dbh->rollback if $oldAutoCommit;
1413       die $error;
1414     }
1415   }
1416
1417   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1418   '';
1419 }
1420
1421 sub _agent_template {
1422   my $self = shift;
1423   $self->_agent_plandata('agent_templatename');
1424 }
1425
1426 sub _agent_invoice_from {
1427   my $self = shift;
1428   $self->_agent_plandata('agent_invoice_from');
1429 }
1430
1431 sub _agent_plandata {
1432   my( $self, $option ) = @_;
1433
1434   my $part_bill_event = qsearchs( 'part_bill_event',
1435     {
1436       'payby'     => $self->cust_main->payby,
1437       'plan'      => 'send_agent',
1438       'plandata'  => { 'op'    => '~',
1439                        'value' => "(^|\n)agentnum ".
1440                                    '([0-9]*, )*'.
1441                                   $self->cust_main->agentnum.
1442                                    '(, [0-9]*)*'.
1443                                   "(\n|\$)",
1444                      },
1445     },
1446     '',
1447     'ORDER BY seconds LIMIT 1'
1448   );
1449
1450   return '' unless $part_bill_event;
1451
1452   if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1453     return $1;
1454   } else {
1455     warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1456          " plandata for $option";
1457     return '';
1458   }
1459
1460 }
1461
1462 =item print_text [ TIME [ , TEMPLATE ] ]
1463
1464 Returns an text invoice, as a list of lines.
1465
1466 TIME an optional value used to control the printing of overdue messages.  The
1467 default is now.  It isn't the date of the invoice; that's the `_date' field.
1468 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1469 L<Time::Local> and L<Date::Parse> for conversion functions.
1470
1471 =cut
1472
1473 #still some false laziness w/_items stuff (and send_csv)
1474 sub print_text {
1475
1476   my( $self, $today, $template ) = @_;
1477   $today ||= time;
1478
1479 #  my $invnum = $self->invnum;
1480   my $cust_main = $self->cust_main;
1481   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1482     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1483
1484   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1485 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1486   #my $balance_due = $self->owed + $pr_total - $cr_total;
1487   my $balance_due = $self->owed + $pr_total;
1488
1489   #my @collect = ();
1490   #my($description,$amount);
1491   @buf = ();
1492
1493   #previous balance
1494   foreach ( @pr_cust_bill ) {
1495     push @buf, [
1496       "Previous Balance, Invoice #". $_->invnum. 
1497                  " (". time2str("%x",$_->_date). ")",
1498       $money_char. sprintf("%10.2f",$_->owed)
1499     ];
1500   }
1501   if (@pr_cust_bill) {
1502     push @buf,['','-----------'];
1503     push @buf,[ 'Total Previous Balance',
1504                 $money_char. sprintf("%10.2f",$pr_total ) ];
1505     push @buf,['',''];
1506   }
1507
1508   #new charges
1509   foreach my $cust_bill_pkg (
1510     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1511     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1512   ) {
1513
1514     my $desc = $cust_bill_pkg->desc;
1515
1516     if ( $cust_bill_pkg->pkgnum > 0 ) {
1517
1518       if ( $cust_bill_pkg->setup != 0 ) {
1519         my $description = $desc;
1520         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1521         push @buf, [ $description,
1522                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1523         push @buf,
1524           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1525               $cust_bill_pkg->cust_pkg->h_labels($self->_date);
1526       }
1527
1528       if ( $cust_bill_pkg->recur != 0 ) {
1529         push @buf, [
1530           "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1531                       time2str("%x", $cust_bill_pkg->edate) . ")",
1532           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1533         ];
1534         push @buf,
1535           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1536               $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate,
1537                                                   $cust_bill_pkg->sdate );
1538       }
1539
1540       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1541
1542     } else { #pkgnum tax or one-shot line item
1543
1544       if ( $cust_bill_pkg->setup != 0 ) {
1545         push @buf, [ $desc,
1546                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1547       }
1548       if ( $cust_bill_pkg->recur != 0 ) {
1549         push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1550                               . time2str("%x", $cust_bill_pkg->edate). ")",
1551                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1552                    ];
1553       }
1554
1555     }
1556
1557   }
1558
1559   push @buf,['','-----------'];
1560   push @buf,['Total New Charges',
1561              $money_char. sprintf("%10.2f",$self->charged) ];
1562   push @buf,['',''];
1563
1564   push @buf,['','-----------'];
1565   push @buf,['Total Charges',
1566              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1567   push @buf,['',''];
1568
1569   #credits
1570   foreach ( $self->cust_credited ) {
1571
1572     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1573
1574     my $reason = substr($_->cust_credit->reason,0,32);
1575     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1576     $reason = " ($reason) " if $reason;
1577     push @buf,[
1578       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1579         $reason,
1580       $money_char. sprintf("%10.2f",$_->amount)
1581     ];
1582   }
1583   #foreach ( @cr_cust_credit ) {
1584   #  push @buf,[
1585   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1586   #    $money_char. sprintf("%10.2f",$_->credited)
1587   #  ];
1588   #}
1589
1590   #get & print payments
1591   foreach ( $self->cust_bill_pay ) {
1592
1593     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1594
1595     push @buf,[
1596       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1597       $money_char. sprintf("%10.2f",$_->amount )
1598     ];
1599   }
1600
1601   #balance due
1602   my $balance_due_msg = $self->balance_due_msg;
1603
1604   push @buf,['','-----------'];
1605   push @buf,[$balance_due_msg, $money_char. 
1606     sprintf("%10.2f", $balance_due ) ];
1607
1608   #create the template
1609   $template ||= $self->_agent_template;
1610   my $templatefile = 'invoice_template';
1611   $templatefile .= "_$template" if length($template);
1612   my @invoice_template = $conf->config($templatefile)
1613     or die "cannot load config file $templatefile";
1614   $invoice_lines = 0;
1615   my $wasfunc = 0;
1616   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1617     /invoice_lines\((\d*)\)/;
1618     $invoice_lines += $1 || scalar(@buf);
1619     $wasfunc=1;
1620   }
1621   die "no invoice_lines() functions in template?" unless $wasfunc;
1622   my $invoice_template = new Text::Template (
1623     TYPE   => 'ARRAY',
1624     SOURCE => [ map "$_\n", @invoice_template ],
1625   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1626   $invoice_template->compile()
1627     or die "can't compile template: $Text::Template::ERROR";
1628
1629   #setup template variables
1630   package FS::cust_bill::_template; #!
1631   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1632
1633   $invnum = $self->invnum;
1634   $date = $self->_date;
1635   $page = 1;
1636   $agent = $self->cust_main->agent->agent;
1637
1638   if ( $FS::cust_bill::invoice_lines ) {
1639     $total_pages =
1640       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1641     $total_pages++
1642       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1643   } else {
1644     $total_pages = 1;
1645   }
1646
1647   #format address (variable for the template)
1648   my $l = 0;
1649   @address = ( '', '', '', '', '', '' );
1650   package FS::cust_bill; #!
1651   $FS::cust_bill::_template::address[$l++] =
1652     $cust_main->payname.
1653       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1654         ? " (P.O. #". $cust_main->payinfo. ")"
1655         : ''
1656       )
1657   ;
1658   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1659     if $cust_main->company;
1660   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1661   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1662     if $cust_main->address2;
1663   $FS::cust_bill::_template::address[$l++] =
1664     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1665
1666   my $countrydefault = $conf->config('countrydefault') || 'US';
1667   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1668     unless $cust_main->country eq $countrydefault;
1669
1670         #  #overdue? (variable for the template)
1671         #  $FS::cust_bill::_template::overdue = ( 
1672         #    $balance_due > 0
1673         #    && $today > $self->_date 
1674         ##    && $self->printed > 1
1675         #    && $self->printed > 0
1676         #  );
1677
1678   #and subroutine for the template
1679   sub FS::cust_bill::_template::invoice_lines {
1680     my $lines = shift || scalar(@buf);
1681     map { 
1682       scalar(@buf) ? shift @buf : [ '', '' ];
1683     }
1684     ( 1 .. $lines );
1685   }
1686
1687   #and fill it in
1688   $FS::cust_bill::_template::page = 1;
1689   my $lines;
1690   my @collect;
1691   while (@buf) {
1692     push @collect, split("\n",
1693       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1694     );
1695     $FS::cust_bill::_template::page++;
1696   }
1697
1698   map "$_\n", @collect;
1699
1700 }
1701
1702 =item print_latex [ TIME [ , TEMPLATE ] ]
1703
1704 Internal method - returns a filename of a filled-in LaTeX template for this
1705 invoice (Note: add ".tex" to get the actual filename).
1706
1707 See print_ps and print_pdf for methods that return PostScript and PDF output.
1708
1709 TIME an optional value used to control the printing of overdue messages.  The
1710 default is now.  It isn't the date of the invoice; that's the `_date' field.
1711 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1712 L<Time::Local> and L<Date::Parse> for conversion functions.
1713
1714 =cut
1715
1716 #still some false laziness w/print_text and print_html (and send_csv) (mostly print_text should use _items stuff though)
1717 sub print_latex {
1718
1719   my( $self, $today, $template ) = @_;
1720   $today ||= time;
1721   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1722     if $DEBUG;
1723
1724   my $cust_main = $self->cust_main;
1725   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1726     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1727
1728   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1729 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1730   #my $balance_due = $self->owed + $pr_total - $cr_total;
1731   my $balance_due = $self->owed + $pr_total;
1732
1733   #create the template
1734   $template ||= $self->_agent_template;
1735   my $templatefile = 'invoice_latex';
1736   my $suffix = length($template) ? "_$template" : '';
1737   $templatefile .= $suffix;
1738   my @invoice_template = map "$_\n", $conf->config($templatefile)
1739     or die "cannot load config file $templatefile";
1740
1741   my($format, $text_template);
1742   if ( grep { /^%%Detail/ } @invoice_template ) {
1743     #change this to a die when the old code is removed
1744     warn "old-style invoice template $templatefile; ".
1745          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1746     $format = 'old';
1747   } else {
1748     $format = 'Text::Template';
1749     $text_template = new Text::Template(
1750       TYPE => 'ARRAY',
1751       SOURCE => \@invoice_template,
1752       DELIMITERS => [ '[@--', '--@]' ],
1753     );
1754
1755     $text_template->compile()
1756       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1757   }
1758
1759   my $returnaddress;
1760   if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1761     $returnaddress = join("\n",
1762       $conf->config_orbase('invoice_latexreturnaddress', $template)
1763     );
1764   } else {
1765     $returnaddress = '~';
1766   }
1767
1768   my %invoice_data = (
1769     'invnum'       => $self->invnum,
1770     'date'         => time2str('%b %o, %Y', $self->_date),
1771     'today'        => time2str('%b %o, %Y', $today),
1772     'agent'        => _latex_escape($cust_main->agent->agent),
1773     'payname'      => _latex_escape($cust_main->payname),
1774     'company'      => _latex_escape($cust_main->company),
1775     'address1'     => _latex_escape($cust_main->address1),
1776     'address2'     => _latex_escape($cust_main->address2),
1777     'city'         => _latex_escape($cust_main->city),
1778     'state'        => _latex_escape($cust_main->state),
1779     'zip'          => _latex_escape($cust_main->zip),
1780     'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
1781     'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
1782     'returnaddress' => $returnaddress,
1783     'quantity'     => 1,
1784     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1785     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1786     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1787   );
1788
1789   my $countrydefault = $conf->config('countrydefault') || 'US';
1790   if ( $cust_main->country eq $countrydefault ) {
1791     $invoice_data{'country'} = '';
1792   } else {
1793     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1794   }
1795
1796   $invoice_data{'notes'} =
1797     join("\n",
1798 #  #do variable substitutions in notes
1799 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1800         $conf->config_orbase('invoice_latexnotes', $template)
1801     );
1802   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1803     if $DEBUG;
1804
1805   $invoice_data{'footer'} =~ s/\n+$//;
1806   $invoice_data{'smallfooter'} =~ s/\n+$//;
1807   $invoice_data{'notes'} =~ s/\n+$//;
1808
1809   $invoice_data{'po_line'} =
1810     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1811       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1812       : '~';
1813
1814   my @filled_in = ();
1815   if ( $format eq 'old' ) {
1816   
1817     my @line_item = ();
1818     my @total_item = ();
1819     while ( @invoice_template ) {
1820       my $line = shift @invoice_template;
1821   
1822       if ( $line =~ /^%%Detail\s*$/ ) {
1823   
1824         while ( ( my $line_item_line = shift @invoice_template )
1825                 !~ /^%%EndDetail\s*$/                            ) {
1826           push @line_item, $line_item_line;
1827         }
1828         foreach my $line_item ( $self->_items ) {
1829         #foreach my $line_item ( $self->_items_pkg ) {
1830           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1831           $invoice_data{'description'} =
1832             _latex_escape($line_item->{'description'});
1833           if ( exists $line_item->{'ext_description'} ) {
1834             $invoice_data{'description'} .=
1835               "\\tabularnewline\n~~".
1836               join( "\\tabularnewline\n~~",
1837                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1838                   );
1839           }
1840           $invoice_data{'amount'} = $line_item->{'amount'};
1841           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1842           push @filled_in,
1843             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1844         }
1845   
1846       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1847   
1848         while ( ( my $total_item_line = shift @invoice_template )
1849                 !~ /^%%EndTotalDetails\s*$/                      ) {
1850           push @total_item, $total_item_line;
1851         }
1852   
1853         my @total_fill = ();
1854   
1855         my $taxtotal = 0;
1856         foreach my $tax ( $self->_items_tax ) {
1857           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1858           $taxtotal += $tax->{'amount'};
1859           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1860           push @total_fill,
1861             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1862                 @total_item;
1863         }
1864
1865         if ( $taxtotal ) {
1866           $invoice_data{'total_item'} = 'Sub-total';
1867           $invoice_data{'total_amount'} =
1868             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1869           unshift @total_fill,
1870             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1871                 @total_item;
1872         }
1873   
1874         $invoice_data{'total_item'} = '\textbf{Total}';
1875         $invoice_data{'total_amount'} =
1876           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1877         push @total_fill,
1878           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1879               @total_item;
1880   
1881         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1882   
1883         # credits
1884         foreach my $credit ( $self->_items_credits ) {
1885           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1886           #$credittotal
1887           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1888           push @total_fill, 
1889             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1890                 @total_item;
1891         }
1892   
1893         # payments
1894         foreach my $payment ( $self->_items_payments ) {
1895           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1896           #$paymenttotal
1897           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1898           push @total_fill, 
1899             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1900                 @total_item;
1901         }
1902   
1903         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1904         $invoice_data{'total_amount'} =
1905           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1906         push @total_fill,
1907           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1908               @total_item;
1909   
1910         push @filled_in, @total_fill;
1911   
1912       } else {
1913         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1914         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1915         push @filled_in, $line;
1916       }
1917   
1918     }
1919
1920     sub nounder {
1921       my $var = $1;
1922       $var =~ s/_/\-/g;
1923       $var;
1924     }
1925
1926   } elsif ( $format eq 'Text::Template' ) {
1927
1928     my @detail_items = ();
1929     my @total_items = ();
1930
1931     $invoice_data{'detail_items'} = \@detail_items;
1932     $invoice_data{'total_items'} = \@total_items;
1933   
1934     foreach my $line_item ( $self->_items ) {
1935       my $detail = {
1936         ext_description => [],
1937       };
1938       $detail->{'ref'} = $line_item->{'pkgnum'};
1939       $detail->{'quantity'} = 1;
1940       $detail->{'description'} = _latex_escape($line_item->{'description'});
1941       if ( exists $line_item->{'ext_description'} ) {
1942         @{$detail->{'ext_description'}} = map {
1943           _latex_escape($_);
1944         } @{$line_item->{'ext_description'}};
1945       }
1946       $detail->{'amount'} = $line_item->{'amount'};
1947       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1948   
1949       push @detail_items, $detail;
1950     }
1951   
1952   
1953     my $taxtotal = 0;
1954     foreach my $tax ( $self->_items_tax ) {
1955       my $total = {};
1956       $total->{'total_item'} = _latex_escape($tax->{'description'});
1957       $taxtotal += $tax->{'amount'};
1958       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1959       push @total_items, $total;
1960     }
1961   
1962     if ( $taxtotal ) {
1963       my $total = {};
1964       $total->{'total_item'} = 'Sub-total';
1965       $total->{'total_amount'} =
1966         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1967       unshift @total_items, $total;
1968     }
1969   
1970     {
1971       my $total = {};
1972       $total->{'total_item'} = '\textbf{Total}';
1973       $total->{'total_amount'} =
1974         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1975       push @total_items, $total;
1976     }
1977   
1978     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1979   
1980     # credits
1981     foreach my $credit ( $self->_items_credits ) {
1982       my $total;
1983       $total->{'total_item'} = _latex_escape($credit->{'description'});
1984       #$credittotal
1985       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1986       push @total_items, $total;
1987     }
1988   
1989     # payments
1990     foreach my $payment ( $self->_items_payments ) {
1991       my $total = {};
1992       $total->{'total_item'} = _latex_escape($payment->{'description'});
1993       #$paymenttotal
1994       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1995       push @total_items, $total;
1996     }
1997   
1998     { 
1999       my $total;
2000       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
2001       $total->{'total_amount'} =
2002         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
2003       push @total_items, $total;
2004     }
2005
2006   } else {
2007     die "guru meditation #54";
2008   }
2009
2010   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2011   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2012                            DIR      => $dir,
2013                            SUFFIX   => '.tex',
2014                            UNLINK   => 0,
2015                          ) or die "can't open temp file: $!\n";
2016   if ( $format eq 'old' ) {
2017     print $fh join('', @filled_in );
2018   } elsif ( $format eq 'Text::Template' ) {
2019     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
2020   } else {
2021     die "guru meditation #32";
2022   }
2023   close $fh;
2024
2025   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2026   return $1;
2027
2028 }
2029
2030 =item print_ps [ TIME [ , TEMPLATE ] ]
2031
2032 Returns an postscript invoice, as a scalar.
2033
2034 TIME an optional value used to control the printing of overdue messages.  The
2035 default is now.  It isn't the date of the invoice; that's the `_date' field.
2036 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2037 L<Time::Local> and L<Date::Parse> for conversion functions.
2038
2039 =cut
2040
2041 sub print_ps {
2042   my $self = shift;
2043
2044   my $file = $self->print_latex(@_);
2045
2046   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2047   chdir($dir);
2048
2049   my $sfile = shell_quote $file;
2050
2051   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2052     or die "pslatex $file.tex failed; see $file.log for details?\n";
2053   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2054     or die "pslatex $file.tex failed; see $file.log for details?\n";
2055
2056   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
2057     or die "dvips failed";
2058
2059   open(POSTSCRIPT, "<$file.ps")
2060     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
2061
2062   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
2063
2064   my $ps = '';
2065   while (<POSTSCRIPT>) {
2066     $ps .= $_;
2067   }
2068
2069   close POSTSCRIPT;
2070
2071   return $ps;
2072
2073 }
2074
2075 =item print_pdf [ TIME [ , TEMPLATE ] ]
2076
2077 Returns an PDF invoice, as a scalar.
2078
2079 TIME an optional value used to control the printing of overdue messages.  The
2080 default is now.  It isn't the date of the invoice; that's the `_date' field.
2081 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2082 L<Time::Local> and L<Date::Parse> for conversion functions.
2083
2084 =cut
2085
2086 sub print_pdf {
2087   my $self = shift;
2088
2089   my $file = $self->print_latex(@_);
2090
2091   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2092   chdir($dir);
2093
2094   #system('pdflatex', "$file.tex");
2095   #system('pdflatex', "$file.tex");
2096   #! LaTeX Error: Unknown graphics extension: .eps.
2097
2098   my $sfile = shell_quote $file;
2099
2100   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2101     or die "pslatex $file.tex failed; see $file.log for details?\n";
2102   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
2103     or die "pslatex $file.tex failed; see $file.log for details?\n";
2104
2105   #system('dvipdf', "$file.dvi", "$file.pdf" );
2106   system(
2107     "dvips -q -t letter -f $sfile.dvi ".
2108     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
2109     "     -c save pop -"
2110   ) == 0
2111     or die "dvips | gs failed: $!";
2112
2113   open(PDF, "<$file.pdf")
2114     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
2115
2116   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
2117
2118   my $pdf = '';
2119   while (<PDF>) {
2120     $pdf .= $_;
2121   }
2122
2123   close PDF;
2124
2125   return $pdf;
2126
2127 }
2128
2129 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2130
2131 Returns an HTML invoice, as a scalar.
2132
2133 TIME an optional value used to control the printing of overdue messages.  The
2134 default is now.  It isn't the date of the invoice; that's the `_date' field.
2135 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2136 L<Time::Local> and L<Date::Parse> for conversion functions.
2137
2138 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2139 when emailing the invoice as part of a multipart/related MIME email.
2140
2141 =cut
2142
2143 #some falze laziness w/print_text and print_latex (and send_csv)
2144 sub print_html {
2145   my( $self, $today, $template, $cid ) = @_;
2146   $today ||= time;
2147
2148   my $cust_main = $self->cust_main;
2149   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2150     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
2151
2152   $template ||= $self->_agent_template;
2153   my $templatefile = 'invoice_html';
2154   my $suffix = length($template) ? "_$template" : '';
2155   $templatefile .= $suffix;
2156   my @html_template = map "$_\n", $conf->config($templatefile)
2157     or die "cannot load config file $templatefile";
2158
2159   my $html_template = new Text::Template(
2160     TYPE   => 'ARRAY',
2161     SOURCE => \@html_template,
2162     DELIMITERS => [ '<%=', '%>' ],
2163   );
2164
2165   $html_template->compile()
2166     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
2167
2168   my %invoice_data = (
2169     'invnum'       => $self->invnum,
2170     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
2171     'today'        => time2str('%b %o, %Y', $today),
2172     'agent'        => encode_entities($cust_main->agent->agent),
2173     'payname'      => encode_entities($cust_main->payname),
2174     'company'      => encode_entities($cust_main->company),
2175     'address1'     => encode_entities($cust_main->address1),
2176     'address2'     => encode_entities($cust_main->address2),
2177     'city'         => encode_entities($cust_main->city),
2178     'state'        => encode_entities($cust_main->state),
2179     'zip'          => encode_entities($cust_main->zip),
2180     'terms'        => $conf->config('invoice_default_terms')
2181                       || 'Payable upon receipt',
2182     'cid'          => $cid,
2183     'template'     => $template,
2184 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2185   );
2186
2187   if (
2188          defined( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2189       && length(  $conf->config_orbase('invoice_htmlreturnaddress', $template) )
2190   ) {
2191     $invoice_data{'returnaddress'} =
2192       join("\n", $conf->config('invoice_htmlreturnaddress', $template) );
2193   } else {
2194     $invoice_data{'returnaddress'} =
2195       join("\n", map { 
2196                        s/~/&nbsp;/g;
2197                        s/\\\\\*?\s*$/<BR>/;
2198                        s/\\hyphenation\{[\w\s\-]+\}//;
2199                        $_;
2200                      }
2201                      $conf->config_orbase( 'invoice_latexreturnaddress',
2202                                            $template
2203                                          )
2204           );
2205   }
2206
2207   my $countrydefault = $conf->config('countrydefault') || 'US';
2208   if ( $cust_main->country eq $countrydefault ) {
2209     $invoice_data{'country'} = '';
2210   } else {
2211     $invoice_data{'country'} =
2212       encode_entities(code2country($cust_main->country));
2213   }
2214
2215   if (
2216          defined( $conf->config_orbase('invoice_htmlnotes', $template) )
2217       && length(  $conf->config_orbase('invoice_htmlnotes', $template) )
2218   ) {
2219     $invoice_data{'notes'} =
2220       join("\n", $conf->config_orbase('invoice_htmlnotes', $template) );
2221   } else {
2222     $invoice_data{'notes'} = 
2223       join("\n", map { 
2224                        s/%%(.*)$/<!-- $1 -->/;
2225                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
2226                        s/\\begin\{enumerate\}/<ol>/;
2227                        s/\\item /  <li>/;
2228                        s/\\end\{enumerate\}/<\/ol>/;
2229                        s/\\textbf\{(.*)\}/<b>$1<\/b>/;
2230                        $_;
2231                      } 
2232                      $conf->config_orbase('invoice_latexnotes', $template)
2233           );
2234   }
2235
2236 #  #do variable substitutions in notes
2237 #  $invoice_data{'notes'} =
2238 #    join("\n",
2239 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
2240 #        $conf->config_orbase('invoice_latexnotes', $suffix)
2241 #    );
2242
2243   if (
2244          defined( $conf->config_orbase('invoice_htmlfooter', $template) )
2245       && length(  $conf->config_orbase('invoice_htmlfooter', $template) )
2246   ) {
2247    $invoice_data{'footer'} =
2248      join("\n", $conf->config_orbase('invoice_htmlfooter', $template) );
2249   } else {
2250    $invoice_data{'footer'} =
2251        join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
2252                       $conf->config_orbase('invoice_latexfooter', $template)
2253            );
2254   }
2255
2256   $invoice_data{'po_line'} =
2257     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2258       ? encode_entities("Purchase Order #". $cust_main->payinfo)
2259       : '';
2260
2261   my $money_char = $conf->config('money_char') || '$';
2262
2263   foreach my $line_item ( $self->_items ) {
2264     my $detail = {
2265       ext_description => [],
2266     };
2267     $detail->{'ref'} = $line_item->{'pkgnum'};
2268     $detail->{'description'} = encode_entities($line_item->{'description'});
2269     if ( exists $line_item->{'ext_description'} ) {
2270       @{$detail->{'ext_description'}} = map {
2271         encode_entities($_);
2272       } @{$line_item->{'ext_description'}};
2273     }
2274     $detail->{'amount'} = $money_char. $line_item->{'amount'};
2275     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2276
2277     push @{$invoice_data{'detail_items'}}, $detail;
2278   }
2279
2280
2281   my $taxtotal = 0;
2282   foreach my $tax ( $self->_items_tax ) {
2283     my $total = {};
2284     $total->{'total_item'} = encode_entities($tax->{'description'});
2285     $taxtotal += $tax->{'amount'};
2286     $total->{'total_amount'} = $money_char. $tax->{'amount'};
2287     push @{$invoice_data{'total_items'}}, $total;
2288   }
2289
2290   if ( $taxtotal ) {
2291     my $total = {};
2292     $total->{'total_item'} = 'Sub-total';
2293     $total->{'total_amount'} =
2294       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
2295     unshift @{$invoice_data{'total_items'}}, $total;
2296   }
2297
2298   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2299   {
2300     my $total = {};
2301     $total->{'total_item'} = '<b>Total</b>';
2302     $total->{'total_amount'} =
2303       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
2304     push @{$invoice_data{'total_items'}}, $total;
2305   }
2306
2307   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2308
2309   # credits
2310   foreach my $credit ( $self->_items_credits ) {
2311     my $total;
2312     $total->{'total_item'} = encode_entities($credit->{'description'});
2313     #$credittotal
2314     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
2315     push @{$invoice_data{'total_items'}}, $total;
2316   }
2317
2318   # payments
2319   foreach my $payment ( $self->_items_payments ) {
2320     my $total = {};
2321     $total->{'total_item'} = encode_entities($payment->{'description'});
2322     #$paymenttotal
2323     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
2324     push @{$invoice_data{'total_items'}}, $total;
2325   }
2326
2327   { 
2328     my $total;
2329     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
2330     $total->{'total_amount'} =
2331       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
2332     push @{$invoice_data{'total_items'}}, $total;
2333   }
2334
2335   $html_template->fill_in( HASH => \%invoice_data);
2336 }
2337
2338 # quick subroutine for print_latex
2339 #
2340 # There are ten characters that LaTeX treats as special characters, which
2341 # means that they do not simply typeset themselves: 
2342 #      # $ % & ~ _ ^ \ { }
2343 #
2344 # TeX ignores blanks following an escaped character; if you want a blank (as
2345 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2346
2347 sub _latex_escape {
2348   my $value = shift;
2349   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2350   $value =~ s/([<>])/\$$1\$/g;
2351   $value;
2352 }
2353
2354 #utility methods for print_*
2355
2356 sub balance_due_msg {
2357   my $self = shift;
2358   my $msg = 'Balance Due';
2359   return $msg unless $conf->exists('invoice_default_terms');
2360   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
2361     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
2362   } elsif ( $conf->config('invoice_default_terms') ) {
2363     $msg .= ' - '. $conf->config('invoice_default_terms');
2364   }
2365   $msg;
2366 }
2367
2368 sub _items {
2369   my $self = shift;
2370   my @display = scalar(@_)
2371                 ? @_
2372                 : qw( _items_previous _items_pkg );
2373                 #: qw( _items_pkg );
2374                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2375   my @b = ();
2376   foreach my $display ( @display ) {
2377     push @b, $self->$display(@_);
2378   }
2379   @b;
2380 }
2381
2382 sub _items_previous {
2383   my $self = shift;
2384   my $cust_main = $self->cust_main;
2385   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2386   my @b = ();
2387   foreach ( @pr_cust_bill ) {
2388     push @b, {
2389       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2390                        ' ('. time2str('%x',$_->_date). ')',
2391       #'pkgpart'     => 'N/A',
2392       'pkgnum'      => 'N/A',
2393       'amount'      => sprintf("%.2f", $_->owed),
2394     };
2395   }
2396   @b;
2397
2398   #{
2399   #    'description'     => 'Previous Balance',
2400   #    #'pkgpart'         => 'N/A',
2401   #    'pkgnum'          => 'N/A',
2402   #    'amount'          => sprintf("%10.2f", $pr_total ),
2403   #    'ext_description' => [ map {
2404   #                                 "Invoice ". $_->invnum.
2405   #                                 " (". time2str("%x",$_->_date). ") ".
2406   #                                 sprintf("%10.2f", $_->owed)
2407   #                         } @pr_cust_bill ],
2408
2409   #};
2410 }
2411
2412 sub _items_pkg {
2413   my $self = shift;
2414   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2415   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2416 }
2417
2418 sub _items_tax {
2419   my $self = shift;
2420   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
2421   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2422 }
2423
2424 sub _items_cust_bill_pkg {
2425   my $self = shift;
2426   my $cust_bill_pkg = shift;
2427
2428   my @b = ();
2429   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
2430
2431     my $desc = $cust_bill_pkg->desc;
2432
2433     if ( $cust_bill_pkg->pkgnum > 0 ) {
2434
2435       if ( $cust_bill_pkg->setup != 0 ) {
2436         my $description = $desc;
2437         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2438         my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
2439         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2440         push @b, {
2441           description     => $description,
2442           #pkgpart         => $part_pkg->pkgpart,
2443           pkgnum          => $cust_bill_pkg->pkgnum,
2444           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2445           ext_description => \@d,
2446         };
2447       }
2448
2449       if ( $cust_bill_pkg->recur != 0 ) {
2450         push @b, {
2451           description     => "$desc (" .
2452                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
2453                                time2str('%x', $cust_bill_pkg->edate). ')',
2454           #pkgpart         => $part_pkg->pkgpart,
2455           pkgnum          => $cust_bill_pkg->pkgnum,
2456           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2457           ext_description =>
2458             [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
2459                                                         $cust_bill_pkg->sdate),
2460               $cust_bill_pkg->details,
2461             ],
2462         };
2463       }
2464
2465     } else { #pkgnum tax or one-shot line item (??)
2466
2467       if ( $cust_bill_pkg->setup != 0 ) {
2468         push @b, {
2469           'description' => $desc,
2470           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2471         };
2472       }
2473       if ( $cust_bill_pkg->recur != 0 ) {
2474         push @b, {
2475           'description' => "$desc (".
2476                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2477                            time2str("%x", $cust_bill_pkg->edate). ')',
2478           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2479         };
2480       }
2481
2482     }
2483
2484   }
2485
2486   @b;
2487
2488 }
2489
2490 sub _items_credits {
2491   my $self = shift;
2492
2493   my @b;
2494   #credits
2495   foreach ( $self->cust_credited ) {
2496
2497     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2498
2499     my $reason = $_->cust_credit->reason;
2500     #my $reason = substr($_->cust_credit->reason,0,32);
2501     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2502     $reason = " ($reason) " if $reason;
2503     push @b, {
2504       #'description' => 'Credit ref\#'. $_->crednum.
2505       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2506       #                 $reason,
2507       'description' => 'Credit applied '.
2508                        time2str("%x",$_->cust_credit->_date). $reason,
2509       'amount'      => sprintf("%.2f",$_->amount),
2510     };
2511   }
2512   #foreach ( @cr_cust_credit ) {
2513   #  push @buf,[
2514   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2515   #    $money_char. sprintf("%10.2f",$_->credited)
2516   #  ];
2517   #}
2518
2519   @b;
2520
2521 }
2522
2523 sub _items_payments {
2524   my $self = shift;
2525
2526   my @b;
2527   #get & print payments
2528   foreach ( $self->cust_bill_pay ) {
2529
2530     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2531
2532     push @b, {
2533       'description' => "Payment received ".
2534                        time2str("%x",$_->cust_pay->_date ),
2535       'amount'      => sprintf("%.2f", $_->amount )
2536     };
2537   }
2538
2539   @b;
2540
2541 }
2542
2543
2544 =back
2545
2546 =head1 SUBROUTINES
2547
2548 =over 4
2549
2550 =item reprint
2551
2552 =cut
2553
2554 sub process_reprint {
2555   process_re_X('print', @_);
2556 }
2557
2558 =item reemail
2559
2560 =cut
2561
2562 sub process_reemail {
2563   process_re_X('email', @_);
2564 }
2565
2566 =item refax
2567
2568 =cut
2569
2570 sub process_refax {
2571   process_re_X('fax', @_);
2572 }
2573
2574 use Storable qw(thaw);
2575 use Data::Dumper;
2576 use MIME::Base64;
2577 sub process_re_X {
2578   my( $method, $job ) = ( shift, shift );
2579   warn "process_re_X $method for job $job\n" if $DEBUG;
2580
2581   my $param = thaw(decode_base64(shift));
2582   warn Dumper($param) if $DEBUG;
2583
2584   re_X(
2585     $method,
2586     $job,
2587     %$param,
2588   );
2589
2590 }
2591
2592 sub re_X {
2593   my($method, $job, %param ) = @_;
2594 #              [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ],
2595   if ( $DEBUG ) {
2596     warn "re_X $method for job $job with param:\n".
2597          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
2598   }
2599
2600   #some false laziness w/search/cust_bill.html
2601   my $distinct = '';
2602   my $orderby = 'ORDER BY cust_bill._date';
2603
2604   my @where;
2605
2606   if ( $param{'begin'} =~ /^(\d+)$/ ) {
2607     push @where, "cust_bill._date >= $1";
2608   }
2609   if ( $param{'end'} =~ /^(\d+)$/ ) {
2610     push @where, "cust_bill._date < $1";
2611   }
2612   if ( $param{'agentnum'} =~ /^(\d+)$/ ) {
2613     push @where, "cust_main.agentnum = $1";
2614   }
2615
2616   my $owed =
2617     "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
2618                  WHERE cust_bill_pay.invnum = cust_bill.invnum )
2619              - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
2620                  WHERE cust_credit_bill.invnum = cust_bill.invnum )";
2621
2622   push @where, "0 != $owed"
2623     if $param{'open'};
2624
2625   push @where, "cust_bill._date < ". (time-86400*$param{'days'})
2626     if $param{'days'};
2627
2628   my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : '';
2629
2630   my $addl_from = 'left join cust_main using ( custnum )';
2631
2632   if ( $param{'newest_percust'} ) {
2633     $distinct = 'DISTINCT ON ( cust_bill.custnum )';
2634     $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
2635     #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
2636   }
2637      
2638   my @cust_bill = qsearch( 'cust_bill',
2639                            {},
2640                            "$distinct cust_bill.*",
2641                            $extra_sql,
2642                            '',
2643                            $addl_from
2644                          );
2645
2646   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2647   foreach my $cust_bill ( @cust_bill ) {
2648     $cust_bill->$method();
2649
2650     if ( $job ) { #progressbar foo
2651       $num++;
2652       if ( time - $min_sec > $last ) {
2653         my $error = $job->update_statustext(
2654           int( 100 * $num / scalar(@cust_bill) )
2655         );
2656         die $error if $error;
2657         $last = time;
2658       }
2659     }
2660
2661   }
2662
2663 }
2664
2665 =back
2666
2667 =head1 BUGS
2668
2669 The delete method.
2670
2671 print_text formatting (and some logic :/) is in source, but needs to be
2672 slurped in from a file.  Also number of lines ($=).
2673
2674 =head1 SEE ALSO
2675
2676 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2677 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2678 documentation.
2679
2680 =cut
2681
2682 1;
2683