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