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