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