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