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