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