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