Added support for FAX invoice destinations using a HylaFAX server.
[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;
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  ) { #email
422
423     #better to notify this person than silence
424     @invoicing_list = ($invoice_from) unless @invoicing_list;
425
426     my $error = send_email(
427       $self->generate_email(
428         'from'   => $invoice_from,
429         'to'     => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
430         'print_text' => [ @print_text ],
431       )
432     );
433     die "can't email invoice: $error\n" if $error;
434     #die "$error\n" if $error;
435
436   }
437
438   if ( grep { $_ =~ /^(POST|FAX)$/ } @invoicing_list ) {
439     my $lpr_data;
440     if ($conf->config('invoice_latex')) {
441       $lpr_data = [ $self->print_ps('', $template) ];
442     } else {
443       $lpr_data = \@print_text;
444     }
445
446     if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
447       my $lpr = $conf->config('lpr');
448       open(LPR, "|$lpr")
449         or die "Can't open pipe to $lpr: $!\n";
450       print LPR @{$lpr_data};
451       close LPR
452         or die $! ? "Error closing $lpr: $!\n"
453                   : "Exit status $? from $lpr\n";
454     }
455
456     if ( grep { $_ eq 'FAX' } @invoicing_list ) { #fax
457       unless ($conf->exists('invoice_latex')) {
458         die 'FAX invoice destination not supported with plain text invoices.'
459       }
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 = $conf->config($templatefile)
1106     or die "cannot load config file $templatefile";
1107
1108   my %invoice_data = (
1109     'invnum'       => $self->invnum,
1110     'date'         => time2str('%b %o, %Y', $self->_date),
1111     'agent'        => _latex_escape($cust_main->agent->agent),
1112     'payname'      => _latex_escape($cust_main->payname),
1113     'company'      => _latex_escape($cust_main->company),
1114     'address1'     => _latex_escape($cust_main->address1),
1115     'address2'     => _latex_escape($cust_main->address2),
1116     'city'         => _latex_escape($cust_main->city),
1117     'state'        => _latex_escape($cust_main->state),
1118     'zip'          => _latex_escape($cust_main->zip),
1119     'country'      => _latex_escape($cust_main->country),
1120     'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
1121     'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
1122     'quantity'     => 1,
1123     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1124     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1125   );
1126
1127   my $countrydefault = $conf->config('countrydefault') || 'US';
1128   $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1129
1130   #do variable substitutions in notes
1131   $invoice_data{'notes'} =
1132     join("\n",
1133       map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1134         $conf->config_orbase('invoice_latexnotes', $suffix)
1135     );
1136
1137   $invoice_data{'footer'} =~ s/\n+$//;
1138   $invoice_data{'smallfooter'} =~ s/\n+$//;
1139   $invoice_data{'notes'} =~ s/\n+$//;
1140
1141   $invoice_data{'po_line'} =
1142     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1143       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1144       : '~';
1145
1146   my @line_item = ();
1147   my @total_item = ();
1148   my @filled_in = ();
1149   while ( @invoice_template ) {
1150     my $line = shift @invoice_template;
1151
1152     if ( $line =~ /^%%Detail\s*$/ ) {
1153
1154       while ( ( my $line_item_line = shift @invoice_template )
1155               !~ /^%%EndDetail\s*$/                            ) {
1156         push @line_item, $line_item_line;
1157       }
1158       foreach my $line_item ( $self->_items ) {
1159       #foreach my $line_item ( $self->_items_pkg ) {
1160         $invoice_data{'ref'} = $line_item->{'pkgnum'};
1161         $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1162         if ( exists $line_item->{'ext_description'} ) {
1163           $invoice_data{'description'} .=
1164             "\\tabularnewline\n~~".
1165             join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1166         }
1167         $invoice_data{'amount'} = $line_item->{'amount'};
1168         $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1169         push @filled_in,
1170           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1171       }
1172
1173     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1174
1175       while ( ( my $total_item_line = shift @invoice_template )
1176               !~ /^%%EndTotalDetails\s*$/                      ) {
1177         push @total_item, $total_item_line;
1178       }
1179
1180       my @total_fill = ();
1181
1182       my $taxtotal = 0;
1183       foreach my $tax ( $self->_items_tax ) {
1184         $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1185         $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1186         push @total_fill,
1187           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1188               @total_item;
1189       }
1190
1191       if ( $taxtotal ) {
1192         $invoice_data{'total_item'} = 'Sub-total';
1193         $invoice_data{'total_amount'} =
1194           '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1195         unshift @total_fill,
1196           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1197               @total_item;
1198       }
1199
1200       $invoice_data{'total_item'} = '\textbf{Total}';
1201       $invoice_data{'total_amount'} =
1202         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1203       push @total_fill,
1204         map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1205             @total_item;
1206
1207       #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1208
1209       # credits
1210       foreach my $credit ( $self->_items_credits ) {
1211         $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1212         #$credittotal
1213         $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1214         push @total_fill, 
1215           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1216               @total_item;
1217       }
1218
1219       # payments
1220       foreach my $payment ( $self->_items_payments ) {
1221         $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1222         #$paymenttotal
1223         $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1224         push @total_fill, 
1225           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1226               @total_item;
1227       }
1228
1229       $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1230       $invoice_data{'total_amount'} =
1231         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1232       push @total_fill,
1233         map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1234             @total_item;
1235
1236       push @filled_in, @total_fill;
1237
1238     } else {
1239       #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1240       $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1241       push @filled_in, $line;
1242     }
1243
1244   }
1245
1246   sub nounder {
1247     my $var = $1;
1248     $var =~ s/_/\-/g;
1249     $var;
1250   }
1251
1252   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1253   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1254                            DIR      => $dir,
1255                            SUFFIX   => '.tex',
1256                            UNLINK   => 0,
1257                          ) or die "can't open temp file: $!\n";
1258   print $fh join("\n", @filled_in ), "\n";
1259   close $fh;
1260
1261   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1262   return $1;
1263
1264 }
1265
1266 =item print_ps [ TIME [ , TEMPLATE ] ]
1267
1268 Returns an postscript invoice, as a scalar.
1269
1270 TIME an optional value used to control the printing of overdue messages.  The
1271 default is now.  It isn't the date of the invoice; that's the `_date' field.
1272 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1273 L<Time::Local> and L<Date::Parse> for conversion functions.
1274
1275 =cut
1276
1277 sub print_ps {
1278   my $self = shift;
1279
1280   my $file = $self->print_latex(@_);
1281
1282   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1283   chdir($dir);
1284
1285   my $sfile = shell_quote $file;
1286
1287   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1288     or die "pslatex $file.tex failed; see $file.log for details?\n";
1289   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1290     or die "pslatex $file.tex failed; see $file.log for details?\n";
1291
1292   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1293     or die "dvips failed";
1294
1295   open(POSTSCRIPT, "<$file.ps")
1296     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1297
1298   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1299
1300   my $ps = '';
1301   while (<POSTSCRIPT>) {
1302     $ps .= $_;
1303   }
1304
1305   close POSTSCRIPT;
1306
1307   return $ps;
1308
1309 }
1310
1311 =item print_pdf [ TIME [ , TEMPLATE ] ]
1312
1313 Returns an PDF invoice, as a scalar.
1314
1315 TIME an optional value used to control the printing of overdue messages.  The
1316 default is now.  It isn't the date of the invoice; that's the `_date' field.
1317 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1318 L<Time::Local> and L<Date::Parse> for conversion functions.
1319
1320 =cut
1321
1322 sub print_pdf {
1323   my $self = shift;
1324
1325   my $file = $self->print_latex(@_);
1326
1327   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1328   chdir($dir);
1329
1330   #system('pdflatex', "$file.tex");
1331   #system('pdflatex', "$file.tex");
1332   #! LaTeX Error: Unknown graphics extension: .eps.
1333
1334   my $sfile = shell_quote $file;
1335
1336   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1337     or die "pslatex $file.tex failed: $!";
1338   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1339     or die "pslatex $file.tex failed: $!";
1340
1341   #system('dvipdf', "$file.dvi", "$file.pdf" );
1342   system(
1343     "dvips -q -t letter -f $sfile.dvi ".
1344     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1345     "     -c save pop -"
1346   ) == 0
1347     or die "dvips | gs failed: $!";
1348
1349   open(PDF, "<$file.pdf")
1350     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1351
1352   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1353
1354   my $pdf = '';
1355   while (<PDF>) {
1356     $pdf .= $_;
1357   }
1358
1359   close PDF;
1360
1361   return $pdf;
1362
1363 }
1364
1365 # quick subroutine for print_latex
1366 #
1367 # There are ten characters that LaTeX treats as special characters, which
1368 # means that they do not simply typeset themselves: 
1369 #      # $ % & ~ _ ^ \ { }
1370 #
1371 # TeX ignores blanks following an escaped character; if you want a blank (as
1372 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1373
1374 sub _latex_escape {
1375   my $value = shift;
1376   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1377   $value;
1378 }
1379
1380 #utility methods for print_*
1381
1382 sub balance_due_msg {
1383   my $self = shift;
1384   my $msg = 'Balance Due';
1385   return $msg unless $conf->exists('invoice_default_terms');
1386   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1387     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1388   } elsif ( $conf->config('invoice_default_terms') ) {
1389     $msg .= ' - '. $conf->config('invoice_default_terms');
1390   }
1391   $msg;
1392 }
1393
1394 sub _items {
1395   my $self = shift;
1396   my @display = scalar(@_)
1397                 ? @_
1398                 : qw( _items_previous _items_pkg );
1399                 #: qw( _items_pkg );
1400                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1401   my @b = ();
1402   foreach my $display ( @display ) {
1403     push @b, $self->$display(@_);
1404   }
1405   @b;
1406 }
1407
1408 sub _items_previous {
1409   my $self = shift;
1410   my $cust_main = $self->cust_main;
1411   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1412   my @b = ();
1413   foreach ( @pr_cust_bill ) {
1414     push @b, {
1415       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
1416                        ' ('. time2str('%x',$_->_date). ')',
1417       #'pkgpart'     => 'N/A',
1418       'pkgnum'      => 'N/A',
1419       'amount'      => sprintf("%10.2f", $_->owed),
1420     };
1421   }
1422   @b;
1423
1424   #{
1425   #    'description'     => 'Previous Balance',
1426   #    #'pkgpart'         => 'N/A',
1427   #    'pkgnum'          => 'N/A',
1428   #    'amount'          => sprintf("%10.2f", $pr_total ),
1429   #    'ext_description' => [ map {
1430   #                                 "Invoice ". $_->invnum.
1431   #                                 " (". time2str("%x",$_->_date). ") ".
1432   #                                 sprintf("%10.2f", $_->owed)
1433   #                         } @pr_cust_bill ],
1434
1435   #};
1436 }
1437
1438 sub _items_pkg {
1439   my $self = shift;
1440   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1441   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1442 }
1443
1444 sub _items_tax {
1445   my $self = shift;
1446   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1447   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1448 }
1449
1450 sub _items_cust_bill_pkg {
1451   my $self = shift;
1452   my $cust_bill_pkg = shift;
1453
1454   my @b = ();
1455   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1456
1457     if ( $cust_bill_pkg->pkgnum ) {
1458
1459       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1460       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1461       my $pkg = $part_pkg->pkg;
1462
1463       if ( $cust_bill_pkg->setup != 0 ) {
1464         my $description = $pkg;
1465         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1466         my @d = $cust_pkg->h_labels_short($self->_date);
1467         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1468         push @b, {
1469           description     => $description,
1470           #pkgpart         => $part_pkg->pkgpart,
1471           pkgnum          => $cust_pkg->pkgnum,
1472           amount          => sprintf("%10.2f", $cust_bill_pkg->setup),
1473           ext_description => \@d,
1474         };
1475       }
1476
1477       if ( $cust_bill_pkg->recur != 0 ) {
1478         push @b, {
1479           description     => "$pkg (" .
1480                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
1481                                time2str('%x', $cust_bill_pkg->edate). ')',
1482           #pkgpart         => $part_pkg->pkgpart,
1483           pkgnum          => $cust_pkg->pkgnum,
1484           amount          => sprintf("%10.2f", $cust_bill_pkg->recur),
1485           ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1486                                                          $cust_bill_pkg->sdate),
1487                                $cust_bill_pkg->details,
1488                              ],
1489         };
1490       }
1491
1492     } else { #pkgnum tax or one-shot line item (??)
1493
1494       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1495                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1496                      : 'Tax';
1497       if ( $cust_bill_pkg->setup != 0 ) {
1498         push @b, {
1499           'description' => $itemdesc,
1500           'amount'      => sprintf("%10.2f", $cust_bill_pkg->setup),
1501         };
1502       }
1503       if ( $cust_bill_pkg->recur != 0 ) {
1504         push @b, {
1505           'description' => "$itemdesc (".
1506                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
1507                            time2str("%x", $cust_bill_pkg->edate). ')',
1508           'amount'      => sprintf("%10.2f", $cust_bill_pkg->recur),
1509         };
1510       }
1511
1512     }
1513
1514   }
1515
1516   @b;
1517
1518 }
1519
1520 sub _items_credits {
1521   my $self = shift;
1522
1523   my @b;
1524   #credits
1525   foreach ( $self->cust_credited ) {
1526
1527     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1528
1529     my $reason = $_->cust_credit->reason;
1530     #my $reason = substr($_->cust_credit->reason,0,32);
1531     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1532     $reason = " ($reason) " if $reason;
1533     push @b, {
1534       #'description' => 'Credit ref\#'. $_->crednum.
1535       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
1536       #                 $reason,
1537       'description' => 'Credit applied '.
1538                        time2str("%x",$_->cust_credit->_date). $reason,
1539       'amount'      => sprintf("%10.2f",$_->amount),
1540     };
1541   }
1542   #foreach ( @cr_cust_credit ) {
1543   #  push @buf,[
1544   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1545   #    $money_char. sprintf("%10.2f",$_->credited)
1546   #  ];
1547   #}
1548
1549   @b;
1550
1551 }
1552
1553 sub _items_payments {
1554   my $self = shift;
1555
1556   my @b;
1557   #get & print payments
1558   foreach ( $self->cust_bill_pay ) {
1559
1560     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1561
1562     push @b, {
1563       'description' => "Payment received ".
1564                        time2str("%x",$_->cust_pay->_date ),
1565       'amount'      => sprintf("%10.2f", $_->amount )
1566     };
1567   }
1568
1569   @b;
1570
1571 }
1572
1573 =back
1574
1575 =head1 BUGS
1576
1577 The delete method.
1578
1579 print_text formatting (and some logic :/) is in source, but needs to be
1580 slurped in from a file.  Also number of lines ($=).
1581
1582 =head1 SEE ALSO
1583
1584 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1585 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
1586 documentation.
1587
1588 =cut
1589
1590 1;
1591