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