use new pkg_svc.pkgsvcnum primary key when modifying pkg_svc records, closes: Bug...
[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 or use new conf/invoice_latex*\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     'returnaddress' => join("\n", $conf->config('invoice_latexreturnaddress') ),
1141     'quantity'     => 1,
1142     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1143     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1144     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1145   );
1146
1147   my $countrydefault = $conf->config('countrydefault') || 'US';
1148   $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1149
1150   #do variable substitutions in notes
1151   $invoice_data{'notes'} =
1152     join("\n",
1153       map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1154         $conf->config_orbase('invoice_latexnotes', $suffix)
1155     );
1156
1157   $invoice_data{'footer'} =~ s/\n+$//;
1158   $invoice_data{'smallfooter'} =~ s/\n+$//;
1159   $invoice_data{'notes'} =~ s/\n+$//;
1160
1161   $invoice_data{'po_line'} =
1162     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1163       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1164       : '~';
1165
1166   my @filled_in = ();
1167   if ( $format eq 'old' ) {
1168   
1169     my @line_item = ();
1170     my @total_item = ();
1171     while ( @invoice_template ) {
1172       my $line = shift @invoice_template;
1173   
1174       if ( $line =~ /^%%Detail\s*$/ ) {
1175   
1176         while ( ( my $line_item_line = shift @invoice_template )
1177                 !~ /^%%EndDetail\s*$/                            ) {
1178           push @line_item, $line_item_line;
1179         }
1180         foreach my $line_item ( $self->_items ) {
1181         #foreach my $line_item ( $self->_items_pkg ) {
1182           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1183           $invoice_data{'description'} =
1184             _latex_escape($line_item->{'description'});
1185           if ( exists $line_item->{'ext_description'} ) {
1186             $invoice_data{'description'} .=
1187               "\\tabularnewline\n~~".
1188               join( "\\tabularnewline\n~~",
1189                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1190                   );
1191           }
1192           $invoice_data{'amount'} = $line_item->{'amount'};
1193           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1194           push @filled_in,
1195             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1196         }
1197   
1198       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1199   
1200         while ( ( my $total_item_line = shift @invoice_template )
1201                 !~ /^%%EndTotalDetails\s*$/                      ) {
1202           push @total_item, $total_item_line;
1203         }
1204   
1205         my @total_fill = ();
1206   
1207         my $taxtotal = 0;
1208         foreach my $tax ( $self->_items_tax ) {
1209           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1210           $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1211           push @total_fill,
1212             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1213                 @total_item;
1214         }
1215   
1216         if ( $taxtotal ) {
1217           $invoice_data{'total_item'} = 'Sub-total';
1218           $invoice_data{'total_amount'} =
1219             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1220           unshift @total_fill,
1221             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1222                 @total_item;
1223         }
1224   
1225         $invoice_data{'total_item'} = '\textbf{Total}';
1226         $invoice_data{'total_amount'} =
1227           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1228         push @total_fill,
1229           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1230               @total_item;
1231   
1232         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1233   
1234         # credits
1235         foreach my $credit ( $self->_items_credits ) {
1236           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1237           #$credittotal
1238           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1239           push @total_fill, 
1240             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1241                 @total_item;
1242         }
1243   
1244         # payments
1245         foreach my $payment ( $self->_items_payments ) {
1246           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1247           #$paymenttotal
1248           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1249           push @total_fill, 
1250             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1251                 @total_item;
1252         }
1253   
1254         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1255         $invoice_data{'total_amount'} =
1256           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1257         push @total_fill,
1258           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1259               @total_item;
1260   
1261         push @filled_in, @total_fill;
1262   
1263       } else {
1264         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1265         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1266         push @filled_in, $line;
1267       }
1268   
1269     }
1270
1271     sub nounder {
1272       my $var = $1;
1273       $var =~ s/_/\-/g;
1274       $var;
1275     }
1276
1277   } elsif ( $format eq 'Text::Template' ) {
1278
1279     my @detail_items = ();
1280     my @total_items = ();
1281
1282     $invoice_data{'detail_items'} = \@detail_items;
1283     $invoice_data{'total_items'} = \@total_items;
1284   
1285     foreach my $line_item ( $self->_items ) {
1286       my $detail = {
1287         ext_description => [],
1288       };
1289       $detail->{'ref'} = $line_item->{'pkgnum'};
1290       $detail->{'quantity'} = 1;
1291       $detail->{'description'} = _latex_escape($line_item->{'description'});
1292       if ( exists $line_item->{'ext_description'} ) {
1293         @{$detail->{'ext_description'}} = map {
1294           _latex_escape($_);
1295         } @{$line_item->{'ext_description'}};
1296       }
1297       $detail->{'amount'} = $line_item->{'amount'};
1298       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1299   
1300       push @detail_items, $detail;
1301     }
1302   
1303   
1304     my $taxtotal = 0;
1305     foreach my $tax ( $self->_items_tax ) {
1306       my $total = {};
1307       $total->{'total_item'} = _latex_escape($tax->{'description'});
1308       $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1309       push @total_items, $total;
1310     }
1311   
1312     if ( $taxtotal ) {
1313       my $total = {};
1314       $total->{'total_item'} = 'Sub-total';
1315       $total->{'total_amount'} =
1316         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1317       unshift @total_items, $total;
1318     }
1319   
1320     {
1321       my $total = {};
1322       $total->{'total_item'} = '\textbf{Total}';
1323       $total->{'total_amount'} =
1324         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1325       push @total_items, $total;
1326     }
1327   
1328     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1329   
1330     # credits
1331     foreach my $credit ( $self->_items_credits ) {
1332       my $total;
1333       $total->{'total_item'} = _latex_escape($credit->{'description'});
1334       #$credittotal
1335       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1336       push @total_items, $total;
1337     }
1338   
1339     # payments
1340     foreach my $payment ( $self->_items_payments ) {
1341       my $total = {};
1342       $total->{'total_item'} = _latex_escape($payment->{'description'});
1343       #$paymenttotal
1344       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1345       push @total_items, $total;
1346     }
1347   
1348     { 
1349       my $total;
1350       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1351       $total->{'total_amount'} =
1352         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1353       push @total_items, $total;
1354     }
1355
1356   } else {
1357     die "guru meditation #54";
1358   }
1359
1360   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1361   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1362                            DIR      => $dir,
1363                            SUFFIX   => '.tex',
1364                            UNLINK   => 0,
1365                          ) or die "can't open temp file: $!\n";
1366   if ( $format eq 'old' ) {
1367     print $fh join('', @filled_in );
1368   } elsif ( $format eq 'Text::Template' ) {
1369     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1370   } else {
1371     die "guru meditation #32";
1372   }
1373   close $fh;
1374
1375   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1376   return $1;
1377
1378 }
1379
1380 =item print_ps [ TIME [ , TEMPLATE ] ]
1381
1382 Returns an postscript invoice, as a scalar.
1383
1384 TIME an optional value used to control the printing of overdue messages.  The
1385 default is now.  It isn't the date of the invoice; that's the `_date' field.
1386 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1387 L<Time::Local> and L<Date::Parse> for conversion functions.
1388
1389 =cut
1390
1391 sub print_ps {
1392   my $self = shift;
1393
1394   my $file = $self->print_latex(@_);
1395
1396   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1397   chdir($dir);
1398
1399   my $sfile = shell_quote $file;
1400
1401   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1402     or die "pslatex $file.tex failed; see $file.log for details?\n";
1403   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1404     or die "pslatex $file.tex failed; see $file.log for details?\n";
1405
1406   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1407     or die "dvips failed";
1408
1409   open(POSTSCRIPT, "<$file.ps")
1410     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1411
1412   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1413
1414   my $ps = '';
1415   while (<POSTSCRIPT>) {
1416     $ps .= $_;
1417   }
1418
1419   close POSTSCRIPT;
1420
1421   return $ps;
1422
1423 }
1424
1425 =item print_pdf [ TIME [ , TEMPLATE ] ]
1426
1427 Returns an PDF invoice, as a scalar.
1428
1429 TIME an optional value used to control the printing of overdue messages.  The
1430 default is now.  It isn't the date of the invoice; that's the `_date' field.
1431 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1432 L<Time::Local> and L<Date::Parse> for conversion functions.
1433
1434 =cut
1435
1436 sub print_pdf {
1437   my $self = shift;
1438
1439   my $file = $self->print_latex(@_);
1440
1441   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1442   chdir($dir);
1443
1444   #system('pdflatex', "$file.tex");
1445   #system('pdflatex', "$file.tex");
1446   #! LaTeX Error: Unknown graphics extension: .eps.
1447
1448   my $sfile = shell_quote $file;
1449
1450   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1451     or die "pslatex $file.tex failed; see $file.log for details?\n";
1452   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1453     or die "pslatex $file.tex failed; see $file.log for details?\n";
1454
1455   #system('dvipdf', "$file.dvi", "$file.pdf" );
1456   system(
1457     "dvips -q -t letter -f $sfile.dvi ".
1458     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1459     "     -c save pop -"
1460   ) == 0
1461     or die "dvips | gs failed: $!";
1462
1463   open(PDF, "<$file.pdf")
1464     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1465
1466   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1467
1468   my $pdf = '';
1469   while (<PDF>) {
1470     $pdf .= $_;
1471   }
1472
1473   close PDF;
1474
1475   return $pdf;
1476
1477 }
1478
1479 # quick subroutine for print_latex
1480 #
1481 # There are ten characters that LaTeX treats as special characters, which
1482 # means that they do not simply typeset themselves: 
1483 #      # $ % & ~ _ ^ \ { }
1484 #
1485 # TeX ignores blanks following an escaped character; if you want a blank (as
1486 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1487
1488 sub _latex_escape {
1489   my $value = shift;
1490   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1491   $value =~ s/([<>])/\$$1\$/g;
1492   $value;
1493 }
1494
1495 #utility methods for print_*
1496
1497 sub balance_due_msg {
1498   my $self = shift;
1499   my $msg = 'Balance Due';
1500   return $msg unless $conf->exists('invoice_default_terms');
1501   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1502     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1503   } elsif ( $conf->config('invoice_default_terms') ) {
1504     $msg .= ' - '. $conf->config('invoice_default_terms');
1505   }
1506   $msg;
1507 }
1508
1509 sub _items {
1510   my $self = shift;
1511   my @display = scalar(@_)
1512                 ? @_
1513                 : qw( _items_previous _items_pkg );
1514                 #: qw( _items_pkg );
1515                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1516   my @b = ();
1517   foreach my $display ( @display ) {
1518     push @b, $self->$display(@_);
1519   }
1520   @b;
1521 }
1522
1523 sub _items_previous {
1524   my $self = shift;
1525   my $cust_main = $self->cust_main;
1526   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1527   my @b = ();
1528   foreach ( @pr_cust_bill ) {
1529     push @b, {
1530       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
1531                        ' ('. time2str('%x',$_->_date). ')',
1532       #'pkgpart'     => 'N/A',
1533       'pkgnum'      => 'N/A',
1534       'amount'      => sprintf("%10.2f", $_->owed),
1535     };
1536   }
1537   @b;
1538
1539   #{
1540   #    'description'     => 'Previous Balance',
1541   #    #'pkgpart'         => 'N/A',
1542   #    'pkgnum'          => 'N/A',
1543   #    'amount'          => sprintf("%10.2f", $pr_total ),
1544   #    'ext_description' => [ map {
1545   #                                 "Invoice ". $_->invnum.
1546   #                                 " (". time2str("%x",$_->_date). ") ".
1547   #                                 sprintf("%10.2f", $_->owed)
1548   #                         } @pr_cust_bill ],
1549
1550   #};
1551 }
1552
1553 sub _items_pkg {
1554   my $self = shift;
1555   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1556   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1557 }
1558
1559 sub _items_tax {
1560   my $self = shift;
1561   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1562   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1563 }
1564
1565 sub _items_cust_bill_pkg {
1566   my $self = shift;
1567   my $cust_bill_pkg = shift;
1568
1569   my @b = ();
1570   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1571
1572     if ( $cust_bill_pkg->pkgnum ) {
1573
1574       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1575       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1576       my $pkg = $part_pkg->pkg;
1577
1578       if ( $cust_bill_pkg->setup != 0 ) {
1579         my $description = $pkg;
1580         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1581         my @d = $cust_pkg->h_labels_short($self->_date);
1582         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1583         push @b, {
1584           description     => $description,
1585           #pkgpart         => $part_pkg->pkgpart,
1586           pkgnum          => $cust_pkg->pkgnum,
1587           amount          => sprintf("%10.2f", $cust_bill_pkg->setup),
1588           ext_description => \@d,
1589         };
1590       }
1591
1592       if ( $cust_bill_pkg->recur != 0 ) {
1593         push @b, {
1594           description     => "$pkg (" .
1595                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
1596                                time2str('%x', $cust_bill_pkg->edate). ')',
1597           #pkgpart         => $part_pkg->pkgpart,
1598           pkgnum          => $cust_pkg->pkgnum,
1599           amount          => sprintf("%10.2f", $cust_bill_pkg->recur),
1600           ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1601                                                          $cust_bill_pkg->sdate),
1602                                $cust_bill_pkg->details,
1603                              ],
1604         };
1605       }
1606
1607     } else { #pkgnum tax or one-shot line item (??)
1608
1609       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1610                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1611                      : 'Tax';
1612       if ( $cust_bill_pkg->setup != 0 ) {
1613         push @b, {
1614           'description' => $itemdesc,
1615           'amount'      => sprintf("%10.2f", $cust_bill_pkg->setup),
1616         };
1617       }
1618       if ( $cust_bill_pkg->recur != 0 ) {
1619         push @b, {
1620           'description' => "$itemdesc (".
1621                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
1622                            time2str("%x", $cust_bill_pkg->edate). ')',
1623           'amount'      => sprintf("%10.2f", $cust_bill_pkg->recur),
1624         };
1625       }
1626
1627     }
1628
1629   }
1630
1631   @b;
1632
1633 }
1634
1635 sub _items_credits {
1636   my $self = shift;
1637
1638   my @b;
1639   #credits
1640   foreach ( $self->cust_credited ) {
1641
1642     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1643
1644     my $reason = $_->cust_credit->reason;
1645     #my $reason = substr($_->cust_credit->reason,0,32);
1646     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1647     $reason = " ($reason) " if $reason;
1648     push @b, {
1649       #'description' => 'Credit ref\#'. $_->crednum.
1650       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
1651       #                 $reason,
1652       'description' => 'Credit applied '.
1653                        time2str("%x",$_->cust_credit->_date). $reason,
1654       'amount'      => sprintf("%10.2f",$_->amount),
1655     };
1656   }
1657   #foreach ( @cr_cust_credit ) {
1658   #  push @buf,[
1659   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1660   #    $money_char. sprintf("%10.2f",$_->credited)
1661   #  ];
1662   #}
1663
1664   @b;
1665
1666 }
1667
1668 sub _items_payments {
1669   my $self = shift;
1670
1671   my @b;
1672   #get & print payments
1673   foreach ( $self->cust_bill_pay ) {
1674
1675     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1676
1677     push @b, {
1678       'description' => "Payment received ".
1679                        time2str("%x",$_->cust_pay->_date ),
1680       'amount'      => sprintf("%10.2f", $_->amount )
1681     };
1682   }
1683
1684   @b;
1685
1686 }
1687
1688 =back
1689
1690 =head1 BUGS
1691
1692 The delete method.
1693
1694 print_text formatting (and some logic :/) is in source, but needs to be
1695 slurped in from a file.  Also number of lines ($=).
1696
1697 =head1 SEE ALSO
1698
1699 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1700 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
1701 documentation.
1702
1703 =cut
1704
1705 1;
1706