33552523ae921645fc1f18b11af85a42bc5203ea
[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 );
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 { $_ ne 'POST' } $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 { $_ ne 'POST' } @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 { $_ ne 'POST' } @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 { $_ eq 'POST' } @invoicing_list ) { #postal
439     @print_text = $self->print_ps('', $template)
440       if $conf->config('invoice_latex');
441     my $lpr = $conf->config('lpr');
442     open(LPR, "|$lpr")
443       or die "Can't open pipe to $lpr: $!\n";
444     print LPR @print_text;
445     close LPR
446       or die $! ? "Error closing $lpr: $!\n"
447                 : "Exit status $? from $lpr\n";
448   }
449
450   '';
451
452 }
453
454 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
455
456 Like B<send>, but only sends the invoice if it is the newest open invoice for
457 this customer.
458
459 =cut
460
461 sub send_if_newest {
462   my $self = shift;
463
464   return ''
465     if scalar(
466                grep { $_->owed > 0 } 
467                     qsearch('cust_bill', {
468                       'custnum' => $self->custnum,
469                       #'_date'   => { op=>'>', value=>$self->_date },
470                       'invnum'  => { op=>'>', value=>$self->invnum },
471                     } )
472              );
473     
474   $self->send(@_);
475 }
476
477 =item send_csv OPTIONS
478
479 Sends invoice as a CSV data-file to a remote host with the specified protocol.
480
481 Options are:
482
483 protocol - currently only "ftp"
484 server
485 username
486 password
487 dir
488
489 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
490 and YYMMDDHHMMSS is a timestamp.
491
492 The fields of the CSV file is as follows:
493
494 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
495
496 =over 4
497
498 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
499
500 If B<record_type> is C<cust_bill>, this is a primary invoice record.  The
501 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
502 fields are filled in.
503
504 If B<record_type> is C<cust_bill_pkg>, this is a line item record.  Only the
505 first two fields (B<record_type> and B<invnum>) and the last five fields
506 (B<pkg> through B<edate>) are filled in.
507
508 =item invnum - invoice number
509
510 =item custnum - customer number
511
512 =item _date - invoice date
513
514 =item charged - total invoice amount
515
516 =item first - customer first name
517
518 =item last - customer first name
519
520 =item company - company name
521
522 =item address1 - address line 1
523
524 =item address2 - address line 1
525
526 =item city
527
528 =item state
529
530 =item zip
531
532 =item country
533
534 =item pkg - line item description
535
536 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
537
538 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
539
540 =item sdate - start date for recurring fee
541
542 =item edate - end date for recurring fee
543
544 =back
545
546 =cut
547
548 sub send_csv {
549   my($self, %opt) = @_;
550
551   #part one: create file
552
553   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
554   mkdir $spooldir, 0700 unless -d $spooldir;
555
556   my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
557
558   open(CSV, ">$file") or die "can't open $file: $!";
559
560   eval "use Text::CSV_XS";
561   die $@ if $@;
562
563   my $csv = Text::CSV_XS->new({'always_quote'=>1});
564
565   my $cust_main = $self->cust_main;
566
567   $csv->combine(
568     'cust_bill',
569     $self->invnum,
570     $self->custnum,
571     time2str("%x", $self->_date),
572     sprintf("%.2f", $self->charged),
573     ( map { $cust_main->getfield($_) }
574         qw( first last company address1 address2 city state zip country ) ),
575     map { '' } (1..5),
576   ) or die "can't create csv";
577   print CSV $csv->string. "\n";
578
579   #new charges (false laziness w/print_text)
580   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
581
582     my($pkg, $setup, $recur, $sdate, $edate);
583     if ( $cust_bill_pkg->pkgnum ) {
584     
585       ($pkg, $setup, $recur, $sdate, $edate) = (
586         $cust_bill_pkg->cust_pkg->part_pkg->pkg,
587         ( $cust_bill_pkg->setup != 0
588           ? sprintf("%.2f", $cust_bill_pkg->setup )
589           : '' ),
590         ( $cust_bill_pkg->recur != 0
591           ? sprintf("%.2f", $cust_bill_pkg->recur )
592           : '' ),
593         time2str("%x", $cust_bill_pkg->sdate),
594         time2str("%x", $cust_bill_pkg->edate),
595       );
596
597     } else { #pkgnum tax
598       next unless $cust_bill_pkg->setup != 0;
599       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
600                        ? ( $cust_bill_pkg->itemdesc || 'Tax' )
601                        : 'Tax';
602       ($pkg, $setup, $recur, $sdate, $edate) =
603         ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
604     }
605
606     $csv->combine(
607       'cust_bill_pkg',
608       $self->invnum,
609       ( map { '' } (1..11) ),
610       ($pkg, $setup, $recur, $sdate, $edate)
611     ) or die "can't create csv";
612     print CSV $csv->string. "\n";
613
614   }
615
616   close CSV or die "can't close CSV: $!";
617
618   #part two: upload it
619
620   my $net;
621   if ( $opt{protocol} eq 'ftp' ) {
622     eval "use Net::FTP;";
623     die $@ if $@;
624     $net = Net::FTP->new($opt{server}) or die @$;
625   } else {
626     die "unknown protocol: $opt{protocol}";
627   }
628
629   $net->login( $opt{username}, $opt{password} )
630     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
631
632   $net->binary or die "can't set binary mode";
633
634   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
635
636   $net->put($file) or die "can't put $file: $!";
637
638   $net->quit;
639
640   unlink $file;
641
642 }
643
644 =item comp
645
646 Pays this invoice with a compliemntary payment.  If there is an error,
647 returns the error, otherwise returns false.
648
649 =cut
650
651 sub comp {
652   my $self = shift;
653   my $cust_pay = new FS::cust_pay ( {
654     'invnum'   => $self->invnum,
655     'paid'     => $self->owed,
656     '_date'    => '',
657     'payby'    => 'COMP',
658     'payinfo'  => $self->cust_main->payinfo,
659     'paybatch' => '',
660   } );
661   $cust_pay->insert;
662 }
663
664 =item realtime_card
665
666 Attempts to pay this invoice with a credit card payment via a
667 Business::OnlinePayment realtime gateway.  See
668 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
669 for supported processors.
670
671 =cut
672
673 sub realtime_card {
674   my $self = shift;
675   $self->realtime_bop( 'CC', @_ );
676 }
677
678 =item realtime_ach
679
680 Attempts to pay this invoice with an electronic check (ACH) payment via a
681 Business::OnlinePayment realtime gateway.  See
682 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
683 for supported processors.
684
685 =cut
686
687 sub realtime_ach {
688   my $self = shift;
689   $self->realtime_bop( 'ECHECK', @_ );
690 }
691
692 =item realtime_lec
693
694 Attempts to pay this invoice with phone bill (LEC) payment via a
695 Business::OnlinePayment realtime gateway.  See
696 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
697 for supported processors.
698
699 =cut
700
701 sub realtime_lec {
702   my $self = shift;
703   $self->realtime_bop( 'LEC', @_ );
704 }
705
706 sub realtime_bop {
707   my( $self, $method ) = @_;
708
709   my $cust_main = $self->cust_main;
710   my $balance = $cust_main->balance;
711   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
712   $amount = sprintf("%.2f", $amount);
713   return "not run (balance $balance)" unless $amount > 0;
714
715   my $description = 'Internet Services';
716   if ( $conf->exists('business-onlinepayment-description') ) {
717     my $dtempl = $conf->config('business-onlinepayment-description');
718
719     my $agent_obj = $cust_main->agent
720       or die "can't retreive agent for $cust_main (agentnum ".
721              $cust_main->agentnum. ")";
722     my $agent = $agent_obj->agent;
723     my $pkgs = join(', ',
724       map { $_->cust_pkg->part_pkg->pkg }
725         grep { $_->pkgnum } $self->cust_bill_pkg
726     );
727     $description = eval qq("$dtempl");
728   }
729
730   $cust_main->realtime_bop($method, $amount,
731     'description' => $description,
732     'invnum'      => $self->invnum,
733   );
734
735 }
736
737 =item batch_card
738
739 Adds a payment for this invoice to the pending credit card batch (see
740 L<FS::cust_pay_batch>).
741
742 =cut
743
744 sub batch_card {
745   my $self = shift;
746   my $cust_main = $self->cust_main;
747
748   my $cust_pay_batch = new FS::cust_pay_batch ( {
749     'invnum'   => $self->getfield('invnum'),
750     'custnum'  => $cust_main->getfield('custnum'),
751     'last'     => $cust_main->getfield('last'),
752     'first'    => $cust_main->getfield('first'),
753     'address1' => $cust_main->getfield('address1'),
754     'address2' => $cust_main->getfield('address2'),
755     'city'     => $cust_main->getfield('city'),
756     'state'    => $cust_main->getfield('state'),
757     'zip'      => $cust_main->getfield('zip'),
758     'country'  => $cust_main->getfield('country'),
759     'cardnum'  => $cust_main->getfield('payinfo'),
760     'exp'      => $cust_main->getfield('paydate'),
761     'payname'  => $cust_main->getfield('payname'),
762     'amount'   => $self->owed,
763   } );
764   my $error = $cust_pay_batch->insert;
765   die $error if $error;
766
767   '';
768 }
769
770 sub _agent_template {
771   my $self = shift;
772   $self->_agent_plandata('agent_templatename');
773 }
774
775 sub _agent_invoice_from {
776   my $self = shift;
777   $self->_agent_plandata('agent_invoice_from');
778 }
779
780 sub _agent_plandata {
781   my( $self, $option ) = @_;
782
783   my $part_bill_event = qsearchs( 'part_bill_event',
784     {
785       'payby'     => $self->cust_main->payby,
786       'plan'      => 'send_agent',
787       'plandata'  => { 'op'    => '~',
788                        'value' => "(^|\n)agentnum ".
789                                   $self->cust_main->agentnum.
790                                   "(\n|\$)",
791                      },
792     },
793     '',
794     'ORDER BY seconds LIMIT 1'
795   );
796
797   return '' unless $part_bill_event;
798
799   if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
800     return $1;
801   } else {
802     warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
803          " plandata for $option";
804     return '';
805   }
806
807 }
808
809 =item print_text [ TIME [ , TEMPLATE ] ]
810
811 Returns an text invoice, as a list of lines.
812
813 TIME an optional value used to control the printing of overdue messages.  The
814 default is now.  It isn't the date of the invoice; that's the `_date' field.
815 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
816 L<Time::Local> and L<Date::Parse> for conversion functions.
817
818 =cut
819
820 #still some false laziness w/print_text
821 sub print_text {
822
823   my( $self, $today, $template ) = @_;
824   $today ||= time;
825
826 #  my $invnum = $self->invnum;
827   my $cust_main = $self->cust_main;
828   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
829     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
830
831   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
832 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
833   #my $balance_due = $self->owed + $pr_total - $cr_total;
834   my $balance_due = $self->owed + $pr_total;
835
836   #my @collect = ();
837   #my($description,$amount);
838   @buf = ();
839
840   #previous balance
841   foreach ( @pr_cust_bill ) {
842     push @buf, [
843       "Previous Balance, Invoice #". $_->invnum. 
844                  " (". time2str("%x",$_->_date). ")",
845       $money_char. sprintf("%10.2f",$_->owed)
846     ];
847   }
848   if (@pr_cust_bill) {
849     push @buf,['','-----------'];
850     push @buf,[ 'Total Previous Balance',
851                 $money_char. sprintf("%10.2f",$pr_total ) ];
852     push @buf,['',''];
853   }
854
855   #new charges
856   foreach my $cust_bill_pkg (
857     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
858     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
859   ) {
860
861     if ( $cust_bill_pkg->pkgnum ) {
862
863       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
864       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
865       my $pkg = $part_pkg->pkg;
866
867       if ( $cust_bill_pkg->setup != 0 ) {
868         my $description = $pkg;
869         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
870         push @buf, [ $description,
871                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
872         push @buf,
873           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
874               $cust_pkg->h_labels($self->_date);
875       }
876
877       if ( $cust_bill_pkg->recur != 0 ) {
878         push @buf, [
879           "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
880                                 time2str("%x", $cust_bill_pkg->edate) . ")",
881           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
882         ];
883         push @buf,
884           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
885               $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
886       }
887
888       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
889
890     } else { #pkgnum tax or one-shot line item
891       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
892                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
893                      : 'Tax';
894       if ( $cust_bill_pkg->setup != 0 ) {
895         push @buf, [ $itemdesc,
896                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
897       }
898       if ( $cust_bill_pkg->recur != 0 ) {
899         push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
900                                   . time2str("%x", $cust_bill_pkg->edate). ")",
901                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
902                    ];
903       }
904     }
905   }
906
907   push @buf,['','-----------'];
908   push @buf,['Total New Charges',
909              $money_char. sprintf("%10.2f",$self->charged) ];
910   push @buf,['',''];
911
912   push @buf,['','-----------'];
913   push @buf,['Total Charges',
914              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
915   push @buf,['',''];
916
917   #credits
918   foreach ( $self->cust_credited ) {
919
920     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
921
922     my $reason = substr($_->cust_credit->reason,0,32);
923     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
924     $reason = " ($reason) " if $reason;
925     push @buf,[
926       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
927         $reason,
928       $money_char. sprintf("%10.2f",$_->amount)
929     ];
930   }
931   #foreach ( @cr_cust_credit ) {
932   #  push @buf,[
933   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
934   #    $money_char. sprintf("%10.2f",$_->credited)
935   #  ];
936   #}
937
938   #get & print payments
939   foreach ( $self->cust_bill_pay ) {
940
941     #something more elaborate if $_->amount ne ->cust_pay->paid ?
942
943     push @buf,[
944       "Payment received ". time2str("%x",$_->cust_pay->_date ),
945       $money_char. sprintf("%10.2f",$_->amount )
946     ];
947   }
948
949   #balance due
950   my $balance_due_msg = $self->balance_due_msg;
951
952   push @buf,['','-----------'];
953   push @buf,[$balance_due_msg, $money_char. 
954     sprintf("%10.2f", $balance_due ) ];
955
956   #create the template
957   $template ||= $self->_agent_template;
958   my $templatefile = 'invoice_template';
959   $templatefile .= "_$template" if length($template);
960   my @invoice_template = $conf->config($templatefile)
961     or die "cannot load config file $templatefile";
962   $invoice_lines = 0;
963   my $wasfunc = 0;
964   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
965     /invoice_lines\((\d*)\)/;
966     $invoice_lines += $1 || scalar(@buf);
967     $wasfunc=1;
968   }
969   die "no invoice_lines() functions in template?" unless $wasfunc;
970   my $invoice_template = new Text::Template (
971     TYPE   => 'ARRAY',
972     SOURCE => [ map "$_\n", @invoice_template ],
973   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
974   $invoice_template->compile()
975     or die "can't compile template: $Text::Template::ERROR";
976
977   #setup template variables
978   package FS::cust_bill::_template; #!
979   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
980
981   $invnum = $self->invnum;
982   $date = $self->_date;
983   $page = 1;
984   $agent = $self->cust_main->agent->agent;
985
986   if ( $FS::cust_bill::invoice_lines ) {
987     $total_pages =
988       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
989     $total_pages++
990       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
991   } else {
992     $total_pages = 1;
993   }
994
995   #format address (variable for the template)
996   my $l = 0;
997   @address = ( '', '', '', '', '', '' );
998   package FS::cust_bill; #!
999   $FS::cust_bill::_template::address[$l++] =
1000     $cust_main->payname.
1001       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1002         ? " (P.O. #". $cust_main->payinfo. ")"
1003         : ''
1004       )
1005   ;
1006   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1007     if $cust_main->company;
1008   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1009   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1010     if $cust_main->address2;
1011   $FS::cust_bill::_template::address[$l++] =
1012     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1013   $FS::cust_bill::_template::address[$l++] = $cust_main->country
1014     unless $cust_main->country eq 'US';
1015
1016         #  #overdue? (variable for the template)
1017         #  $FS::cust_bill::_template::overdue = ( 
1018         #    $balance_due > 0
1019         #    && $today > $self->_date 
1020         ##    && $self->printed > 1
1021         #    && $self->printed > 0
1022         #  );
1023
1024   #and subroutine for the template
1025   sub FS::cust_bill::_template::invoice_lines {
1026     my $lines = shift || scalar(@buf);
1027     map { 
1028       scalar(@buf) ? shift @buf : [ '', '' ];
1029     }
1030     ( 1 .. $lines );
1031   }
1032
1033   #and fill it in
1034   $FS::cust_bill::_template::page = 1;
1035   my $lines;
1036   my @collect;
1037   while (@buf) {
1038     push @collect, split("\n",
1039       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1040     );
1041     $FS::cust_bill::_template::page++;
1042   }
1043
1044   map "$_\n", @collect;
1045
1046 }
1047
1048 =item print_latex [ TIME [ , TEMPLATE ] ]
1049
1050 Internal method - returns a filename of a filled-in LaTeX template for this
1051 invoice (Note: add ".tex" to get the actual filename).
1052
1053 See print_ps and print_pdf for methods that return PostScript and PDF output.
1054
1055 TIME an optional value used to control the printing of overdue messages.  The
1056 default is now.  It isn't the date of the invoice; that's the `_date' field.
1057 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1058 L<Time::Local> and L<Date::Parse> for conversion functions.
1059
1060 =cut
1061
1062 #still some false laziness w/print_text
1063 sub print_latex {
1064
1065   my( $self, $today, $template ) = @_;
1066   $today ||= time;
1067
1068 #  my $invnum = $self->invnum;
1069   my $cust_main = $self->cust_main;
1070   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1071     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1072
1073   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1074 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1075   #my $balance_due = $self->owed + $pr_total - $cr_total;
1076   my $balance_due = $self->owed + $pr_total;
1077
1078   #my @collect = ();
1079   #my($description,$amount);
1080   @buf = ();
1081
1082   #create the template
1083   $template ||= $self->_agent_template;
1084   my $templatefile = 'invoice_latex';
1085   my $suffix = length($template) ? "_$template" : '';
1086   $templatefile .= $suffix;
1087   my @invoice_template = $conf->config($templatefile)
1088     or die "cannot load config file $templatefile";
1089
1090   my %invoice_data = (
1091     'invnum'       => $self->invnum,
1092     'date'         => time2str('%b %o, %Y', $self->_date),
1093     'agent'        => _latex_escape($cust_main->agent->agent),
1094     'payname'      => _latex_escape($cust_main->payname),
1095     'company'      => _latex_escape($cust_main->company),
1096     'address1'     => _latex_escape($cust_main->address1),
1097     'address2'     => _latex_escape($cust_main->address2),
1098     'city'         => _latex_escape($cust_main->city),
1099     'state'        => _latex_escape($cust_main->state),
1100     'zip'          => _latex_escape($cust_main->zip),
1101     'country'      => _latex_escape($cust_main->country),
1102     'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
1103     'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
1104     'quantity'     => 1,
1105     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1106     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1107   );
1108
1109   my $countrydefault = $conf->config('countrydefault') || 'US';
1110   $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1111
1112   #do variable substitutions in notes
1113   $invoice_data{'notes'} =
1114     join("\n",
1115       map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1116         $conf->config_orbase('invoice_latexnotes', $suffix)
1117     );
1118
1119   $invoice_data{'footer'} =~ s/\n+$//;
1120   $invoice_data{'smallfooter'} =~ s/\n+$//;
1121   $invoice_data{'notes'} =~ s/\n+$//;
1122
1123   $invoice_data{'po_line'} =
1124     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1125       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1126       : '~';
1127
1128   my @line_item = ();
1129   my @total_item = ();
1130   my @filled_in = ();
1131   while ( @invoice_template ) {
1132     my $line = shift @invoice_template;
1133
1134     if ( $line =~ /^%%Detail\s*$/ ) {
1135
1136       while ( ( my $line_item_line = shift @invoice_template )
1137               !~ /^%%EndDetail\s*$/                            ) {
1138         push @line_item, $line_item_line;
1139       }
1140       foreach my $line_item ( $self->_items ) {
1141       #foreach my $line_item ( $self->_items_pkg ) {
1142         $invoice_data{'ref'} = $line_item->{'pkgnum'};
1143         $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1144         if ( exists $line_item->{'ext_description'} ) {
1145           $invoice_data{'description'} .=
1146             "\\tabularnewline\n~~".
1147             join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1148         }
1149         $invoice_data{'amount'} = $line_item->{'amount'};
1150         $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1151         push @filled_in,
1152           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1153       }
1154
1155     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1156
1157       while ( ( my $total_item_line = shift @invoice_template )
1158               !~ /^%%EndTotalDetails\s*$/                      ) {
1159         push @total_item, $total_item_line;
1160       }
1161
1162       my @total_fill = ();
1163
1164       my $taxtotal = 0;
1165       foreach my $tax ( $self->_items_tax ) {
1166         $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1167         $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1168         push @total_fill,
1169           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1170               @total_item;
1171       }
1172
1173       if ( $taxtotal ) {
1174         $invoice_data{'total_item'} = 'Sub-total';
1175         $invoice_data{'total_amount'} =
1176           '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1177         unshift @total_fill,
1178           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1179               @total_item;
1180       }
1181
1182       $invoice_data{'total_item'} = '\textbf{Total}';
1183       $invoice_data{'total_amount'} =
1184         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1185       push @total_fill,
1186         map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1187             @total_item;
1188
1189       #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1190
1191       # credits
1192       foreach my $credit ( $self->_items_credits ) {
1193         $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1194         #$credittotal
1195         $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1196         push @total_fill, 
1197           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1198               @total_item;
1199       }
1200
1201       # payments
1202       foreach my $payment ( $self->_items_payments ) {
1203         $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1204         #$paymenttotal
1205         $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1206         push @total_fill, 
1207           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1208               @total_item;
1209       }
1210
1211       $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1212       $invoice_data{'total_amount'} =
1213         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1214       push @total_fill,
1215         map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1216             @total_item;
1217
1218       push @filled_in, @total_fill;
1219
1220     } else {
1221       #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1222       $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1223       push @filled_in, $line;
1224     }
1225
1226   }
1227
1228   sub nounder {
1229     my $var = $1;
1230     $var =~ s/_/\-/g;
1231     $var;
1232   }
1233
1234   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1235   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1236                            DIR      => $dir,
1237                            SUFFIX   => '.tex',
1238                            UNLINK   => 0,
1239                          ) or die "can't open temp file: $!\n";
1240   print $fh join("\n", @filled_in ), "\n";
1241   close $fh;
1242
1243   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1244   return $1;
1245
1246 }
1247
1248 =item print_ps [ TIME [ , TEMPLATE ] ]
1249
1250 Returns an postscript invoice, as a scalar.
1251
1252 TIME an optional value used to control the printing of overdue messages.  The
1253 default is now.  It isn't the date of the invoice; that's the `_date' field.
1254 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1255 L<Time::Local> and L<Date::Parse> for conversion functions.
1256
1257 =cut
1258
1259 sub print_ps {
1260   my $self = shift;
1261
1262   my $file = $self->print_latex(@_);
1263
1264   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1265   chdir($dir);
1266
1267   my $sfile = shell_quote $file;
1268
1269   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1270     or die "pslatex $file.tex failed; see $file.log for details?\n";
1271   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1272     or die "pslatex $file.tex failed; see $file.log for details?\n";
1273
1274   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1275     or die "dvips failed";
1276
1277   open(POSTSCRIPT, "<$file.ps")
1278     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1279
1280   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1281
1282   my $ps = '';
1283   while (<POSTSCRIPT>) {
1284     $ps .= $_;
1285   }
1286
1287   close POSTSCRIPT;
1288
1289   return $ps;
1290
1291 }
1292
1293 =item print_pdf [ TIME [ , TEMPLATE ] ]
1294
1295 Returns an PDF invoice, as a scalar.
1296
1297 TIME an optional value used to control the printing of overdue messages.  The
1298 default is now.  It isn't the date of the invoice; that's the `_date' field.
1299 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1300 L<Time::Local> and L<Date::Parse> for conversion functions.
1301
1302 =cut
1303
1304 sub print_pdf {
1305   my $self = shift;
1306
1307   my $file = $self->print_latex(@_);
1308
1309   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1310   chdir($dir);
1311
1312   #system('pdflatex', "$file.tex");
1313   #system('pdflatex', "$file.tex");
1314   #! LaTeX Error: Unknown graphics extension: .eps.
1315
1316   my $sfile = shell_quote $file;
1317
1318   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1319     or die "pslatex $file.tex failed: $!";
1320   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1321     or die "pslatex $file.tex failed: $!";
1322
1323   #system('dvipdf', "$file.dvi", "$file.pdf" );
1324   system(
1325     "dvips -q -t letter -f $sfile.dvi ".
1326     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1327     "     -c save pop -"
1328   ) == 0
1329     or die "dvips | gs failed: $!";
1330
1331   open(PDF, "<$file.pdf")
1332     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1333
1334   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1335
1336   my $pdf = '';
1337   while (<PDF>) {
1338     $pdf .= $_;
1339   }
1340
1341   close PDF;
1342
1343   return $pdf;
1344
1345 }
1346
1347 # quick subroutine for print_latex
1348 #
1349 # There are ten characters that LaTeX treats as special characters, which
1350 # means that they do not simply typeset themselves: 
1351 #      # $ % & ~ _ ^ \ { }
1352 #
1353 # TeX ignores blanks following an escaped character; if you want a blank (as
1354 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1355
1356 sub _latex_escape {
1357   my $value = shift;
1358   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1359   $value;
1360 }
1361
1362 #utility methods for print_*
1363
1364 sub balance_due_msg {
1365   my $self = shift;
1366   my $msg = 'Balance Due';
1367   return $msg unless $conf->exists('invoice_default_terms');
1368   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1369     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1370   } elsif ( $conf->config('invoice_default_terms') ) {
1371     $msg .= ' - '. $conf->config('invoice_default_terms');
1372   }
1373   $msg;
1374 }
1375
1376 sub _items {
1377   my $self = shift;
1378   my @display = scalar(@_)
1379                 ? @_
1380                 : qw( _items_previous _items_pkg );
1381                 #: qw( _items_pkg );
1382                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1383   my @b = ();
1384   foreach my $display ( @display ) {
1385     push @b, $self->$display(@_);
1386   }
1387   @b;
1388 }
1389
1390 sub _items_previous {
1391   my $self = shift;
1392   my $cust_main = $self->cust_main;
1393   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1394   my @b = ();
1395   foreach ( @pr_cust_bill ) {
1396     push @b, {
1397       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
1398                        ' ('. time2str('%x',$_->_date). ')',
1399       #'pkgpart'     => 'N/A',
1400       'pkgnum'      => 'N/A',
1401       'amount'      => sprintf("%10.2f", $_->owed),
1402     };
1403   }
1404   @b;
1405
1406   #{
1407   #    'description'     => 'Previous Balance',
1408   #    #'pkgpart'         => 'N/A',
1409   #    'pkgnum'          => 'N/A',
1410   #    'amount'          => sprintf("%10.2f", $pr_total ),
1411   #    'ext_description' => [ map {
1412   #                                 "Invoice ". $_->invnum.
1413   #                                 " (". time2str("%x",$_->_date). ") ".
1414   #                                 sprintf("%10.2f", $_->owed)
1415   #                         } @pr_cust_bill ],
1416
1417   #};
1418 }
1419
1420 sub _items_pkg {
1421   my $self = shift;
1422   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1423   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1424 }
1425
1426 sub _items_tax {
1427   my $self = shift;
1428   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1429   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1430 }
1431
1432 sub _items_cust_bill_pkg {
1433   my $self = shift;
1434   my $cust_bill_pkg = shift;
1435
1436   my @b = ();
1437   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1438
1439     if ( $cust_bill_pkg->pkgnum ) {
1440
1441       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1442       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1443       my $pkg = $part_pkg->pkg;
1444
1445       if ( $cust_bill_pkg->setup != 0 ) {
1446         my $description = $pkg;
1447         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1448         my @d = $cust_pkg->h_labels_short($self->_date);
1449         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1450         push @b, {
1451           description     => $description,
1452           #pkgpart         => $part_pkg->pkgpart,
1453           pkgnum          => $cust_pkg->pkgnum,
1454           amount          => sprintf("%10.2f", $cust_bill_pkg->setup),
1455           ext_description => \@d,
1456         };
1457       }
1458
1459       if ( $cust_bill_pkg->recur != 0 ) {
1460         push @b, {
1461           description     => "$pkg (" .
1462                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
1463                                time2str('%x', $cust_bill_pkg->edate). ')',
1464           #pkgpart         => $part_pkg->pkgpart,
1465           pkgnum          => $cust_pkg->pkgnum,
1466           amount          => sprintf("%10.2f", $cust_bill_pkg->recur),
1467           ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
1468                                                          $cust_bill_pkg->sdate),
1469                                $cust_bill_pkg->details,
1470                              ],
1471         };
1472       }
1473
1474     } else { #pkgnum tax or one-shot line item (??)
1475
1476       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1477                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1478                      : 'Tax';
1479       if ( $cust_bill_pkg->setup != 0 ) {
1480         push @b, {
1481           'description' => $itemdesc,
1482           'amount'      => sprintf("%10.2f", $cust_bill_pkg->setup),
1483         };
1484       }
1485       if ( $cust_bill_pkg->recur != 0 ) {
1486         push @b, {
1487           'description' => "$itemdesc (".
1488                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
1489                            time2str("%x", $cust_bill_pkg->edate). ')',
1490           'amount'      => sprintf("%10.2f", $cust_bill_pkg->recur),
1491         };
1492       }
1493
1494     }
1495
1496   }
1497
1498   @b;
1499
1500 }
1501
1502 sub _items_credits {
1503   my $self = shift;
1504
1505   my @b;
1506   #credits
1507   foreach ( $self->cust_credited ) {
1508
1509     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1510
1511     my $reason = $_->cust_credit->reason;
1512     #my $reason = substr($_->cust_credit->reason,0,32);
1513     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1514     $reason = " ($reason) " if $reason;
1515     push @b, {
1516       #'description' => 'Credit ref\#'. $_->crednum.
1517       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
1518       #                 $reason,
1519       'description' => 'Credit applied '.
1520                        time2str("%x",$_->cust_credit->_date). $reason,
1521       'amount'      => sprintf("%10.2f",$_->amount),
1522     };
1523   }
1524   #foreach ( @cr_cust_credit ) {
1525   #  push @buf,[
1526   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1527   #    $money_char. sprintf("%10.2f",$_->credited)
1528   #  ];
1529   #}
1530
1531   @b;
1532
1533 }
1534
1535 sub _items_payments {
1536   my $self = shift;
1537
1538   my @b;
1539   #get & print payments
1540   foreach ( $self->cust_bill_pay ) {
1541
1542     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1543
1544     push @b, {
1545       'description' => "Payment received ".
1546                        time2str("%x",$_->cust_pay->_date ),
1547       'amount'      => sprintf("%10.2f", $_->amount )
1548     };
1549   }
1550
1551   @b;
1552
1553 }
1554
1555 =back
1556
1557 =head1 BUGS
1558
1559 The delete method.
1560
1561 print_text formatting (and some logic :/) is in source, but needs to be
1562 slurped in from a file.  Also number of lines ($=).
1563
1564 =head1 SEE ALSO
1565
1566 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1567 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
1568 documentation.
1569
1570 =cut
1571
1572 1;
1573