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