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