4cfc59d10c21334983cc2b207a59ecb17b816f76
[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   system("pslatex $file.tex >/dev/null 2>&1") == 0
1142     or die "pslatex failed: $!";
1143   system("pslatex $file.tex >/dev/null 2>&1") == 0
1144     or die "pslatex failed: $!";
1145
1146   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1147     or die "dbips failed: $!";
1148
1149   open(POSTSCRIPT, "<$file.ps")
1150     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1151
1152   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1153
1154   my $ps = '';
1155   while (<POSTSCRIPT>) {
1156     $ps .= $_;
1157   }
1158
1159   close POSTSCRIPT;
1160
1161   return $ps;
1162
1163 }
1164
1165 =item print_pdf [ TIME [ , TEMPLATE ] ]
1166
1167 Returns an PDF invoice, as a scalar.
1168
1169 TIME an optional value used to control the printing of overdue messages.  The
1170 default is now.  It isn't the date of the invoice; that's the `_date' field.
1171 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1172 L<Time::Local> and L<Date::Parse> for conversion functions.
1173
1174 =cut
1175
1176 sub print_pdf {
1177   my $self = shift;
1178
1179   my $file = $self->print_latex(@_);
1180
1181   #system('pdflatex', "$file.tex");
1182   #system('pdflatex', "$file.tex");
1183   #! LaTeX Error: Unknown graphics extension: .eps.
1184
1185   system("pslatex $file.tex >/dev/null 2>&1") == 0
1186     or die "pslatex failed: $!";
1187   system("pslatex $file.tex >/dev/null 2>&1") == 0
1188     or die "pslatex failed: $!";
1189
1190   #system('dvipdf', "$file.dvi", "$file.pdf" );
1191   system(
1192     "dvips -q -t letter -f $file.dvi ".
1193     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf ".
1194     "     -c save pop -"
1195   ) == 0
1196     or die "dvips failed: $!";
1197
1198   open(PDF, "<$file.pdf")
1199     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1200
1201   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1202
1203   my $pdf = '';
1204   while (<PDF>) {
1205     $pdf .= $_;
1206   }
1207
1208   close PDF;
1209
1210   return $pdf;
1211
1212 }
1213
1214 # quick subroutine for print_latex
1215 #
1216 # There are ten characters that LaTeX treats as special characters, which
1217 # means that they do not simply typeset themselves: 
1218 #      # $ % & ~ _ ^ \ { }
1219 #
1220 # TeX ignores blanks following an escaped character; if you want a blank (as
1221 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1222
1223 sub _latex_escape {
1224   my $value = shift;
1225   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1226   $value;
1227 }
1228
1229 #utility methods for print_*
1230
1231 sub balance_due_msg {
1232   my $self = shift;
1233   my $msg = 'Balance Due';
1234   return $msg unless $conf->exists('invoice_default_terms');
1235   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1236     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1237   } elsif ( $conf->config('invoice_default_terms') ) {
1238     $msg .= ' - '. $conf->config('invoice_default_terms');
1239   }
1240   $msg;
1241 }
1242
1243 sub _items {
1244   my $self = shift;
1245   my @display = scalar(@_)
1246                 ? @_
1247                 : qw( _items_previous _items_pkg );
1248                 #: qw( _items_pkg );
1249                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1250   my @b = ();
1251   foreach my $display ( @display ) {
1252     push @b, $self->$display(@_);
1253   }
1254   @b;
1255 }
1256
1257 sub _items_previous {
1258   my $self = shift;
1259   my $cust_main = $self->cust_main;
1260   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1261   my @b = ();
1262   foreach ( @pr_cust_bill ) {
1263     push @b, {
1264       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
1265                        ' ('. time2str('%x',$_->_date). ')',
1266       #'pkgpart'     => 'N/A',
1267       'pkgnum'      => 'N/A',
1268       'amount'      => sprintf("%10.2f", $_->owed),
1269     };
1270   }
1271   @b;
1272
1273   #{
1274   #    'description'     => 'Previous Balance',
1275   #    #'pkgpart'         => 'N/A',
1276   #    'pkgnum'          => 'N/A',
1277   #    'amount'          => sprintf("%10.2f", $pr_total ),
1278   #    'ext_description' => [ map {
1279   #                                 "Invoice ". $_->invnum.
1280   #                                 " (". time2str("%x",$_->_date). ") ".
1281   #                                 sprintf("%10.2f", $_->owed)
1282   #                         } @pr_cust_bill ],
1283
1284   #};
1285 }
1286
1287 sub _items_pkg {
1288   my $self = shift;
1289   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1290   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1291 }
1292
1293 sub _items_tax {
1294   my $self = shift;
1295   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1296   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1297 }
1298
1299 sub _items_cust_bill_pkg {
1300   my $self = shift;
1301   my $cust_bill_pkg = shift;
1302
1303   my @b = ();
1304   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1305
1306     if ( $cust_bill_pkg->pkgnum ) {
1307
1308       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1309       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1310       my $pkg = $part_pkg->pkg;
1311
1312       my %labels;
1313       #tie %labels, 'Tie::IxHash';
1314       push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1315       my @ext_description;
1316       foreach my $label ( keys %labels ) {
1317         my @values = @{ $labels{$label} };
1318         my $num = scalar(@values);
1319         if ( $num > 5 ) {
1320           push @ext_description, "$label ($num)";
1321         } else {
1322           push @ext_description, map { "$label: $_" } @values;
1323         }
1324       }
1325
1326       if ( $cust_bill_pkg->setup != 0 ) {
1327         my $description = $pkg;
1328         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1329         my @d = @ext_description;
1330         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1331         push @b, {
1332           'description'     => $description,
1333           #'pkgpart'         => $part_pkg->pkgpart,
1334           'pkgnum'          => $cust_pkg->pkgnum,
1335           'amount'          => sprintf("%10.2f", $cust_bill_pkg->setup),
1336           'ext_description' => \@d,
1337         };
1338       }
1339
1340       if ( $cust_bill_pkg->recur != 0 ) {
1341         push @b, {
1342           'description'     => "$pkg (" .
1343                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
1344                                time2str('%x', $cust_bill_pkg->edate). ')',
1345           #'pkgpart'         => $part_pkg->pkgpart,
1346           'pkgnum'          => $cust_pkg->pkgnum,
1347           'amount'          => sprintf("%10.2f", $cust_bill_pkg->recur),
1348           'ext_description' => [ @ext_description,
1349                                  $cust_bill_pkg->details,
1350                                ],
1351         };
1352       }
1353
1354     } else { #pkgnum tax or one-shot line item (??)
1355
1356       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1357                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1358                      : 'Tax';
1359       if ( $cust_bill_pkg->setup != 0 ) {
1360         push @b, {
1361           'description' => $itemdesc,
1362           'amount'      => sprintf("%10.2f", $cust_bill_pkg->setup),
1363         };
1364       }
1365       if ( $cust_bill_pkg->recur != 0 ) {
1366         push @b, {
1367           'description' => "$itemdesc (".
1368                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
1369                            time2str("%x", $cust_bill_pkg->edate). ')',
1370           'amount'      => sprintf("%10.2f", $cust_bill_pkg->recur),
1371         };
1372       }
1373
1374     }
1375
1376   }
1377
1378   @b;
1379
1380 }
1381
1382 sub _items_credits {
1383   my $self = shift;
1384
1385   my @b;
1386   #credits
1387   foreach ( $self->cust_credited ) {
1388
1389     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1390
1391     my $reason = $_->cust_credit->reason;
1392     #my $reason = substr($_->cust_credit->reason,0,32);
1393     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1394     $reason = " ($reason) " if $reason;
1395     push @b, {
1396       #'description' => 'Credit ref\#'. $_->crednum.
1397       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
1398       #                 $reason,
1399       'description' => 'Credit applied'.
1400                        time2str("%x",$_->cust_credit->_date). $reason,
1401       'amount'      => sprintf("%10.2f",$_->amount),
1402     };
1403   }
1404   #foreach ( @cr_cust_credit ) {
1405   #  push @buf,[
1406   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1407   #    $money_char. sprintf("%10.2f",$_->credited)
1408   #  ];
1409   #}
1410
1411   @b;
1412
1413 }
1414
1415 sub _items_payments {
1416   my $self = shift;
1417
1418   my @b;
1419   #get & print payments
1420   foreach ( $self->cust_bill_pay ) {
1421
1422     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1423
1424     push @b, {
1425       'description' => "Payment received ".
1426                        time2str("%x",$_->cust_pay->_date ),
1427       'amount'      => sprintf("%10.2f", $_->amount )
1428     };
1429   }
1430
1431   @b;
1432
1433 }
1434
1435 =back
1436
1437 =head1 BUGS
1438
1439 The delete method.
1440
1441 print_text formatting (and some logic :/) is in source, but needs to be
1442 slurped in from a file.  Also number of lines ($=).
1443
1444 =head1 SEE ALSO
1445
1446 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1447 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
1448 documentation.
1449
1450 =cut
1451
1452 1;
1453