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