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