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