fix picking up alternate invoice_latexnotes_* files, and expand country codes on...
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Date::Format;
7 use Text::Template 1.20;
8 use File::Temp 0.14;
9 use String::ShellQuote;
10 use HTML::Entities;
11 use Locale::Country;
12 use FS::UID qw( datasrc );
13 use FS::Record qw( qsearch qsearchs );
14 use FS::Misc qw( send_email send_fax );
15 use FS::cust_main;
16 use FS::cust_bill_pkg;
17 use FS::cust_credit;
18 use FS::cust_pay;
19 use FS::cust_pkg;
20 use FS::cust_credit_bill;
21 use FS::cust_pay_batch;
22 use FS::cust_bill_event;
23
24 @ISA = qw( FS::Record );
25
26 $DEBUG = 1;
27
28 #ask FS::UID to run this stuff for us later
29 FS::UID->install_callback( sub { 
30   $conf = new FS::Conf;
31   $money_char = $conf->config('money_char') || '$';  
32 } );
33
34 =head1 NAME
35
36 FS::cust_bill - Object methods for cust_bill records
37
38 =head1 SYNOPSIS
39
40   use FS::cust_bill;
41
42   $record = new FS::cust_bill \%hash;
43   $record = new FS::cust_bill { 'column' => 'value' };
44
45   $error = $record->insert;
46
47   $error = $new_record->replace($old_record);
48
49   $error = $record->delete;
50
51   $error = $record->check;
52
53   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
54
55   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
56
57   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
58
59   @cust_pay_objects = $cust_bill->cust_pay;
60
61   $tax_amount = $record->tax;
62
63   @lines = $cust_bill->print_text;
64   @lines = $cust_bill->print_text $time;
65
66 =head1 DESCRIPTION
67
68 An FS::cust_bill object represents an invoice; a declaration that a customer
69 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
70 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
71 following fields are currently supported:
72
73 =over 4
74
75 =item invnum - primary key (assigned automatically for new invoices)
76
77 =item custnum - customer (see L<FS::cust_main>)
78
79 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
80 L<Time::Local> and L<Date::Parse> for conversion functions.
81
82 =item charged - amount of this invoice
83
84 =item printed - deprecated
85
86 =item closed - books closed flag, empty or `Y'
87
88 =back
89
90 =head1 METHODS
91
92 =over 4
93
94 =item new HASHREF
95
96 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
97 Invoices are normally created by calling the bill method of a customer object
98 (see L<FS::cust_main>).
99
100 =cut
101
102 sub table { 'cust_bill'; }
103
104 =item insert
105
106 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
107 returns the error, otherwise returns false.
108
109 =item delete
110
111 Currently unimplemented.  I don't remove invoices because there would then be
112 no record you ever posted this invoice (which is bad, no?)
113
114 =cut
115
116 sub delete {
117   my $self = shift;
118   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
119   $self->SUPER::delete(@_);
120 }
121
122 =item replace OLD_RECORD
123
124 Replaces the OLD_RECORD with this one in the database.  If there is an error,
125 returns the error, otherwise returns false.
126
127 Only printed may be changed.  printed is normally updated by calling the
128 collect method of a customer object (see L<FS::cust_main>).
129
130 =cut
131
132 sub replace {
133   my( $new, $old ) = ( shift, shift );
134   return "Can't change custnum!" unless $old->custnum == $new->custnum;
135   #return "Can't change _date!" unless $old->_date eq $new->_date;
136   return "Can't change _date!" unless $old->_date == $new->_date;
137   return "Can't change charged!" unless $old->charged == $new->charged;
138
139   $new->SUPER::replace($old);
140 }
141
142 =item check
143
144 Checks all fields to make sure this is a valid invoice.  If there is an error,
145 returns the error, otherwise returns false.  Called by the insert and replace
146 methods.
147
148 =cut
149
150 sub check {
151   my $self = shift;
152
153   my $error =
154     $self->ut_numbern('invnum')
155     || $self->ut_number('custnum')
156     || $self->ut_numbern('_date')
157     || $self->ut_money('charged')
158     || $self->ut_numbern('printed')
159     || $self->ut_enum('closed', [ '', 'Y' ])
160   ;
161   return $error if $error;
162
163   return "Unknown customer"
164     unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
165
166   $self->_date(time) unless $self->_date;
167
168   $self->printed(0) if $self->printed eq '';
169
170   $self->SUPER::check;
171 }
172
173 =item previous
174
175 Returns a list consisting of the total previous balance for this customer, 
176 followed by the previous outstanding invoices (as FS::cust_bill objects also).
177
178 =cut
179
180 sub previous {
181   my $self = shift;
182   my $total = 0;
183   my @cust_bill = sort { $a->_date <=> $b->_date }
184     grep { $_->owed != 0 && $_->_date < $self->_date }
185       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
186   ;
187   foreach ( @cust_bill ) { $total += $_->owed; }
188   $total, @cust_bill;
189 }
190
191 =item cust_bill_pkg
192
193 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
194
195 =cut
196
197 sub cust_bill_pkg {
198   my $self = shift;
199   qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
200 }
201
202 =item cust_bill_event
203
204 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
205 invoice.
206
207 =cut
208
209 sub cust_bill_event {
210   my $self = shift;
211   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
212 }
213
214
215 =item cust_main
216
217 Returns the customer (see L<FS::cust_main>) for this invoice.
218
219 =cut
220
221 sub cust_main {
222   my $self = shift;
223   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
224 }
225
226 =item cust_credit
227
228 Depreciated.  See the cust_credited method.
229
230  #Returns a list consisting of the total previous credited (see
231  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
232  #outstanding credits (FS::cust_credit objects).
233
234 =cut
235
236 sub cust_credit {
237   use Carp;
238   croak "FS::cust_bill->cust_credit depreciated; see ".
239         "FS::cust_bill->cust_credit_bill";
240   #my $self = shift;
241   #my $total = 0;
242   #my @cust_credit = sort { $a->_date <=> $b->_date }
243   #  grep { $_->credited != 0 && $_->_date < $self->_date }
244   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
245   #;
246   #foreach (@cust_credit) { $total += $_->credited; }
247   #$total, @cust_credit;
248 }
249
250 =item cust_pay
251
252 Depreciated.  See the cust_bill_pay method.
253
254 #Returns all payments (see L<FS::cust_pay>) for this invoice.
255
256 =cut
257
258 sub cust_pay {
259   use Carp;
260   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
261   #my $self = shift;
262   #sort { $a->_date <=> $b->_date }
263   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
264   #;
265 }
266
267 =item cust_bill_pay
268
269 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
270
271 =cut
272
273 sub cust_bill_pay {
274   my $self = shift;
275   sort { $a->_date <=> $b->_date }
276     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
277 }
278
279 =item cust_credited
280
281 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
282
283 =cut
284
285 sub cust_credited {
286   my $self = shift;
287   sort { $a->_date <=> $b->_date }
288     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
289   ;
290 }
291
292 =item tax
293
294 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
295
296 =cut
297
298 sub tax {
299   my $self = shift;
300   my $total = 0;
301   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
302                                              'pkgnum' => 0 } );
303   foreach (@taxlines) { $total += $_->setup; }
304   $total;
305 }
306
307 =item owed
308
309 Returns the amount owed (still outstanding) on this invoice, which is charged
310 minus all payment applications (see L<FS::cust_bill_pay>) and credit
311 applications (see L<FS::cust_credit_bill>).
312
313 =cut
314
315 sub owed {
316   my $self = shift;
317   my $balance = $self->charged;
318   $balance -= $_->amount foreach ( $self->cust_bill_pay );
319   $balance -= $_->amount foreach ( $self->cust_credited );
320   $balance = sprintf( "%.2f", $balance);
321   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
322   $balance;
323 }
324
325
326 =item generate_email PARAMHASH
327
328 PARAMHASH can contain the following:
329
330 =over 4
331
332 =item from       => sender address, required
333
334 =item tempate    => alternate template name, optional
335
336 =item print_text => text attachment arrayref, optional
337
338 =item subject    => email subject, optional
339
340 =back
341
342 Returns an argument list to be passed to L<FS::Misc::send_email>.
343
344 =cut
345
346 sub generate_email {
347
348   my $self = shift;
349   my %args = @_;
350
351   my $mimeparts;
352   if ($conf->exists('invoice_email_pdf')) {
353     #warn "[FS::cust_bill::send] creating PDF attachment";
354     #mime parts arguments a la MIME::Entity->build().
355     $mimeparts = [
356       {
357         'Type'        => 'application/pdf',
358         'Encoding'    => 'base64',
359         'Data'        => [ $self->print_pdf('', $args{'template'}) ],
360         'Disposition' => 'attachment',
361         'Filename'    => 'invoice.pdf',
362       },
363     ];
364   }
365
366   my $email_text;
367   if ($conf->exists('invoice_email_pdf')
368       and scalar($conf->config('invoice_email_pdf_note'))) {
369
370     #warn "[FS::cust_bill::send] using 'invoice_email_pdf_note'";
371     $email_text = [ map { $_ . "\n" } $conf->config('invoice_email_pdf_note') ];
372   } else {
373     #warn "[FS::cust_bill::send] not using 'invoice_email_pdf_note'";
374     if (ref($args{'print_text'}) eq 'ARRAY') {
375       $email_text = $args{'print_text'};
376     } else {
377       $email_text = [ $self->print_text('', $args{'template'}) ];
378     }
379   }
380
381   my @invoicing_list;
382   if (ref($args{'to'} eq 'ARRAY')) {
383     @invoicing_list = @{$args{'to'}};
384   } else {
385     @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list;
386   }
387
388   return (
389     'from'      => $args{'from'},
390     'to'        => [ @invoicing_list ],
391     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
392     'body'      => $email_text,
393     'mimeparts' => $mimeparts,
394   );
395
396
397 }
398
399 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
400
401 Sends this invoice to the destinations configured for this customer: send
402 emails or print.  See L<FS::cust_main_invoice>.
403
404 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
405
406 AGENTNUM, if specified, means that this invoice will only be sent for customers
407 of the specified agent.
408
409 INVOICE_FROM, if specified, overrides the default email invoice From: address.
410
411 =cut
412
413 sub send {
414   my $self = shift;
415   my $template = scalar(@_) ? shift : '';
416   return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
417   my $invoice_from =
418     scalar(@_)
419       ? shift
420       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
421
422   my @print_text = $self->print_text('', $template);
423   my @invoicing_list = $self->cust_main->invoicing_list;
424
425   if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list  ) {
426     #email
427
428     #better to notify this person than silence
429     @invoicing_list = ($invoice_from) unless @invoicing_list;
430
431     my $error = send_email(
432       $self->generate_email(
433         'from'       => $invoice_from,
434         'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
435         'print_text' => [ @print_text ],
436       )
437     );
438     die "can't email invoice: $error\n" if $error;
439     #die "$error\n" if $error;
440
441   }
442
443   if ( grep { $_ =~ /^(POST|FAX)$/ } @invoicing_list ) {
444     my $lpr_data;
445     if ($conf->config('invoice_latex')) {
446       $lpr_data = [ $self->print_ps('', $template) ];
447     } else {
448       $lpr_data = \@print_text;
449     }
450
451     if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
452       my $lpr = $conf->config('lpr');
453       open(LPR, "|$lpr")
454         or die "Can't open pipe to $lpr: $!\n";
455       print LPR @{$lpr_data};
456       close LPR
457         or die $! ? "Error closing $lpr: $!\n"
458                   : "Exit status $? from $lpr\n";
459     }
460
461     if ( grep { $_ eq 'FAX' } @invoicing_list ) { #fax
462       die 'FAX invoice destination not supported with plain text invoices.'
463         unless $conf->exists('invoice_latex');
464       my $dialstring = $self->cust_main->getfield('fax');
465       #Check $dialstring?
466       my $error = send_fax(docdata => $lpr_data, dialstring => $dialstring);
467       die $error if $error;
468     }
469
470   }
471
472   '';
473
474 }
475
476 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
477
478 Like B<send>, but only sends the invoice if it is the newest open invoice for
479 this customer.
480
481 =cut
482
483 sub send_if_newest {
484   my $self = shift;
485
486   return ''
487     if scalar(
488                grep { $_->owed > 0 } 
489                     qsearch('cust_bill', {
490                       'custnum' => $self->custnum,
491                       #'_date'   => { op=>'>', value=>$self->_date },
492                       'invnum'  => { op=>'>', value=>$self->invnum },
493                     } )
494              );
495     
496   $self->send(@_);
497 }
498
499 =item send_csv OPTIONS
500
501 Sends invoice as a CSV data-file to a remote host with the specified protocol.
502
503 Options are:
504
505 protocol - currently only "ftp"
506 server
507 username
508 password
509 dir
510
511 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
512 and YYMMDDHHMMSS is a timestamp.
513
514 The fields of the CSV file is as follows:
515
516 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
517
518 =over 4
519
520 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
521
522 If B<record_type> is C<cust_bill>, this is a primary invoice record.  The
523 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
524 fields are filled in.
525
526 If B<record_type> is C<cust_bill_pkg>, this is a line item record.  Only the
527 first two fields (B<record_type> and B<invnum>) and the last five fields
528 (B<pkg> through B<edate>) are filled in.
529
530 =item invnum - invoice number
531
532 =item custnum - customer number
533
534 =item _date - invoice date
535
536 =item charged - total invoice amount
537
538 =item first - customer first name
539
540 =item last - customer first name
541
542 =item company - company name
543
544 =item address1 - address line 1
545
546 =item address2 - address line 1
547
548 =item city
549
550 =item state
551
552 =item zip
553
554 =item country
555
556 =item pkg - line item description
557
558 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
559
560 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
561
562 =item sdate - start date for recurring fee
563
564 =item edate - end date for recurring fee
565
566 =back
567
568 =cut
569
570 sub send_csv {
571   my($self, %opt) = @_;
572
573   #part one: create file
574
575   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
576   mkdir $spooldir, 0700 unless -d $spooldir;
577
578   my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
579
580   open(CSV, ">$file") or die "can't open $file: $!";
581
582   eval "use Text::CSV_XS";
583   die $@ if $@;
584
585   my $csv = Text::CSV_XS->new({'always_quote'=>1});
586
587   my $cust_main = $self->cust_main;
588
589   $csv->combine(
590     'cust_bill',
591     $self->invnum,
592     $self->custnum,
593     time2str("%x", $self->_date),
594     sprintf("%.2f", $self->charged),
595     ( map { $cust_main->getfield($_) }
596         qw( first last company address1 address2 city state zip country ) ),
597     map { '' } (1..5),
598   ) or die "can't create csv";
599   print CSV $csv->string. "\n";
600
601   #new charges (false laziness w/print_text)
602   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
603
604     my($pkg, $setup, $recur, $sdate, $edate);
605     if ( $cust_bill_pkg->pkgnum ) {
606     
607       ($pkg, $setup, $recur, $sdate, $edate) = (
608         $cust_bill_pkg->cust_pkg->part_pkg->pkg,
609         ( $cust_bill_pkg->setup != 0
610           ? sprintf("%.2f", $cust_bill_pkg->setup )
611           : '' ),
612         ( $cust_bill_pkg->recur != 0
613           ? sprintf("%.2f", $cust_bill_pkg->recur )
614           : '' ),
615         time2str("%x", $cust_bill_pkg->sdate),
616         time2str("%x", $cust_bill_pkg->edate),
617       );
618
619     } else { #pkgnum tax
620       next unless $cust_bill_pkg->setup != 0;
621       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
622                        ? ( $cust_bill_pkg->itemdesc || 'Tax' )
623                        : 'Tax';
624       ($pkg, $setup, $recur, $sdate, $edate) =
625         ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
626     }
627
628     $csv->combine(
629       'cust_bill_pkg',
630       $self->invnum,
631       ( map { '' } (1..11) ),
632       ($pkg, $setup, $recur, $sdate, $edate)
633     ) or die "can't create csv";
634     print CSV $csv->string. "\n";
635
636   }
637
638   close CSV or die "can't close CSV: $!";
639
640   #part two: upload it
641
642   my $net;
643   if ( $opt{protocol} eq 'ftp' ) {
644     eval "use Net::FTP;";
645     die $@ if $@;
646     $net = Net::FTP->new($opt{server}) or die @$;
647   } else {
648     die "unknown protocol: $opt{protocol}";
649   }
650
651   $net->login( $opt{username}, $opt{password} )
652     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
653
654   $net->binary or die "can't set binary mode";
655
656   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
657
658   $net->put($file) or die "can't put $file: $!";
659
660   $net->quit;
661
662   unlink $file;
663
664 }
665
666 =item comp
667
668 Pays this invoice with a compliemntary payment.  If there is an error,
669 returns the error, otherwise returns false.
670
671 =cut
672
673 sub comp {
674   my $self = shift;
675   my $cust_pay = new FS::cust_pay ( {
676     'invnum'   => $self->invnum,
677     'paid'     => $self->owed,
678     '_date'    => '',
679     'payby'    => 'COMP',
680     'payinfo'  => $self->cust_main->payinfo,
681     'paybatch' => '',
682   } );
683   $cust_pay->insert;
684 }
685
686 =item realtime_card
687
688 Attempts to pay this invoice with a credit card payment via a
689 Business::OnlinePayment realtime gateway.  See
690 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
691 for supported processors.
692
693 =cut
694
695 sub realtime_card {
696   my $self = shift;
697   $self->realtime_bop( 'CC', @_ );
698 }
699
700 =item realtime_ach
701
702 Attempts to pay this invoice with an electronic check (ACH) payment via a
703 Business::OnlinePayment realtime gateway.  See
704 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
705 for supported processors.
706
707 =cut
708
709 sub realtime_ach {
710   my $self = shift;
711   $self->realtime_bop( 'ECHECK', @_ );
712 }
713
714 =item realtime_lec
715
716 Attempts to pay this invoice with phone bill (LEC) payment via a
717 Business::OnlinePayment realtime gateway.  See
718 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
719 for supported processors.
720
721 =cut
722
723 sub realtime_lec {
724   my $self = shift;
725   $self->realtime_bop( 'LEC', @_ );
726 }
727
728 sub realtime_bop {
729   my( $self, $method ) = @_;
730
731   my $cust_main = $self->cust_main;
732   my $balance = $cust_main->balance;
733   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
734   $amount = sprintf("%.2f", $amount);
735   return "not run (balance $balance)" unless $amount > 0;
736
737   my $description = 'Internet Services';
738   if ( $conf->exists('business-onlinepayment-description') ) {
739     my $dtempl = $conf->config('business-onlinepayment-description');
740
741     my $agent_obj = $cust_main->agent
742       or die "can't retreive agent for $cust_main (agentnum ".
743              $cust_main->agentnum. ")";
744     my $agent = $agent_obj->agent;
745     my $pkgs = join(', ',
746       map { $_->cust_pkg->part_pkg->pkg }
747         grep { $_->pkgnum } $self->cust_bill_pkg
748     );
749     $description = eval qq("$dtempl");
750   }
751
752   $cust_main->realtime_bop($method, $amount,
753     'description' => $description,
754     'invnum'      => $self->invnum,
755   );
756
757 }
758
759 =item batch_card
760
761 Adds a payment for this invoice to the pending credit card batch (see
762 L<FS::cust_pay_batch>).
763
764 =cut
765
766 sub batch_card {
767   my $self = shift;
768   my $cust_main = $self->cust_main;
769
770   my $cust_pay_batch = new FS::cust_pay_batch ( {
771     'invnum'   => $self->getfield('invnum'),
772     'custnum'  => $cust_main->getfield('custnum'),
773     'last'     => $cust_main->getfield('last'),
774     'first'    => $cust_main->getfield('first'),
775     'address1' => $cust_main->getfield('address1'),
776     'address2' => $cust_main->getfield('address2'),
777     'city'     => $cust_main->getfield('city'),
778     'state'    => $cust_main->getfield('state'),
779     'zip'      => $cust_main->getfield('zip'),
780     'country'  => $cust_main->getfield('country'),
781     'cardnum'  => $cust_main->payinfo,
782     'exp'      => $cust_main->getfield('paydate'),
783     'payname'  => $cust_main->getfield('payname'),
784     'amount'   => $self->owed,
785   } );
786   my $error = $cust_pay_batch->insert;
787   die $error if $error;
788
789   '';
790 }
791
792 sub _agent_template {
793   my $self = shift;
794   $self->_agent_plandata('agent_templatename');
795 }
796
797 sub _agent_invoice_from {
798   my $self = shift;
799   $self->_agent_plandata('agent_invoice_from');
800 }
801
802 sub _agent_plandata {
803   my( $self, $option ) = @_;
804
805   my $part_bill_event = qsearchs( 'part_bill_event',
806     {
807       'payby'     => $self->cust_main->payby,
808       'plan'      => 'send_agent',
809       'plandata'  => { 'op'    => '~',
810                        'value' => "(^|\n)agentnum ".
811                                   $self->cust_main->agentnum.
812                                   "(\n|\$)",
813                      },
814     },
815     '',
816     'ORDER BY seconds LIMIT 1'
817   );
818
819   return '' unless $part_bill_event;
820
821   if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
822     return $1;
823   } else {
824     warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
825          " plandata for $option";
826     return '';
827   }
828
829 }
830
831 =item print_text [ TIME [ , TEMPLATE ] ]
832
833 Returns an text invoice, as a list of lines.
834
835 TIME an optional value used to control the printing of overdue messages.  The
836 default is now.  It isn't the date of the invoice; that's the `_date' field.
837 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
838 L<Time::Local> and L<Date::Parse> for conversion functions.
839
840 =cut
841
842 #still some false laziness w/print_text
843 sub print_text {
844
845   my( $self, $today, $template ) = @_;
846   $today ||= time;
847
848 #  my $invnum = $self->invnum;
849   my $cust_main = $self->cust_main;
850   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
851     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
852
853   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
854 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
855   #my $balance_due = $self->owed + $pr_total - $cr_total;
856   my $balance_due = $self->owed + $pr_total;
857
858   #my @collect = ();
859   #my($description,$amount);
860   @buf = ();
861
862   #previous balance
863   foreach ( @pr_cust_bill ) {
864     push @buf, [
865       "Previous Balance, Invoice #". $_->invnum. 
866                  " (". time2str("%x",$_->_date). ")",
867       $money_char. sprintf("%10.2f",$_->owed)
868     ];
869   }
870   if (@pr_cust_bill) {
871     push @buf,['','-----------'];
872     push @buf,[ 'Total Previous Balance',
873                 $money_char. sprintf("%10.2f",$pr_total ) ];
874     push @buf,['',''];
875   }
876
877   #new charges
878   foreach my $cust_bill_pkg (
879     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
880     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
881   ) {
882
883     if ( $cust_bill_pkg->pkgnum ) {
884
885       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
886       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
887       my $pkg = $part_pkg->pkg;
888
889       if ( $cust_bill_pkg->setup != 0 ) {
890         my $description = $pkg;
891         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
892         push @buf, [ $description,
893                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
894         push @buf,
895           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
896               $cust_pkg->h_labels($self->_date);
897       }
898
899       if ( $cust_bill_pkg->recur != 0 ) {
900         push @buf, [
901           "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
902                                 time2str("%x", $cust_bill_pkg->edate) . ")",
903           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
904         ];
905         push @buf,
906           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
907               $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
908       }
909
910       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
911
912     } else { #pkgnum tax or one-shot line item
913       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
914                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
915                      : 'Tax';
916       if ( $cust_bill_pkg->setup != 0 ) {
917         push @buf, [ $itemdesc,
918                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
919       }
920       if ( $cust_bill_pkg->recur != 0 ) {
921         push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
922                                   . time2str("%x", $cust_bill_pkg->edate). ")",
923                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
924                    ];
925       }
926     }
927   }
928
929   push @buf,['','-----------'];
930   push @buf,['Total New Charges',
931              $money_char. sprintf("%10.2f",$self->charged) ];
932   push @buf,['',''];
933
934   push @buf,['','-----------'];
935   push @buf,['Total Charges',
936              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
937   push @buf,['',''];
938
939   #credits
940   foreach ( $self->cust_credited ) {
941
942     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
943
944     my $reason = substr($_->cust_credit->reason,0,32);
945     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
946     $reason = " ($reason) " if $reason;
947     push @buf,[
948       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
949         $reason,
950       $money_char. sprintf("%10.2f",$_->amount)
951     ];
952   }
953   #foreach ( @cr_cust_credit ) {
954   #  push @buf,[
955   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
956   #    $money_char. sprintf("%10.2f",$_->credited)
957   #  ];
958   #}
959
960   #get & print payments
961   foreach ( $self->cust_bill_pay ) {
962
963     #something more elaborate if $_->amount ne ->cust_pay->paid ?
964
965     push @buf,[
966       "Payment received ". time2str("%x",$_->cust_pay->_date ),
967       $money_char. sprintf("%10.2f",$_->amount )
968     ];
969   }
970
971   #balance due
972   my $balance_due_msg = $self->balance_due_msg;
973
974   push @buf,['','-----------'];
975   push @buf,[$balance_due_msg, $money_char. 
976     sprintf("%10.2f", $balance_due ) ];
977
978   #create the template
979   $template ||= $self->_agent_template;
980   my $templatefile = 'invoice_template';
981   $templatefile .= "_$template" if length($template);
982   my @invoice_template = $conf->config($templatefile)
983     or die "cannot load config file $templatefile";
984   $invoice_lines = 0;
985   my $wasfunc = 0;
986   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
987     /invoice_lines\((\d*)\)/;
988     $invoice_lines += $1 || scalar(@buf);
989     $wasfunc=1;
990   }
991   die "no invoice_lines() functions in template?" unless $wasfunc;
992   my $invoice_template = new Text::Template (
993     TYPE   => 'ARRAY',
994     SOURCE => [ map "$_\n", @invoice_template ],
995   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
996   $invoice_template->compile()
997     or die "can't compile template: $Text::Template::ERROR";
998
999   #setup template variables
1000   package FS::cust_bill::_template; #!
1001   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1002
1003   $invnum = $self->invnum;
1004   $date = $self->_date;
1005   $page = 1;
1006   $agent = $self->cust_main->agent->agent;
1007
1008   if ( $FS::cust_bill::invoice_lines ) {
1009     $total_pages =
1010       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1011     $total_pages++
1012       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1013   } else {
1014     $total_pages = 1;
1015   }
1016
1017   #format address (variable for the template)
1018   my $l = 0;
1019   @address = ( '', '', '', '', '', '' );
1020   package FS::cust_bill; #!
1021   $FS::cust_bill::_template::address[$l++] =
1022     $cust_main->payname.
1023       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1024         ? " (P.O. #". $cust_main->payinfo. ")"
1025         : ''
1026       )
1027   ;
1028   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1029     if $cust_main->company;
1030   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1031   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1032     if $cust_main->address2;
1033   $FS::cust_bill::_template::address[$l++] =
1034     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1035
1036   my $countrydefault = $conf->config('countrydefault') || 'US';
1037   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1038     unless $cust_main->country eq $countrydefault;
1039
1040         #  #overdue? (variable for the template)
1041         #  $FS::cust_bill::_template::overdue = ( 
1042         #    $balance_due > 0
1043         #    && $today > $self->_date 
1044         ##    && $self->printed > 1
1045         #    && $self->printed > 0
1046         #  );
1047
1048   #and subroutine for the template
1049   sub FS::cust_bill::_template::invoice_lines {
1050     my $lines = shift || scalar(@buf);
1051     map { 
1052       scalar(@buf) ? shift @buf : [ '', '' ];
1053     }
1054     ( 1 .. $lines );
1055   }
1056
1057   #and fill it in
1058   $FS::cust_bill::_template::page = 1;
1059   my $lines;
1060   my @collect;
1061   while (@buf) {
1062     push @collect, split("\n",
1063       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1064     );
1065     $FS::cust_bill::_template::page++;
1066   }
1067
1068   map "$_\n", @collect;
1069
1070 }
1071
1072 =item print_latex [ TIME [ , TEMPLATE ] ]
1073
1074 Internal method - returns a filename of a filled-in LaTeX template for this
1075 invoice (Note: add ".tex" to get the actual filename).
1076
1077 See print_ps and print_pdf for methods that return PostScript and PDF output.
1078
1079 TIME an optional value used to control the printing of overdue messages.  The
1080 default is now.  It isn't the date of the invoice; that's the `_date' field.
1081 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1082 L<Time::Local> and L<Date::Parse> for conversion functions.
1083
1084 =cut
1085
1086 #still some false laziness w/print_text
1087 sub print_latex {
1088
1089   my( $self, $today, $template ) = @_;
1090   $today ||= time;
1091   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1092     if $DEBUG;
1093
1094   my $cust_main = $self->cust_main;
1095   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1096     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1097
1098   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1099 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1100   #my $balance_due = $self->owed + $pr_total - $cr_total;
1101   my $balance_due = $self->owed + $pr_total;
1102
1103   #create the template
1104   $template ||= $self->_agent_template;
1105   my $templatefile = 'invoice_latex';
1106   my $suffix = length($template) ? "_$template" : '';
1107   $templatefile .= $suffix;
1108   my @invoice_template = map "$_\n", $conf->config($templatefile)
1109     or die "cannot load config file $templatefile";
1110
1111   my($format, $text_template);
1112   if ( grep { /^%%Detail/ } @invoice_template ) {
1113     #change this to a die when the old code is removed
1114     warn "old-style invoice template $templatefile; ".
1115          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1116     $format = 'old';
1117   } else {
1118     $format = 'Text::Template';
1119     $text_template = new Text::Template(
1120       TYPE => 'ARRAY',
1121       SOURCE => \@invoice_template,
1122       DELIMITERS => [ '[@--', '--@]' ],
1123     );
1124
1125     $text_template->compile()
1126       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1127   }
1128
1129   my $returnaddress;
1130   if ( $conf->exists('invoice_latexreturnaddress')
1131        && length($conf->exists('invoice_latexreturnaddress'))
1132      )
1133   {
1134     $returnaddress = join("\n", $conf->config('invoice_latexreturnaddress') );
1135   } else {
1136     $returnaddress = '~';
1137   }
1138
1139   my %invoice_data = (
1140     'invnum'       => $self->invnum,
1141     'date'         => time2str('%b %o, %Y', $self->_date),
1142     'today'        => time2str('%b %o, %Y', $today),
1143     'agent'        => _latex_escape($cust_main->agent->agent),
1144     'payname'      => _latex_escape($cust_main->payname),
1145     'company'      => _latex_escape($cust_main->company),
1146     'address1'     => _latex_escape($cust_main->address1),
1147     'address2'     => _latex_escape($cust_main->address2),
1148     'city'         => _latex_escape($cust_main->city),
1149     'state'        => _latex_escape($cust_main->state),
1150     'zip'          => _latex_escape($cust_main->zip),
1151     'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
1152     'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
1153     'returnaddress' => $returnaddress,
1154     'quantity'     => 1,
1155     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1156     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1157     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1158   );
1159
1160   my $countrydefault = $conf->config('countrydefault') || 'US';
1161   if ( $cust_main->country eq $countrydefault ) {
1162     $invoice_data{'country'} = '';
1163   } else {
1164     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1165   }
1166
1167   #do variable substitutions in notes
1168   $invoice_data{'notes'} =
1169     join("\n",
1170       map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1171         $conf->config_orbase('invoice_latexnotes', $template)
1172     );
1173   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1174     if $DEBUG;
1175
1176   $invoice_data{'footer'} =~ s/\n+$//;
1177   $invoice_data{'smallfooter'} =~ s/\n+$//;
1178   $invoice_data{'notes'} =~ s/\n+$//;
1179
1180   $invoice_data{'po_line'} =
1181     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1182       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1183       : '~';
1184
1185   my @filled_in = ();
1186   if ( $format eq 'old' ) {
1187   
1188     my @line_item = ();
1189     my @total_item = ();
1190     while ( @invoice_template ) {
1191       my $line = shift @invoice_template;
1192   
1193       if ( $line =~ /^%%Detail\s*$/ ) {
1194   
1195         while ( ( my $line_item_line = shift @invoice_template )
1196                 !~ /^%%EndDetail\s*$/                            ) {
1197           push @line_item, $line_item_line;
1198         }
1199         foreach my $line_item ( $self->_items ) {
1200         #foreach my $line_item ( $self->_items_pkg ) {
1201           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1202           $invoice_data{'description'} =
1203             _latex_escape($line_item->{'description'});
1204           if ( exists $line_item->{'ext_description'} ) {
1205             $invoice_data{'description'} .=
1206               "\\tabularnewline\n~~".
1207               join( "\\tabularnewline\n~~",
1208                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1209                   );
1210           }
1211           $invoice_data{'amount'} = $line_item->{'amount'};
1212           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1213           push @filled_in,
1214             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1215         }
1216   
1217       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1218   
1219         while ( ( my $total_item_line = shift @invoice_template )
1220                 !~ /^%%EndTotalDetails\s*$/                      ) {
1221           push @total_item, $total_item_line;
1222         }
1223   
1224         my @total_fill = ();
1225   
1226         my $taxtotal = 0;
1227         foreach my $tax ( $self->_items_tax ) {
1228           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1229           $taxtotal += $tax->{'amount'};
1230           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1231           push @total_fill,
1232             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1233                 @total_item;
1234         }
1235
1236         if ( $taxtotal ) {
1237           $invoice_data{'total_item'} = 'Sub-total';
1238           $invoice_data{'total_amount'} =
1239             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1240           unshift @total_fill,
1241             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1242                 @total_item;
1243         }
1244   
1245         $invoice_data{'total_item'} = '\textbf{Total}';
1246         $invoice_data{'total_amount'} =
1247           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1248         push @total_fill,
1249           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1250               @total_item;
1251   
1252         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1253   
1254         # credits
1255         foreach my $credit ( $self->_items_credits ) {
1256           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1257           #$credittotal
1258           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1259           push @total_fill, 
1260             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1261                 @total_item;
1262         }
1263   
1264         # payments
1265         foreach my $payment ( $self->_items_payments ) {
1266           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1267           #$paymenttotal
1268           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1269           push @total_fill, 
1270             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1271                 @total_item;
1272         }
1273   
1274         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1275         $invoice_data{'total_amount'} =
1276           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1277         push @total_fill,
1278           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1279               @total_item;
1280   
1281         push @filled_in, @total_fill;
1282   
1283       } else {
1284         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1285         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1286         push @filled_in, $line;
1287       }
1288   
1289     }
1290
1291     sub nounder {
1292       my $var = $1;
1293       $var =~ s/_/\-/g;
1294       $var;
1295     }
1296
1297   } elsif ( $format eq 'Text::Template' ) {
1298
1299     my @detail_items = ();
1300     my @total_items = ();
1301
1302     $invoice_data{'detail_items'} = \@detail_items;
1303     $invoice_data{'total_items'} = \@total_items;
1304   
1305     foreach my $line_item ( $self->_items ) {
1306       my $detail = {
1307         ext_description => [],
1308       };
1309       $detail->{'ref'} = $line_item->{'pkgnum'};
1310       $detail->{'quantity'} = 1;
1311       $detail->{'description'} = _latex_escape($line_item->{'description'});
1312       if ( exists $line_item->{'ext_description'} ) {
1313         @{$detail->{'ext_description'}} = map {
1314           _latex_escape($_);
1315         } @{$line_item->{'ext_description'}};
1316       }
1317       $detail->{'amount'} = $line_item->{'amount'};
1318       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1319   
1320       push @detail_items, $detail;
1321     }
1322   
1323   
1324     my $taxtotal = 0;
1325     foreach my $tax ( $self->_items_tax ) {
1326       my $total = {};
1327       $total->{'total_item'} = _latex_escape($tax->{'description'});
1328       $taxtotal += $tax->{'amount'};
1329       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1330       push @total_items, $total;
1331     }
1332   
1333     if ( $taxtotal ) {
1334       my $total = {};
1335       $total->{'total_item'} = 'Sub-total';
1336       $total->{'total_amount'} =
1337         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1338       unshift @total_items, $total;
1339     }
1340   
1341     {
1342       my $total = {};
1343       $total->{'total_item'} = '\textbf{Total}';
1344       $total->{'total_amount'} =
1345         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1346       push @total_items, $total;
1347     }
1348   
1349     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1350   
1351     # credits
1352     foreach my $credit ( $self->_items_credits ) {
1353       my $total;
1354       $total->{'total_item'} = _latex_escape($credit->{'description'});
1355       #$credittotal
1356       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1357       push @total_items, $total;
1358     }
1359   
1360     # payments
1361     foreach my $payment ( $self->_items_payments ) {
1362       my $total = {};
1363       $total->{'total_item'} = _latex_escape($payment->{'description'});
1364       #$paymenttotal
1365       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1366       push @total_items, $total;
1367     }
1368   
1369     { 
1370       my $total;
1371       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1372       $total->{'total_amount'} =
1373         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1374       push @total_items, $total;
1375     }
1376
1377   } else {
1378     die "guru meditation #54";
1379   }
1380
1381   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1382   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1383                            DIR      => $dir,
1384                            SUFFIX   => '.tex',
1385                            UNLINK   => 0,
1386                          ) or die "can't open temp file: $!\n";
1387   if ( $format eq 'old' ) {
1388     print $fh join('', @filled_in );
1389   } elsif ( $format eq 'Text::Template' ) {
1390     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1391   } else {
1392     die "guru meditation #32";
1393   }
1394   close $fh;
1395
1396   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1397   return $1;
1398
1399 }
1400
1401 =item print_ps [ TIME [ , TEMPLATE ] ]
1402
1403 Returns an postscript invoice, as a scalar.
1404
1405 TIME an optional value used to control the printing of overdue messages.  The
1406 default is now.  It isn't the date of the invoice; that's the `_date' field.
1407 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1408 L<Time::Local> and L<Date::Parse> for conversion functions.
1409
1410 =cut
1411
1412 sub print_ps {
1413   my $self = shift;
1414
1415   my $file = $self->print_latex(@_);
1416
1417   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1418   chdir($dir);
1419
1420   my $sfile = shell_quote $file;
1421
1422   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1423     or die "pslatex $file.tex failed; see $file.log for details?\n";
1424   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1425     or die "pslatex $file.tex failed; see $file.log for details?\n";
1426
1427   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1428     or die "dvips failed";
1429
1430   open(POSTSCRIPT, "<$file.ps")
1431     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1432
1433   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1434
1435   my $ps = '';
1436   while (<POSTSCRIPT>) {
1437     $ps .= $_;
1438   }
1439
1440   close POSTSCRIPT;
1441
1442   return $ps;
1443
1444 }
1445
1446 =item print_pdf [ TIME [ , TEMPLATE ] ]
1447
1448 Returns an PDF invoice, as a scalar.
1449
1450 TIME an optional value used to control the printing of overdue messages.  The
1451 default is now.  It isn't the date of the invoice; that's the `_date' field.
1452 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1453 L<Time::Local> and L<Date::Parse> for conversion functions.
1454
1455 =cut
1456
1457 sub print_pdf {
1458   my $self = shift;
1459
1460   my $file = $self->print_latex(@_);
1461
1462   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1463   chdir($dir);
1464
1465   #system('pdflatex', "$file.tex");
1466   #system('pdflatex', "$file.tex");
1467   #! LaTeX Error: Unknown graphics extension: .eps.
1468
1469   my $sfile = shell_quote $file;
1470
1471   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1472     or die "pslatex $file.tex failed; see $file.log for details?\n";
1473   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1474     or die "pslatex $file.tex failed; see $file.log for details?\n";
1475
1476   #system('dvipdf', "$file.dvi", "$file.pdf" );
1477   system(
1478     "dvips -q -t letter -f $sfile.dvi ".
1479     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1480     "     -c save pop -"
1481   ) == 0
1482     or die "dvips | gs failed: $!";
1483
1484   open(PDF, "<$file.pdf")
1485     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1486
1487   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1488
1489   my $pdf = '';
1490   while (<PDF>) {
1491     $pdf .= $_;
1492   }
1493
1494   close PDF;
1495
1496   return $pdf;
1497
1498 }
1499
1500 =item print_html [ TIME [ , TEMPLATE ] ]
1501
1502 Returns an HTML invoice, as a scalar.
1503
1504 TIME an optional value used to control the printing of overdue messages.  The
1505 default is now.  It isn't the date of the invoice; that's the `_date' field.
1506 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1507 L<Time::Local> and L<Date::Parse> for conversion functions.
1508
1509 =cut
1510
1511 #sub print_html {
1512 #  my $self = shift;
1513 #
1514 #  my $file = $self->print_latex(@_);
1515 #
1516 #  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1517 #  chdir($dir);
1518 #
1519 #  my $sfile = shell_quote $file;
1520 #
1521 #  system("htlatex $sfile.tex") == 0
1522 #    or die "hlatex $file.tex failed; is hlatex installed, or see $file.log for details?\n";
1523 #  #system("ltoh $sfile.tex") == 0
1524 #  #  or die "ltoh $file.tex failed; is hlatex installed, or see $file.log for details?\n";
1525 #
1526 #  open(HTML, "<$file.html")
1527 #    or die "can't open $file.html: $! (error in LaTeX template?)\n";
1528 #
1529 #  #unlink("$file.dvi", "$file.log", "$file.aux", "$file.html", "$file.tex");
1530 #
1531 #  my $html = '';
1532 #  while (<HTML>) {
1533 #
1534 #    s/<link\s+rel="stylesheet"\s+type="text\/css"\s+href="invoice\.(\d+)\.(\w+)\.css">/<link rel="stylesheet" type="text\/css" href="cust_bill.html?$1.$2.css">/;
1535 ##    s/<link\s+//;
1536 #    $html .= $_;
1537 #  }
1538 #
1539 #  close HTML;
1540 #
1541 #  return $html;
1542 #
1543 #}
1544 #
1545 ##inefficient proof-of-concept for now
1546 #sub print_html_css {
1547 #  my $self = shift;
1548 #
1549 #  my $file = $self->print_latex(@_);
1550 #
1551 #  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1552 #  chdir($dir);
1553 #
1554 #  my $sfile = shell_quote $file;
1555 #
1556 #  system("htlatex $sfile.tex") == 0
1557 #    or die "hlatex $file.tex failed; is hlatex installed, or see $file.log for details?\n";
1558 #  #system("ltoh $sfile.tex") == 0
1559 #  #  or die "ltoh $file.tex failed; is hlatex installed, or see $file.log for details?\n";
1560 #
1561 #  open(CSS, "<$file.css")
1562 #    or die "can't open $file.html: $! (error in LaTeX template?)\n";
1563 #
1564 #  unlink("$file.dvi", "$file.log", "$file.aux", "$file.html", "$file.tex");
1565 #
1566 #  my $css = '';
1567 #  while (<CSS>) {
1568 #    $css .= $_;
1569 #  }
1570 #
1571 #  close CSS;
1572 #
1573 #  return $css;
1574 #
1575 #}
1576
1577 sub print_html {
1578   my( $self, $today, $template ) = @_;
1579   $today ||= time;
1580
1581   my $cust_main = $self->cust_main;
1582   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1583     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1584
1585   $template ||= $self->_agent_template;
1586   my $templatefile = 'invoice_html';
1587   my $suffix = length($template) ? "_$template" : '';
1588   $templatefile .= $suffix;
1589   my @html_template = map "$_\n", $conf->config($templatefile)
1590     or die "cannot load config file $templatefile";
1591
1592   my $html_template = new Text::Template(
1593     TYPE   => 'ARRAY',
1594     SOURCE => \@html_template,
1595     DELIMITERS => [ '<%=', '%>' ],
1596   );
1597
1598   $html_template->compile()
1599     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1600
1601   my $returnaddress = $conf->exists('invoice_htmlreturnaddress')
1602     ? join("\n", $conf->config('invoice_htmlreturnaddress') )
1603     : join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; }
1604                      $conf->config('invoice_latexreturnaddress')
1605           );
1606   warn $conf->config('invoice_latexreturnaddress');
1607   warn $returnaddress;
1608
1609   my %invoice_data = (
1610     'invnum'       => $self->invnum,
1611     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
1612     'agent'        => encode_entities($cust_main->agent->agent),
1613     'payname'      => encode_entities($cust_main->payname),
1614     'company'      => encode_entities($cust_main->company),
1615     'address1'     => encode_entities($cust_main->address1),
1616     'address2'     => encode_entities($cust_main->address2),
1617     'city'         => encode_entities($cust_main->city),
1618     'state'        => encode_entities($cust_main->state),
1619     'zip'          => encode_entities($cust_main->zip),
1620 #    'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
1621 #    'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
1622     'returnaddress' => $returnaddress,
1623     'terms'        => $conf->config('invoice_default_terms')
1624                       || 'Payable upon receipt',
1625     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1626 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1627   );
1628
1629   my $countrydefault = $conf->config('countrydefault') || 'US';
1630   if ( $cust_main->country eq $countrydefault ) {
1631     $invoice_data{'country'} = '';
1632   } else {
1633     $invoice_data{'country'} =
1634       encode_entities(code2country($cust_main->country));
1635   }
1636
1637   my $countrydefault = $conf->config('countrydefault') || 'US';
1638   $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1639
1640 #  #do variable substitutions in notes
1641 #  $invoice_data{'notes'} =
1642 #    join("\n",
1643 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1644 #        $conf->config_orbase('invoice_latexnotes', $suffix)
1645 #    );
1646 #
1647 #  $invoice_data{'footer'} =~ s/\n+$//;
1648 #  $invoice_data{'smallfooter'} =~ s/\n+$//;
1649 #  $invoice_data{'notes'} =~ s/\n+$//;
1650 #
1651   $invoice_data{'po_line'} =
1652     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1653       ? encode_entities("Purchase Order #". $cust_main->payinfo)
1654       : '';
1655
1656   my $money_char = $conf->config('money_char') || '$';
1657
1658   foreach my $line_item ( $self->_items ) {
1659     my $detail = {
1660       ext_description => [],
1661     };
1662     $detail->{'ref'} = $line_item->{'pkgnum'};
1663     $detail->{'description'} = encode_entities($line_item->{'description'});
1664     if ( exists $line_item->{'ext_description'} ) {
1665       @{$detail->{'ext_description'}} = map {
1666         encode_entities($_);
1667       } @{$line_item->{'ext_description'}};
1668     }
1669     $detail->{'amount'} = $money_char. $line_item->{'amount'};
1670     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1671
1672     push @{$invoice_data{'detail_items'}}, $detail;
1673   }
1674
1675
1676   my $taxtotal = 0;
1677   foreach my $tax ( $self->_items_tax ) {
1678     my $total = {};
1679     $total->{'total_item'} = encode_entities($tax->{'description'});
1680     $taxtotal += $tax->{'amount'};
1681     $total->{'total_amount'} = $money_char. $tax->{'amount'};
1682     push @{$invoice_data{'total_items'}}, $total;
1683   }
1684
1685   if ( $taxtotal ) {
1686     my $total = {};
1687     $total->{'total_item'} = 'Sub-total';
1688     $total->{'total_amount'} =
1689       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1690     unshift @{$invoice_data{'total_items'}}, $total;
1691   }
1692
1693   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1694   {
1695     my $total = {};
1696     $total->{'total_item'} = '<b>Total</b>';
1697     $total->{'total_amount'} =
1698       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1699     push @{$invoice_data{'total_items'}}, $total;
1700   }
1701
1702   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1703
1704   # credits
1705   foreach my $credit ( $self->_items_credits ) {
1706     my $total;
1707     $total->{'total_item'} = encode_entities($credit->{'description'});
1708     #$credittotal
1709     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1710     push @{$invoice_data{'total_items'}}, $total;
1711   }
1712
1713   # payments
1714   foreach my $payment ( $self->_items_payments ) {
1715     my $total = {};
1716     $total->{'total_item'} = encode_entities($payment->{'description'});
1717     #$paymenttotal
1718     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1719     push @{$invoice_data{'total_items'}}, $total;
1720   }
1721
1722   { 
1723     my $total;
1724     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1725     $total->{'total_amount'} =
1726       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1727     push @{$invoice_data{'total_items'}}, $total;
1728   }
1729
1730   $html_template->fill_in( HASH => \%invoice_data);
1731 }
1732
1733 # quick subroutine for print_latex
1734 #
1735 # There are ten characters that LaTeX treats as special characters, which
1736 # means that they do not simply typeset themselves: 
1737 #      # $ % & ~ _ ^ \ { }
1738 #
1739 # TeX ignores blanks following an escaped character; if you want a blank (as
1740 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1741
1742 sub _latex_escape {
1743   my $value = shift;
1744   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1745   $value =~ s/([<>])/\$$1\$/g;
1746   $value;
1747 }
1748
1749 #utility methods for print_*
1750
1751 sub balance_due_msg {
1752   my $self = shift;
1753   my $msg = 'Balance Due';
1754   return $msg unless $conf->exists('invoice_default_terms');
1755   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1756     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1757   } elsif ( $conf->config('invoice_default_terms') ) {
1758     $msg .= ' - '. $conf->config('invoice_default_terms');
1759   }
1760   $msg;
1761 }
1762
1763 sub _items {
1764   my $self = shift;
1765   my @display = scalar(@_)
1766                 ? @_
1767                 : qw( _items_previous _items_pkg );
1768                 #: qw( _items_pkg );
1769                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1770   my @b = ();
1771   foreach my $display ( @display ) {
1772     push @b, $self->$display(@_);
1773   }
1774   @b;
1775 }
1776
1777 sub _items_previous {
1778   my $self = shift;
1779   my $cust_main = $self->cust_main;
1780   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1781   my @b = ();
1782   foreach ( @pr_cust_bill ) {
1783     push @b, {
1784       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
1785                        ' ('. time2str('%x',$_->_date). ')',
1786       #'pkgpart'     => 'N/A',
1787       'pkgnum'      => 'N/A',
1788       'amount'      => sprintf("%.2f", $_->owed),
1789     };
1790   }
1791   @b;
1792
1793   #{
1794   #    'description'     => 'Previous Balance',
1795   #    #'pkgpart'         => 'N/A',
1796   #    'pkgnum'          => 'N/A',
1797   #    'amount'          => sprintf("%10.2f", $pr_total ),
1798   #    'ext_description' => [ map {
1799   #                                 "Invoice ". $_->invnum.
1800   #                                 " (". time2str("%x",$_->_date). ") ".
1801   #                                 sprintf("%10.2f", $_->owed)
1802   #                         } @pr_cust_bill ],
1803
1804   #};
1805 }
1806
1807 sub _items_pkg {
1808   my $self = shift;
1809   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1810   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1811 }
1812
1813 sub _items_tax {
1814   my $self = shift;
1815   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1816   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1817 }
1818
1819 sub _items_cust_bill_pkg {
1820   my $self = shift;
1821   my $cust_bill_pkg = shift;
1822
1823   my @b = ();
1824   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1825
1826     if ( $cust_bill_pkg->pkgnum ) {
1827
1828       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1829       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1830       my $pkg = $part_pkg->pkg;
1831
1832       if ( $cust_bill_pkg->setup != 0 ) {
1833         my $description = $pkg;
1834         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1835         my @d = $cust_pkg->h_labels_short($self->_date);
1836         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1837         push @b, {
1838           description     => $description,
1839           #pkgpart         => $part_pkg->pkgpart,
1840           pkgnum          => $cust_pkg->pkgnum,
1841           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
1842           ext_description => \@d,
1843         };
1844       }
1845
1846       if ( $cust_bill_pkg->recur != 0 ) {
1847         push @b, {
1848           description     => "$pkg (" .
1849                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
1850                                time2str('%x', $cust_bill_pkg->edate). ')',
1851           #pkgpart         => $part_pkg->pkgpart,
1852           pkgnum          => $cust_pkg->pkgnum,
1853           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
1854           ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1855                                                          $cust_bill_pkg->sdate),
1856                                $cust_bill_pkg->details,
1857                              ],
1858         };
1859       }
1860
1861     } else { #pkgnum tax or one-shot line item (??)
1862
1863       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1864                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1865                      : 'Tax';
1866       if ( $cust_bill_pkg->setup != 0 ) {
1867         push @b, {
1868           'description' => $itemdesc,
1869           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
1870         };
1871       }
1872       if ( $cust_bill_pkg->recur != 0 ) {
1873         push @b, {
1874           'description' => "$itemdesc (".
1875                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
1876                            time2str("%x", $cust_bill_pkg->edate). ')',
1877           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
1878         };
1879       }
1880
1881     }
1882
1883   }
1884
1885   @b;
1886
1887 }
1888
1889 sub _items_credits {
1890   my $self = shift;
1891
1892   my @b;
1893   #credits
1894   foreach ( $self->cust_credited ) {
1895
1896     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1897
1898     my $reason = $_->cust_credit->reason;
1899     #my $reason = substr($_->cust_credit->reason,0,32);
1900     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1901     $reason = " ($reason) " if $reason;
1902     push @b, {
1903       #'description' => 'Credit ref\#'. $_->crednum.
1904       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
1905       #                 $reason,
1906       'description' => 'Credit applied '.
1907                        time2str("%x",$_->cust_credit->_date). $reason,
1908       'amount'      => sprintf("%.2f",$_->amount),
1909     };
1910   }
1911   #foreach ( @cr_cust_credit ) {
1912   #  push @buf,[
1913   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1914   #    $money_char. sprintf("%10.2f",$_->credited)
1915   #  ];
1916   #}
1917
1918   @b;
1919
1920 }
1921
1922 sub _items_payments {
1923   my $self = shift;
1924
1925   my @b;
1926   #get & print payments
1927   foreach ( $self->cust_bill_pay ) {
1928
1929     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1930
1931     push @b, {
1932       'description' => "Payment received ".
1933                        time2str("%x",$_->cust_pay->_date ),
1934       'amount'      => sprintf("%.2f", $_->amount )
1935     };
1936   }
1937
1938   @b;
1939
1940 }
1941
1942 =back
1943
1944 =head1 BUGS
1945
1946 The delete method.
1947
1948 print_text formatting (and some logic :/) is in source, but needs to be
1949 slurped in from a file.  Also number of lines ($=).
1950
1951 =head1 SEE ALSO
1952
1953 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1954 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
1955 documentation.
1956
1957 =cut
1958
1959 1;
1960