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