eek, fix silly problem in invoice sending refactoring
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Date::Format;
7 use Text::Template 1.20;
8 use File::Temp 0.14;
9 use String::ShellQuote;
10 use HTML::Entities;
11 use Locale::Country;
12 use FS::UID qw( datasrc );
13 use FS::Record qw( qsearch qsearchs );
14 use FS::Misc qw( send_email send_fax );
15 use FS::cust_main;
16 use FS::cust_bill_pkg;
17 use FS::cust_credit;
18 use FS::cust_pay;
19 use FS::cust_pkg;
20 use FS::cust_credit_bill;
21 use FS::cust_pay_batch;
22 use FS::cust_bill_event;
23 use FS::part_pkg;
24 use FS::cust_bill_pay;
25 use FS::part_bill_event;
26
27 @ISA = qw( FS::Record );
28
29 $DEBUG = 0;
30
31 #ask FS::UID to run this stuff for us later
32 FS::UID->install_callback( sub { 
33   $conf = new FS::Conf;
34   $money_char = $conf->config('money_char') || '$';  
35 } );
36
37 =head1 NAME
38
39 FS::cust_bill - Object methods for cust_bill records
40
41 =head1 SYNOPSIS
42
43   use FS::cust_bill;
44
45   $record = new FS::cust_bill \%hash;
46   $record = new FS::cust_bill { 'column' => 'value' };
47
48   $error = $record->insert;
49
50   $error = $new_record->replace($old_record);
51
52   $error = $record->delete;
53
54   $error = $record->check;
55
56   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
57
58   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
59
60   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
61
62   @cust_pay_objects = $cust_bill->cust_pay;
63
64   $tax_amount = $record->tax;
65
66   @lines = $cust_bill->print_text;
67   @lines = $cust_bill->print_text $time;
68
69 =head1 DESCRIPTION
70
71 An FS::cust_bill object represents an invoice; a declaration that a customer
72 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
73 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
74 following fields are currently supported:
75
76 =over 4
77
78 =item invnum - primary key (assigned automatically for new invoices)
79
80 =item custnum - customer (see L<FS::cust_main>)
81
82 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
83 L<Time::Local> and L<Date::Parse> for conversion functions.
84
85 =item charged - amount of this invoice
86
87 =item printed - deprecated
88
89 =item closed - books closed flag, empty or `Y'
90
91 =back
92
93 =head1 METHODS
94
95 =over 4
96
97 =item new HASHREF
98
99 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
100 Invoices are normally created by calling the bill method of a customer object
101 (see L<FS::cust_main>).
102
103 =cut
104
105 sub table { 'cust_bill'; }
106
107 =item insert
108
109 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
110 returns the error, otherwise returns false.
111
112 =item delete
113
114 Currently unimplemented.  I don't remove invoices because there would then be
115 no record you ever posted this invoice (which is bad, no?)
116
117 =cut
118
119 sub delete {
120   my $self = shift;
121   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
122   $self->SUPER::delete(@_);
123 }
124
125 =item replace OLD_RECORD
126
127 Replaces the OLD_RECORD with this one in the database.  If there is an error,
128 returns the error, otherwise returns false.
129
130 Only printed may be changed.  printed is normally updated by calling the
131 collect method of a customer object (see L<FS::cust_main>).
132
133 =cut
134
135 sub replace {
136   my( $new, $old ) = ( shift, shift );
137   return "Can't change custnum!" unless $old->custnum == $new->custnum;
138   #return "Can't change _date!" unless $old->_date eq $new->_date;
139   return "Can't change _date!" unless $old->_date == $new->_date;
140   return "Can't change charged!" unless $old->charged == $new->charged;
141
142   $new->SUPER::replace($old);
143 }
144
145 =item check
146
147 Checks all fields to make sure this is a valid invoice.  If there is an error,
148 returns the error, otherwise returns false.  Called by the insert and replace
149 methods.
150
151 =cut
152
153 sub check {
154   my $self = shift;
155
156   my $error =
157     $self->ut_numbern('invnum')
158     || $self->ut_number('custnum')
159     || $self->ut_numbern('_date')
160     || $self->ut_money('charged')
161     || $self->ut_numbern('printed')
162     || $self->ut_enum('closed', [ '', 'Y' ])
163   ;
164   return $error if $error;
165
166   return "Unknown customer"
167     unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
168
169   $self->_date(time) unless $self->_date;
170
171   $self->printed(0) if $self->printed eq '';
172
173   $self->SUPER::check;
174 }
175
176 =item previous
177
178 Returns a list consisting of the total previous balance for this customer, 
179 followed by the previous outstanding invoices (as FS::cust_bill objects also).
180
181 =cut
182
183 sub previous {
184   my $self = shift;
185   my $total = 0;
186   my @cust_bill = sort { $a->_date <=> $b->_date }
187     grep { $_->owed != 0 && $_->_date < $self->_date }
188       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
189   ;
190   foreach ( @cust_bill ) { $total += $_->owed; }
191   $total, @cust_bill;
192 }
193
194 =item cust_bill_pkg
195
196 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
197
198 =cut
199
200 sub cust_bill_pkg {
201   my $self = shift;
202   qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
203 }
204
205 =item cust_bill_event
206
207 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
208 invoice.
209
210 =cut
211
212 sub cust_bill_event {
213   my $self = shift;
214   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
215 }
216
217
218 =item cust_main
219
220 Returns the customer (see L<FS::cust_main>) for this invoice.
221
222 =cut
223
224 sub cust_main {
225   my $self = shift;
226   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
227 }
228
229 =item cust_credit
230
231 Depreciated.  See the cust_credited method.
232
233  #Returns a list consisting of the total previous credited (see
234  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
235  #outstanding credits (FS::cust_credit objects).
236
237 =cut
238
239 sub cust_credit {
240   use Carp;
241   croak "FS::cust_bill->cust_credit depreciated; see ".
242         "FS::cust_bill->cust_credit_bill";
243   #my $self = shift;
244   #my $total = 0;
245   #my @cust_credit = sort { $a->_date <=> $b->_date }
246   #  grep { $_->credited != 0 && $_->_date < $self->_date }
247   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
248   #;
249   #foreach (@cust_credit) { $total += $_->credited; }
250   #$total, @cust_credit;
251 }
252
253 =item cust_pay
254
255 Depreciated.  See the cust_bill_pay method.
256
257 #Returns all payments (see L<FS::cust_pay>) for this invoice.
258
259 =cut
260
261 sub cust_pay {
262   use Carp;
263   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
264   #my $self = shift;
265   #sort { $a->_date <=> $b->_date }
266   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
267   #;
268 }
269
270 =item cust_bill_pay
271
272 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
273
274 =cut
275
276 sub cust_bill_pay {
277   my $self = shift;
278   sort { $a->_date <=> $b->_date }
279     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
280 }
281
282 =item cust_credited
283
284 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
285
286 =cut
287
288 sub cust_credited {
289   my $self = shift;
290   sort { $a->_date <=> $b->_date }
291     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
292   ;
293 }
294
295 =item tax
296
297 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
298
299 =cut
300
301 sub tax {
302   my $self = shift;
303   my $total = 0;
304   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
305                                              'pkgnum' => 0 } );
306   foreach (@taxlines) { $total += $_->setup; }
307   $total;
308 }
309
310 =item owed
311
312 Returns the amount owed (still outstanding) on this invoice, which is charged
313 minus all payment applications (see L<FS::cust_bill_pay>) and credit
314 applications (see L<FS::cust_credit_bill>).
315
316 =cut
317
318 sub owed {
319   my $self = shift;
320   my $balance = $self->charged;
321   $balance -= $_->amount foreach ( $self->cust_bill_pay );
322   $balance -= $_->amount foreach ( $self->cust_credited );
323   $balance = sprintf( "%.2f", $balance);
324   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
325   $balance;
326 }
327
328
329 =item generate_email PARAMHASH
330
331 PARAMHASH can contain the following:
332
333 =over 4
334
335 =item from       => sender address, required
336
337 =item tempate    => alternate template name, optional
338
339 =item print_text => text attachment arrayref, optional
340
341 =item subject    => email subject, optional
342
343 =back
344
345 Returns an argument list to be passed to L<FS::Misc::send_email>.
346
347 =cut
348
349 use MIME::Entity;
350
351 sub generate_email {
352
353   my $self = shift;
354   my %args = @_;
355
356   my $me = '[FS::cust_bill::generate_email]';
357
358   my %return = (
359     'from'      => $args{'from'},
360     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
361   );
362
363   if (ref($args{'to'} eq 'ARRAY')) {
364     $return{'to'} = $args{'to'};
365   } else {
366     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
367                            $self->cust_main->invoicing_list
368                     ];
369   }
370
371   if ( $conf->exists('invoice_html') ) {
372
373     warn "$me creating HTML/text multipart message"
374       if $DEBUG;
375
376     $return{'nobody'} = 1;
377
378     my $alternative = build MIME::Entity
379       'Type'        => 'multipart/alternative',
380       'Encoding'    => '7bit',
381       'Disposition' => 'inline'
382     ;
383
384     my $data;
385     if ( $conf->exists('invoice_email_pdf')
386          and scalar($conf->config('invoice_email_pdf_note')) ) {
387
388       warn "$me using 'invoice_email_pdf_note' in multipart message"
389         if $DEBUG;
390       $data = [ map { $_ . "\n" }
391                     $conf->config('invoice_email_pdf_note')
392               ];
393
394     } else {
395
396       warn "$me not using 'invoice_email_pdf_note' in multipart message"
397         if $DEBUG;
398       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
399         $data = $args{'print_text'};
400       } else {
401         $data = [ $self->print_text('', $args{'template'}) ];
402       }
403
404     }
405
406     $alternative->attach(
407       'Type'        => 'text/plain',
408       #'Encoding'    => 'quoted-printable',
409       'Encoding'    => '7bit',
410       'Data'        => $data,
411       'Disposition' => 'inline',
412     );
413
414     $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
415     my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
416
417     my $image = build MIME::Entity
418       'Type'       => 'image/png',
419       'Encoding'   => 'base64',
420       'Path'       => "$FS::UID::conf_dir/conf.$FS::UID::datasrc/logo.png",
421       'Filename'   => 'logo.png',
422       'Content-ID' => "<$content_id>",
423     ;
424
425     $alternative->attach(
426       'Type'        => 'text/html',
427       'Encoding'    => 'quoted-printable',
428       'Data'        => [ '<html>',
429                          '  <head>',
430                          '    <title>',
431                          '      '. encode_entities($return{'subject'}), 
432                          '    </title>',
433                          '  </head>',
434                          '  <body bgcolor="#e8e8e8">',
435                          $self->print_html('', $args{'template'}, $content_id),
436                          '  </body>',
437                          '</html>',
438                        ],
439       'Disposition' => 'inline',
440       #'Filename'    => 'invoice.pdf',
441     );
442
443     if ( $conf->exists('invoice_email_pdf') ) {
444
445       #attaching pdf too:
446       # multipart/mixed
447       #   multipart/related
448       #     multipart/alternative
449       #       text/plain
450       #       text/html
451       #     image/png
452       #   application/pdf
453
454       my $related = build MIME::Entity 'Type'     => 'multipart/related',
455                                        'Encoding' => '7bit';
456
457       #false laziness w/Misc::send_email
458       $related->head->replace('Content-type',
459         $related->mime_type.
460         '; boundary="'. $related->head->multipart_boundary. '"'.
461         '; type=multipart/alternative'
462       );
463
464       $related->add_part($alternative);
465
466       $related->add_part($image);
467
468       my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
469
470       $return{'mimeparts'} = [ $related, $pdf ];
471
472     } else {
473
474       #no other attachment:
475       # multipart/related
476       #   multipart/alternative
477       #     text/plain
478       #     text/html
479       #   image/png
480
481       $return{'content-type'} = 'multipart/related';
482       $return{'mimeparts'} = [ $alternative, $image ];
483       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
484       #$return{'disposition'} = 'inline';
485
486     }
487   
488   } else {
489
490     if ( $conf->exists('invoice_email_pdf') ) {
491       warn "$me creating PDF attachment"
492         if $DEBUG;
493
494       #mime parts arguments a la MIME::Entity->build().
495       $return{'mimeparts'} = [
496         { $self->mimebuild_pdf('', $args{'template'}) }
497       ];
498     }
499   
500     if ( $conf->exists('invoice_email_pdf')
501          and scalar($conf->config('invoice_email_pdf_note')) ) {
502
503       warn "$me using 'invoice_email_pdf_note'"
504         if $DEBUG;
505       $return{'body'} = [ map { $_ . "\n" }
506                               $conf->config('invoice_email_pdf_note')
507                         ];
508
509     } else {
510
511       warn "$me not using 'invoice_email_pdf_note'"
512         if $DEBUG;
513       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
514         $return{'body'} = $args{'print_text'};
515       } else {
516         $return{'body'} = [ $self->print_text('', $args{'template'}) ];
517       }
518
519     }
520
521   }
522
523   %return;
524
525 }
526
527 =item mimebuild_pdf
528
529 Returns a list suitable for passing to MIME::Entity->build(), representing
530 this invoice as PDF attachment.
531
532 =cut
533
534 sub mimebuild_pdf {
535   my $self = shift;
536   (
537     'Type'        => 'application/pdf',
538     'Encoding'    => 'base64',
539     'Data'        => [ $self->print_pdf(@_) ],
540     'Disposition' => 'attachment',
541     'Filename'    => 'invoice.pdf',
542   );
543 }
544
545 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
546
547 Sends this invoice to the destinations configured for this customer: send
548 emails or print.  See L<FS::cust_main_invoice>.
549
550 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
551
552 AGENTNUM, if specified, means that this invoice will only be sent for customers
553 of the specified agent.
554
555 INVOICE_FROM, if specified, overrides the default email invoice From: address.
556
557 =cut
558
559 sub send {
560   my $self = shift;
561   my $template = scalar(@_) ? shift : '';
562   return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
563
564   my $invoice_from =
565     scalar(@_)
566       ? shift
567       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
568
569   my @invoicing_list = $self->cust_main->invoicing_list;
570
571   $self->email($template, $invoice_from)
572     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
573
574   $self->print($template)
575     if grep { $_ eq 'POST' } @invoicing_list; #postal
576
577   $self->fax($template)
578     if grep { $_ eq 'FAX' } @invoicing_list; #fax
579
580   '';
581
582 }
583
584 =item email [ TEMPLATENAME  [ , INVOICE_FROM ] ] 
585
586 Emails this invoice.
587
588 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
589
590 INVOICE_FROM, if specified, overrides the default email invoice From: address.
591
592 =cut
593
594 sub email {
595   my $self = shift;
596   my $template = scalar(@_) ? shift : '';
597   my $invoice_from =
598     scalar(@_)
599       ? shift
600       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
601
602   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
603                             $self->cust_main->invoicing_list;
604
605   #better to notify this person than silence
606   @invoicing_list = ($invoice_from) unless @invoicing_list;
607
608   my $error = send_email(
609     $self->generate_email(
610       'from'       => $invoice_from,
611       'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
612       'template'   => $template,
613     )
614   );
615   die "can't email invoice: $error\n" if $error;
616   #die "$error\n" if $error;
617
618 }
619
620 =item lpr_data [ TEMPLATENAME ]
621
622 Returns the postscript or plaintext for this invoice.
623
624 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
625
626 =cut
627
628 sub lpr_data {
629   my( $self, $template) = @_;
630   $conf->exists('invoice_latex')
631     ? [ $self->print_ps('', $template) ]
632     : [ $self->print_text('', $template) ];
633 }
634
635 =item print [ TEMPLATENAME ]
636
637 Prints this invoice.
638
639 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
640
641 =cut
642
643 sub print {
644   my $self = shift;
645   my $template = scalar(@_) ? shift : '';
646
647   my $lpr = $conf->config('lpr');
648   open(LPR, "|$lpr")
649     or die "Can't open pipe to $lpr: $!\n";
650   print LPR @{ $self->lpr_data($template) };
651   close LPR
652     or die $! ? "Error closing $lpr: $!\n"
653               : "Exit status $? from $lpr\n";
654 }
655
656 =item fax [ TEMPLATENAME ] 
657
658 Faxes this invoice.
659
660 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
661
662 =cut
663
664 sub fax {
665   my $self = shift;
666   my $template = scalar(@_) ? shift : '';
667
668   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
669     unless $conf->exists('invoice_latex');
670
671   my $dialstring = $self->cust_main->getfield('fax');
672   #Check $dialstring?
673
674   my $error = send_fax( 'docdata'    => $self->lpr_data($template),
675                         'dialstring' => $dialstring,
676                       );
677   die $error if $error;
678
679 }
680
681 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
682
683 Like B<send>, but only sends the invoice if it is the newest open invoice for
684 this customer.
685
686 =cut
687
688 sub send_if_newest {
689   my $self = shift;
690
691   return ''
692     if scalar(
693                grep { $_->owed > 0 } 
694                     qsearch('cust_bill', {
695                       'custnum' => $self->custnum,
696                       #'_date'   => { op=>'>', value=>$self->_date },
697                       'invnum'  => { op=>'>', value=>$self->invnum },
698                     } )
699              );
700     
701   $self->send(@_);
702 }
703
704 =item send_csv OPTIONS
705
706 Sends invoice as a CSV data-file to a remote host with the specified protocol.
707
708 Options are:
709
710 protocol - currently only "ftp"
711 server
712 username
713 password
714 dir
715
716 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
717 and YYMMDDHHMMSS is a timestamp.
718
719 The fields of the CSV file is as follows:
720
721 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
722
723 =over 4
724
725 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
726
727 If B<record_type> is C<cust_bill>, this is a primary invoice record.  The
728 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
729 fields are filled in.
730
731 If B<record_type> is C<cust_bill_pkg>, this is a line item record.  Only the
732 first two fields (B<record_type> and B<invnum>) and the last five fields
733 (B<pkg> through B<edate>) are filled in.
734
735 =item invnum - invoice number
736
737 =item custnum - customer number
738
739 =item _date - invoice date
740
741 =item charged - total invoice amount
742
743 =item first - customer first name
744
745 =item last - customer first name
746
747 =item company - company name
748
749 =item address1 - address line 1
750
751 =item address2 - address line 1
752
753 =item city
754
755 =item state
756
757 =item zip
758
759 =item country
760
761 =item pkg - line item description
762
763 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
764
765 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
766
767 =item sdate - start date for recurring fee
768
769 =item edate - end date for recurring fee
770
771 =back
772
773 =cut
774
775 sub send_csv {
776   my($self, %opt) = @_;
777
778   #part one: create file
779
780   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
781   mkdir $spooldir, 0700 unless -d $spooldir;
782
783   my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
784
785   open(CSV, ">$file") or die "can't open $file: $!";
786
787   eval "use Text::CSV_XS";
788   die $@ if $@;
789
790   my $csv = Text::CSV_XS->new({'always_quote'=>1});
791
792   my $cust_main = $self->cust_main;
793
794   $csv->combine(
795     'cust_bill',
796     $self->invnum,
797     $self->custnum,
798     time2str("%x", $self->_date),
799     sprintf("%.2f", $self->charged),
800     ( map { $cust_main->getfield($_) }
801         qw( first last company address1 address2 city state zip country ) ),
802     map { '' } (1..5),
803   ) or die "can't create csv";
804   print CSV $csv->string. "\n";
805
806   #new charges (false laziness w/print_text)
807   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
808
809     my($pkg, $setup, $recur, $sdate, $edate);
810     if ( $cust_bill_pkg->pkgnum ) {
811     
812       ($pkg, $setup, $recur, $sdate, $edate) = (
813         $cust_bill_pkg->cust_pkg->part_pkg->pkg,
814         ( $cust_bill_pkg->setup != 0
815           ? sprintf("%.2f", $cust_bill_pkg->setup )
816           : '' ),
817         ( $cust_bill_pkg->recur != 0
818           ? sprintf("%.2f", $cust_bill_pkg->recur )
819           : '' ),
820         time2str("%x", $cust_bill_pkg->sdate),
821         time2str("%x", $cust_bill_pkg->edate),
822       );
823
824     } else { #pkgnum tax
825       next unless $cust_bill_pkg->setup != 0;
826       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
827                        ? ( $cust_bill_pkg->itemdesc || 'Tax' )
828                        : 'Tax';
829       ($pkg, $setup, $recur, $sdate, $edate) =
830         ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
831     }
832
833     $csv->combine(
834       'cust_bill_pkg',
835       $self->invnum,
836       ( map { '' } (1..11) ),
837       ($pkg, $setup, $recur, $sdate, $edate)
838     ) or die "can't create csv";
839     print CSV $csv->string. "\n";
840
841   }
842
843   close CSV or die "can't close CSV: $!";
844
845   #part two: upload it
846
847   my $net;
848   if ( $opt{protocol} eq 'ftp' ) {
849     eval "use Net::FTP;";
850     die $@ if $@;
851     $net = Net::FTP->new($opt{server}) or die @$;
852   } else {
853     die "unknown protocol: $opt{protocol}";
854   }
855
856   $net->login( $opt{username}, $opt{password} )
857     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
858
859   $net->binary or die "can't set binary mode";
860
861   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
862
863   $net->put($file) or die "can't put $file: $!";
864
865   $net->quit;
866
867   unlink $file;
868
869 }
870
871 =item comp
872
873 Pays this invoice with a compliemntary payment.  If there is an error,
874 returns the error, otherwise returns false.
875
876 =cut
877
878 sub comp {
879   my $self = shift;
880   my $cust_pay = new FS::cust_pay ( {
881     'invnum'   => $self->invnum,
882     'paid'     => $self->owed,
883     '_date'    => '',
884     'payby'    => 'COMP',
885     'payinfo'  => $self->cust_main->payinfo,
886     'paybatch' => '',
887   } );
888   $cust_pay->insert;
889 }
890
891 =item realtime_card
892
893 Attempts to pay this invoice with a credit card payment via a
894 Business::OnlinePayment realtime gateway.  See
895 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
896 for supported processors.
897
898 =cut
899
900 sub realtime_card {
901   my $self = shift;
902   $self->realtime_bop( 'CC', @_ );
903 }
904
905 =item realtime_ach
906
907 Attempts to pay this invoice with an electronic check (ACH) payment via a
908 Business::OnlinePayment realtime gateway.  See
909 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
910 for supported processors.
911
912 =cut
913
914 sub realtime_ach {
915   my $self = shift;
916   $self->realtime_bop( 'ECHECK', @_ );
917 }
918
919 =item realtime_lec
920
921 Attempts to pay this invoice with phone bill (LEC) payment via a
922 Business::OnlinePayment realtime gateway.  See
923 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
924 for supported processors.
925
926 =cut
927
928 sub realtime_lec {
929   my $self = shift;
930   $self->realtime_bop( 'LEC', @_ );
931 }
932
933 sub realtime_bop {
934   my( $self, $method ) = @_;
935
936   my $cust_main = $self->cust_main;
937   my $balance = $cust_main->balance;
938   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
939   $amount = sprintf("%.2f", $amount);
940   return "not run (balance $balance)" unless $amount > 0;
941
942   my $description = 'Internet Services';
943   if ( $conf->exists('business-onlinepayment-description') ) {
944     my $dtempl = $conf->config('business-onlinepayment-description');
945
946     my $agent_obj = $cust_main->agent
947       or die "can't retreive agent for $cust_main (agentnum ".
948              $cust_main->agentnum. ")";
949     my $agent = $agent_obj->agent;
950     my $pkgs = join(', ',
951       map { $_->cust_pkg->part_pkg->pkg }
952         grep { $_->pkgnum } $self->cust_bill_pkg
953     );
954     $description = eval qq("$dtempl");
955   }
956
957   $cust_main->realtime_bop($method, $amount,
958     'description' => $description,
959     'invnum'      => $self->invnum,
960   );
961
962 }
963
964 =item batch_card
965
966 Adds a payment for this invoice to the pending credit card batch (see
967 L<FS::cust_pay_batch>).
968
969 =cut
970
971 sub batch_card {
972   my $self = shift;
973   my $cust_main = $self->cust_main;
974
975   my $cust_pay_batch = new FS::cust_pay_batch ( {
976     'invnum'   => $self->getfield('invnum'),
977     'custnum'  => $cust_main->getfield('custnum'),
978     'last'     => $cust_main->getfield('last'),
979     'first'    => $cust_main->getfield('first'),
980     'address1' => $cust_main->getfield('address1'),
981     'address2' => $cust_main->getfield('address2'),
982     'city'     => $cust_main->getfield('city'),
983     'state'    => $cust_main->getfield('state'),
984     'zip'      => $cust_main->getfield('zip'),
985     'country'  => $cust_main->getfield('country'),
986     'cardnum'  => $cust_main->payinfo,
987     'exp'      => $cust_main->getfield('paydate'),
988     'payname'  => $cust_main->getfield('payname'),
989     'amount'   => $self->owed,
990   } );
991   my $error = $cust_pay_batch->insert;
992   die $error if $error;
993
994   '';
995 }
996
997 sub _agent_template {
998   my $self = shift;
999   $self->_agent_plandata('agent_templatename');
1000 }
1001
1002 sub _agent_invoice_from {
1003   my $self = shift;
1004   $self->_agent_plandata('agent_invoice_from');
1005 }
1006
1007 sub _agent_plandata {
1008   my( $self, $option ) = @_;
1009
1010   my $part_bill_event = qsearchs( 'part_bill_event',
1011     {
1012       'payby'     => $self->cust_main->payby,
1013       'plan'      => 'send_agent',
1014       'plandata'  => { 'op'    => '~',
1015                        'value' => "(^|\n)agentnum ".
1016                                   $self->cust_main->agentnum.
1017                                   "(\n|\$)",
1018                      },
1019     },
1020     '',
1021     'ORDER BY seconds LIMIT 1'
1022   );
1023
1024   return '' unless $part_bill_event;
1025
1026   if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1027     return $1;
1028   } else {
1029     warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1030          " plandata for $option";
1031     return '';
1032   }
1033
1034 }
1035
1036 =item print_text [ TIME [ , TEMPLATE ] ]
1037
1038 Returns an text invoice, as a list of lines.
1039
1040 TIME an optional value used to control the printing of overdue messages.  The
1041 default is now.  It isn't the date of the invoice; that's the `_date' field.
1042 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1043 L<Time::Local> and L<Date::Parse> for conversion functions.
1044
1045 =cut
1046
1047 #still some false laziness w/print_text
1048 sub print_text {
1049
1050   my( $self, $today, $template ) = @_;
1051   $today ||= time;
1052
1053 #  my $invnum = $self->invnum;
1054   my $cust_main = $self->cust_main;
1055   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1056     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1057
1058   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1059 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1060   #my $balance_due = $self->owed + $pr_total - $cr_total;
1061   my $balance_due = $self->owed + $pr_total;
1062
1063   #my @collect = ();
1064   #my($description,$amount);
1065   @buf = ();
1066
1067   #previous balance
1068   foreach ( @pr_cust_bill ) {
1069     push @buf, [
1070       "Previous Balance, Invoice #". $_->invnum. 
1071                  " (". time2str("%x",$_->_date). ")",
1072       $money_char. sprintf("%10.2f",$_->owed)
1073     ];
1074   }
1075   if (@pr_cust_bill) {
1076     push @buf,['','-----------'];
1077     push @buf,[ 'Total Previous Balance',
1078                 $money_char. sprintf("%10.2f",$pr_total ) ];
1079     push @buf,['',''];
1080   }
1081
1082   #new charges
1083   foreach my $cust_bill_pkg (
1084     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1085     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1086   ) {
1087
1088     if ( $cust_bill_pkg->pkgnum > 0 ) {
1089
1090       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1091       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1092       my $pkg = $part_pkg->pkg;
1093
1094       if ( $cust_bill_pkg->setup != 0 ) {
1095         my $description = $pkg;
1096         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1097         push @buf, [ $description,
1098                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1099         push @buf,
1100           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1101               $cust_pkg->h_labels($self->_date);
1102       }
1103
1104       if ( $cust_bill_pkg->recur != 0 ) {
1105         push @buf, [
1106           "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1107                                 time2str("%x", $cust_bill_pkg->edate) . ")",
1108           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1109         ];
1110         push @buf,
1111           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1112               $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
1113       }
1114
1115       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1116
1117     } else { #pkgnum tax or one-shot line item
1118       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1119                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1120                      : 'Tax';
1121       if ( $cust_bill_pkg->setup != 0 ) {
1122         push @buf, [ $itemdesc,
1123                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1124       }
1125       if ( $cust_bill_pkg->recur != 0 ) {
1126         push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1127                                   . time2str("%x", $cust_bill_pkg->edate). ")",
1128                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1129                    ];
1130       }
1131     }
1132   }
1133
1134   push @buf,['','-----------'];
1135   push @buf,['Total New Charges',
1136              $money_char. sprintf("%10.2f",$self->charged) ];
1137   push @buf,['',''];
1138
1139   push @buf,['','-----------'];
1140   push @buf,['Total Charges',
1141              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1142   push @buf,['',''];
1143
1144   #credits
1145   foreach ( $self->cust_credited ) {
1146
1147     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1148
1149     my $reason = substr($_->cust_credit->reason,0,32);
1150     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1151     $reason = " ($reason) " if $reason;
1152     push @buf,[
1153       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1154         $reason,
1155       $money_char. sprintf("%10.2f",$_->amount)
1156     ];
1157   }
1158   #foreach ( @cr_cust_credit ) {
1159   #  push @buf,[
1160   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1161   #    $money_char. sprintf("%10.2f",$_->credited)
1162   #  ];
1163   #}
1164
1165   #get & print payments
1166   foreach ( $self->cust_bill_pay ) {
1167
1168     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1169
1170     push @buf,[
1171       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1172       $money_char. sprintf("%10.2f",$_->amount )
1173     ];
1174   }
1175
1176   #balance due
1177   my $balance_due_msg = $self->balance_due_msg;
1178
1179   push @buf,['','-----------'];
1180   push @buf,[$balance_due_msg, $money_char. 
1181     sprintf("%10.2f", $balance_due ) ];
1182
1183   #create the template
1184   $template ||= $self->_agent_template;
1185   my $templatefile = 'invoice_template';
1186   $templatefile .= "_$template" if length($template);
1187   my @invoice_template = $conf->config($templatefile)
1188     or die "cannot load config file $templatefile";
1189   $invoice_lines = 0;
1190   my $wasfunc = 0;
1191   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1192     /invoice_lines\((\d*)\)/;
1193     $invoice_lines += $1 || scalar(@buf);
1194     $wasfunc=1;
1195   }
1196   die "no invoice_lines() functions in template?" unless $wasfunc;
1197   my $invoice_template = new Text::Template (
1198     TYPE   => 'ARRAY',
1199     SOURCE => [ map "$_\n", @invoice_template ],
1200   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1201   $invoice_template->compile()
1202     or die "can't compile template: $Text::Template::ERROR";
1203
1204   #setup template variables
1205   package FS::cust_bill::_template; #!
1206   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1207
1208   $invnum = $self->invnum;
1209   $date = $self->_date;
1210   $page = 1;
1211   $agent = $self->cust_main->agent->agent;
1212
1213   if ( $FS::cust_bill::invoice_lines ) {
1214     $total_pages =
1215       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1216     $total_pages++
1217       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1218   } else {
1219     $total_pages = 1;
1220   }
1221
1222   #format address (variable for the template)
1223   my $l = 0;
1224   @address = ( '', '', '', '', '', '' );
1225   package FS::cust_bill; #!
1226   $FS::cust_bill::_template::address[$l++] =
1227     $cust_main->payname.
1228       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1229         ? " (P.O. #". $cust_main->payinfo. ")"
1230         : ''
1231       )
1232   ;
1233   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1234     if $cust_main->company;
1235   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1236   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1237     if $cust_main->address2;
1238   $FS::cust_bill::_template::address[$l++] =
1239     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1240
1241   my $countrydefault = $conf->config('countrydefault') || 'US';
1242   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1243     unless $cust_main->country eq $countrydefault;
1244
1245         #  #overdue? (variable for the template)
1246         #  $FS::cust_bill::_template::overdue = ( 
1247         #    $balance_due > 0
1248         #    && $today > $self->_date 
1249         ##    && $self->printed > 1
1250         #    && $self->printed > 0
1251         #  );
1252
1253   #and subroutine for the template
1254   sub FS::cust_bill::_template::invoice_lines {
1255     my $lines = shift || scalar(@buf);
1256     map { 
1257       scalar(@buf) ? shift @buf : [ '', '' ];
1258     }
1259     ( 1 .. $lines );
1260   }
1261
1262   #and fill it in
1263   $FS::cust_bill::_template::page = 1;
1264   my $lines;
1265   my @collect;
1266   while (@buf) {
1267     push @collect, split("\n",
1268       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1269     );
1270     $FS::cust_bill::_template::page++;
1271   }
1272
1273   map "$_\n", @collect;
1274
1275 }
1276
1277 =item print_latex [ TIME [ , TEMPLATE ] ]
1278
1279 Internal method - returns a filename of a filled-in LaTeX template for this
1280 invoice (Note: add ".tex" to get the actual filename).
1281
1282 See print_ps and print_pdf for methods that return PostScript and PDF output.
1283
1284 TIME an optional value used to control the printing of overdue messages.  The
1285 default is now.  It isn't the date of the invoice; that's the `_date' field.
1286 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1287 L<Time::Local> and L<Date::Parse> for conversion functions.
1288
1289 =cut
1290
1291 #still some false laziness w/print_text
1292 sub print_latex {
1293
1294   my( $self, $today, $template ) = @_;
1295   $today ||= time;
1296   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1297     if $DEBUG;
1298
1299   my $cust_main = $self->cust_main;
1300   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1301     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1302
1303   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1304 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1305   #my $balance_due = $self->owed + $pr_total - $cr_total;
1306   my $balance_due = $self->owed + $pr_total;
1307
1308   #create the template
1309   $template ||= $self->_agent_template;
1310   my $templatefile = 'invoice_latex';
1311   my $suffix = length($template) ? "_$template" : '';
1312   $templatefile .= $suffix;
1313   my @invoice_template = map "$_\n", $conf->config($templatefile)
1314     or die "cannot load config file $templatefile";
1315
1316   my($format, $text_template);
1317   if ( grep { /^%%Detail/ } @invoice_template ) {
1318     #change this to a die when the old code is removed
1319     warn "old-style invoice template $templatefile; ".
1320          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1321     $format = 'old';
1322   } else {
1323     $format = 'Text::Template';
1324     $text_template = new Text::Template(
1325       TYPE => 'ARRAY',
1326       SOURCE => \@invoice_template,
1327       DELIMITERS => [ '[@--', '--@]' ],
1328     );
1329
1330     $text_template->compile()
1331       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1332   }
1333
1334   my $returnaddress;
1335   if ( $conf->exists('invoice_latexreturnaddress')
1336        && length($conf->exists('invoice_latexreturnaddress'))
1337      )
1338   {
1339     $returnaddress = join("\n", $conf->config('invoice_latexreturnaddress') );
1340   } else {
1341     $returnaddress = '~';
1342   }
1343
1344   my %invoice_data = (
1345     'invnum'       => $self->invnum,
1346     'date'         => time2str('%b %o, %Y', $self->_date),
1347     'today'        => time2str('%b %o, %Y', $today),
1348     'agent'        => _latex_escape($cust_main->agent->agent),
1349     'payname'      => _latex_escape($cust_main->payname),
1350     'company'      => _latex_escape($cust_main->company),
1351     'address1'     => _latex_escape($cust_main->address1),
1352     'address2'     => _latex_escape($cust_main->address2),
1353     'city'         => _latex_escape($cust_main->city),
1354     'state'        => _latex_escape($cust_main->state),
1355     'zip'          => _latex_escape($cust_main->zip),
1356     'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
1357     'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
1358     'returnaddress' => $returnaddress,
1359     'quantity'     => 1,
1360     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1361     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1362     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1363   );
1364
1365   my $countrydefault = $conf->config('countrydefault') || 'US';
1366   if ( $cust_main->country eq $countrydefault ) {
1367     $invoice_data{'country'} = '';
1368   } else {
1369     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1370   }
1371
1372   $invoice_data{'notes'} =
1373     join("\n",
1374 #  #do variable substitutions in notes
1375 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1376         $conf->config_orbase('invoice_latexnotes', $template)
1377     );
1378   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1379     if $DEBUG;
1380
1381   $invoice_data{'footer'} =~ s/\n+$//;
1382   $invoice_data{'smallfooter'} =~ s/\n+$//;
1383   $invoice_data{'notes'} =~ s/\n+$//;
1384
1385   $invoice_data{'po_line'} =
1386     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1387       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1388       : '~';
1389
1390   my @filled_in = ();
1391   if ( $format eq 'old' ) {
1392   
1393     my @line_item = ();
1394     my @total_item = ();
1395     while ( @invoice_template ) {
1396       my $line = shift @invoice_template;
1397   
1398       if ( $line =~ /^%%Detail\s*$/ ) {
1399   
1400         while ( ( my $line_item_line = shift @invoice_template )
1401                 !~ /^%%EndDetail\s*$/                            ) {
1402           push @line_item, $line_item_line;
1403         }
1404         foreach my $line_item ( $self->_items ) {
1405         #foreach my $line_item ( $self->_items_pkg ) {
1406           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1407           $invoice_data{'description'} =
1408             _latex_escape($line_item->{'description'});
1409           if ( exists $line_item->{'ext_description'} ) {
1410             $invoice_data{'description'} .=
1411               "\\tabularnewline\n~~".
1412               join( "\\tabularnewline\n~~",
1413                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1414                   );
1415           }
1416           $invoice_data{'amount'} = $line_item->{'amount'};
1417           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1418           push @filled_in,
1419             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1420         }
1421   
1422       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1423   
1424         while ( ( my $total_item_line = shift @invoice_template )
1425                 !~ /^%%EndTotalDetails\s*$/                      ) {
1426           push @total_item, $total_item_line;
1427         }
1428   
1429         my @total_fill = ();
1430   
1431         my $taxtotal = 0;
1432         foreach my $tax ( $self->_items_tax ) {
1433           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1434           $taxtotal += $tax->{'amount'};
1435           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1436           push @total_fill,
1437             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1438                 @total_item;
1439         }
1440
1441         if ( $taxtotal ) {
1442           $invoice_data{'total_item'} = 'Sub-total';
1443           $invoice_data{'total_amount'} =
1444             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1445           unshift @total_fill,
1446             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1447                 @total_item;
1448         }
1449   
1450         $invoice_data{'total_item'} = '\textbf{Total}';
1451         $invoice_data{'total_amount'} =
1452           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1453         push @total_fill,
1454           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1455               @total_item;
1456   
1457         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1458   
1459         # credits
1460         foreach my $credit ( $self->_items_credits ) {
1461           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1462           #$credittotal
1463           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1464           push @total_fill, 
1465             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1466                 @total_item;
1467         }
1468   
1469         # payments
1470         foreach my $payment ( $self->_items_payments ) {
1471           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1472           #$paymenttotal
1473           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1474           push @total_fill, 
1475             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1476                 @total_item;
1477         }
1478   
1479         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1480         $invoice_data{'total_amount'} =
1481           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1482         push @total_fill,
1483           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1484               @total_item;
1485   
1486         push @filled_in, @total_fill;
1487   
1488       } else {
1489         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1490         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1491         push @filled_in, $line;
1492       }
1493   
1494     }
1495
1496     sub nounder {
1497       my $var = $1;
1498       $var =~ s/_/\-/g;
1499       $var;
1500     }
1501
1502   } elsif ( $format eq 'Text::Template' ) {
1503
1504     my @detail_items = ();
1505     my @total_items = ();
1506
1507     $invoice_data{'detail_items'} = \@detail_items;
1508     $invoice_data{'total_items'} = \@total_items;
1509   
1510     foreach my $line_item ( $self->_items ) {
1511       my $detail = {
1512         ext_description => [],
1513       };
1514       $detail->{'ref'} = $line_item->{'pkgnum'};
1515       $detail->{'quantity'} = 1;
1516       $detail->{'description'} = _latex_escape($line_item->{'description'});
1517       if ( exists $line_item->{'ext_description'} ) {
1518         @{$detail->{'ext_description'}} = map {
1519           _latex_escape($_);
1520         } @{$line_item->{'ext_description'}};
1521       }
1522       $detail->{'amount'} = $line_item->{'amount'};
1523       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1524   
1525       push @detail_items, $detail;
1526     }
1527   
1528   
1529     my $taxtotal = 0;
1530     foreach my $tax ( $self->_items_tax ) {
1531       my $total = {};
1532       $total->{'total_item'} = _latex_escape($tax->{'description'});
1533       $taxtotal += $tax->{'amount'};
1534       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1535       push @total_items, $total;
1536     }
1537   
1538     if ( $taxtotal ) {
1539       my $total = {};
1540       $total->{'total_item'} = 'Sub-total';
1541       $total->{'total_amount'} =
1542         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1543       unshift @total_items, $total;
1544     }
1545   
1546     {
1547       my $total = {};
1548       $total->{'total_item'} = '\textbf{Total}';
1549       $total->{'total_amount'} =
1550         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1551       push @total_items, $total;
1552     }
1553   
1554     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1555   
1556     # credits
1557     foreach my $credit ( $self->_items_credits ) {
1558       my $total;
1559       $total->{'total_item'} = _latex_escape($credit->{'description'});
1560       #$credittotal
1561       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1562       push @total_items, $total;
1563     }
1564   
1565     # payments
1566     foreach my $payment ( $self->_items_payments ) {
1567       my $total = {};
1568       $total->{'total_item'} = _latex_escape($payment->{'description'});
1569       #$paymenttotal
1570       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1571       push @total_items, $total;
1572     }
1573   
1574     { 
1575       my $total;
1576       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1577       $total->{'total_amount'} =
1578         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1579       push @total_items, $total;
1580     }
1581
1582   } else {
1583     die "guru meditation #54";
1584   }
1585
1586   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1587   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1588                            DIR      => $dir,
1589                            SUFFIX   => '.tex',
1590                            UNLINK   => 0,
1591                          ) or die "can't open temp file: $!\n";
1592   if ( $format eq 'old' ) {
1593     print $fh join('', @filled_in );
1594   } elsif ( $format eq 'Text::Template' ) {
1595     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1596   } else {
1597     die "guru meditation #32";
1598   }
1599   close $fh;
1600
1601   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1602   return $1;
1603
1604 }
1605
1606 =item print_ps [ TIME [ , TEMPLATE ] ]
1607
1608 Returns an postscript invoice, as a scalar.
1609
1610 TIME an optional value used to control the printing of overdue messages.  The
1611 default is now.  It isn't the date of the invoice; that's the `_date' field.
1612 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1613 L<Time::Local> and L<Date::Parse> for conversion functions.
1614
1615 =cut
1616
1617 sub print_ps {
1618   my $self = shift;
1619
1620   my $file = $self->print_latex(@_);
1621
1622   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1623   chdir($dir);
1624
1625   my $sfile = shell_quote $file;
1626
1627   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1628     or die "pslatex $file.tex failed; see $file.log for details?\n";
1629   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1630     or die "pslatex $file.tex failed; see $file.log for details?\n";
1631
1632   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1633     or die "dvips failed";
1634
1635   open(POSTSCRIPT, "<$file.ps")
1636     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1637
1638   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1639
1640   my $ps = '';
1641   while (<POSTSCRIPT>) {
1642     $ps .= $_;
1643   }
1644
1645   close POSTSCRIPT;
1646
1647   return $ps;
1648
1649 }
1650
1651 =item print_pdf [ TIME [ , TEMPLATE ] ]
1652
1653 Returns an PDF invoice, as a scalar.
1654
1655 TIME an optional value used to control the printing of overdue messages.  The
1656 default is now.  It isn't the date of the invoice; that's the `_date' field.
1657 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1658 L<Time::Local> and L<Date::Parse> for conversion functions.
1659
1660 =cut
1661
1662 sub print_pdf {
1663   my $self = shift;
1664
1665   my $file = $self->print_latex(@_);
1666
1667   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1668   chdir($dir);
1669
1670   #system('pdflatex', "$file.tex");
1671   #system('pdflatex', "$file.tex");
1672   #! LaTeX Error: Unknown graphics extension: .eps.
1673
1674   my $sfile = shell_quote $file;
1675
1676   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1677     or die "pslatex $file.tex failed; see $file.log for details?\n";
1678   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1679     or die "pslatex $file.tex failed; see $file.log for details?\n";
1680
1681   #system('dvipdf', "$file.dvi", "$file.pdf" );
1682   system(
1683     "dvips -q -t letter -f $sfile.dvi ".
1684     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1685     "     -c save pop -"
1686   ) == 0
1687     or die "dvips | gs failed: $!";
1688
1689   open(PDF, "<$file.pdf")
1690     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1691
1692   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1693
1694   my $pdf = '';
1695   while (<PDF>) {
1696     $pdf .= $_;
1697   }
1698
1699   close PDF;
1700
1701   return $pdf;
1702
1703 }
1704
1705 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1706
1707 Returns an HTML invoice, as a scalar.
1708
1709 TIME an optional value used to control the printing of overdue messages.  The
1710 default is now.  It isn't the date of the invoice; that's the `_date' field.
1711 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1712 L<Time::Local> and L<Date::Parse> for conversion functions.
1713
1714 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1715 when emailing the invoice as part of a multipart/related MIME email.
1716
1717 =cut
1718
1719 sub print_html {
1720   my( $self, $today, $template, $cid ) = @_;
1721   $today ||= time;
1722
1723   my $cust_main = $self->cust_main;
1724   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1725     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1726
1727   $template ||= $self->_agent_template;
1728   my $templatefile = 'invoice_html';
1729   my $suffix = length($template) ? "_$template" : '';
1730   $templatefile .= $suffix;
1731   my @html_template = map "$_\n", $conf->config($templatefile)
1732     or die "cannot load config file $templatefile";
1733
1734   my $html_template = new Text::Template(
1735     TYPE   => 'ARRAY',
1736     SOURCE => \@html_template,
1737     DELIMITERS => [ '<%=', '%>' ],
1738   );
1739
1740   $html_template->compile()
1741     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1742
1743   my %invoice_data = (
1744     'invnum'       => $self->invnum,
1745     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
1746     'today'        => time2str('%b %o, %Y', $today),
1747     'agent'        => encode_entities($cust_main->agent->agent),
1748     'payname'      => encode_entities($cust_main->payname),
1749     'company'      => encode_entities($cust_main->company),
1750     'address1'     => encode_entities($cust_main->address1),
1751     'address2'     => encode_entities($cust_main->address2),
1752     'city'         => encode_entities($cust_main->city),
1753     'state'        => encode_entities($cust_main->state),
1754     'zip'          => encode_entities($cust_main->zip),
1755     'terms'        => $conf->config('invoice_default_terms')
1756                       || 'Payable upon receipt',
1757     'cid'          => $cid,
1758 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1759   );
1760
1761   $invoice_data{'returnaddress'} = $conf->exists('invoice_htmlreturnaddress')
1762     ? join("\n", $conf->config('invoice_htmlreturnaddress') )
1763     : join("\n", map { 
1764                        s/~/&nbsp;/g;
1765                        s/\\\\\*?\s*$/<BR>/;
1766                        s/\\hyphenation\{[\w\s\-]+\}//;
1767                        $_;
1768                      }
1769                      $conf->config('invoice_latexreturnaddress')
1770           );
1771
1772   my $countrydefault = $conf->config('countrydefault') || 'US';
1773   if ( $cust_main->country eq $countrydefault ) {
1774     $invoice_data{'country'} = '';
1775   } else {
1776     $invoice_data{'country'} =
1777       encode_entities(code2country($cust_main->country));
1778   }
1779
1780   $invoice_data{'notes'} =
1781     length($conf->config_orbase('invoice_htmlnotes', $template))
1782       ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1783       : join("\n", map { 
1784                          s/%%(.*)$/<!-- $1 -->/;
1785                          s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1786                          s/\\begin\{enumerate\}/<ol>/;
1787                          s/\\item /  <li>/;
1788                          s/\\end\{enumerate\}/<\/ol>/;
1789                          s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1790                          $_;
1791                        } 
1792                        $conf->config_orbase('invoice_latexnotes', $template)
1793             );
1794
1795 #  #do variable substitutions in notes
1796 #  $invoice_data{'notes'} =
1797 #    join("\n",
1798 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1799 #        $conf->config_orbase('invoice_latexnotes', $suffix)
1800 #    );
1801
1802    $invoice_data{'footer'} = $conf->exists('invoice_htmlfooter')
1803      ? join("\n", $conf->config('invoice_htmlfooter') )
1804      : join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
1805                       $conf->config('invoice_latexfooter')
1806            );
1807
1808   $invoice_data{'po_line'} =
1809     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1810       ? encode_entities("Purchase Order #". $cust_main->payinfo)
1811       : '';
1812
1813   my $money_char = $conf->config('money_char') || '$';
1814
1815   foreach my $line_item ( $self->_items ) {
1816     my $detail = {
1817       ext_description => [],
1818     };
1819     $detail->{'ref'} = $line_item->{'pkgnum'};
1820     $detail->{'description'} = encode_entities($line_item->{'description'});
1821     if ( exists $line_item->{'ext_description'} ) {
1822       @{$detail->{'ext_description'}} = map {
1823         encode_entities($_);
1824       } @{$line_item->{'ext_description'}};
1825     }
1826     $detail->{'amount'} = $money_char. $line_item->{'amount'};
1827     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1828
1829     push @{$invoice_data{'detail_items'}}, $detail;
1830   }
1831
1832
1833   my $taxtotal = 0;
1834   foreach my $tax ( $self->_items_tax ) {
1835     my $total = {};
1836     $total->{'total_item'} = encode_entities($tax->{'description'});
1837     $taxtotal += $tax->{'amount'};
1838     $total->{'total_amount'} = $money_char. $tax->{'amount'};
1839     push @{$invoice_data{'total_items'}}, $total;
1840   }
1841
1842   if ( $taxtotal ) {
1843     my $total = {};
1844     $total->{'total_item'} = 'Sub-total';
1845     $total->{'total_amount'} =
1846       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1847     unshift @{$invoice_data{'total_items'}}, $total;
1848   }
1849
1850   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1851   {
1852     my $total = {};
1853     $total->{'total_item'} = '<b>Total</b>';
1854     $total->{'total_amount'} =
1855       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1856     push @{$invoice_data{'total_items'}}, $total;
1857   }
1858
1859   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1860
1861   # credits
1862   foreach my $credit ( $self->_items_credits ) {
1863     my $total;
1864     $total->{'total_item'} = encode_entities($credit->{'description'});
1865     #$credittotal
1866     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1867     push @{$invoice_data{'total_items'}}, $total;
1868   }
1869
1870   # payments
1871   foreach my $payment ( $self->_items_payments ) {
1872     my $total = {};
1873     $total->{'total_item'} = encode_entities($payment->{'description'});
1874     #$paymenttotal
1875     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1876     push @{$invoice_data{'total_items'}}, $total;
1877   }
1878
1879   { 
1880     my $total;
1881     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1882     $total->{'total_amount'} =
1883       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1884     push @{$invoice_data{'total_items'}}, $total;
1885   }
1886
1887   $html_template->fill_in( HASH => \%invoice_data);
1888 }
1889
1890 # quick subroutine for print_latex
1891 #
1892 # There are ten characters that LaTeX treats as special characters, which
1893 # means that they do not simply typeset themselves: 
1894 #      # $ % & ~ _ ^ \ { }
1895 #
1896 # TeX ignores blanks following an escaped character; if you want a blank (as
1897 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1898
1899 sub _latex_escape {
1900   my $value = shift;
1901   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1902   $value =~ s/([<>])/\$$1\$/g;
1903   $value;
1904 }
1905
1906 #utility methods for print_*
1907
1908 sub balance_due_msg {
1909   my $self = shift;
1910   my $msg = 'Balance Due';
1911   return $msg unless $conf->exists('invoice_default_terms');
1912   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1913     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1914   } elsif ( $conf->config('invoice_default_terms') ) {
1915     $msg .= ' - '. $conf->config('invoice_default_terms');
1916   }
1917   $msg;
1918 }
1919
1920 sub _items {
1921   my $self = shift;
1922   my @display = scalar(@_)
1923                 ? @_
1924                 : qw( _items_previous _items_pkg );
1925                 #: qw( _items_pkg );
1926                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1927   my @b = ();
1928   foreach my $display ( @display ) {
1929     push @b, $self->$display(@_);
1930   }
1931   @b;
1932 }
1933
1934 sub _items_previous {
1935   my $self = shift;
1936   my $cust_main = $self->cust_main;
1937   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1938   my @b = ();
1939   foreach ( @pr_cust_bill ) {
1940     push @b, {
1941       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
1942                        ' ('. time2str('%x',$_->_date). ')',
1943       #'pkgpart'     => 'N/A',
1944       'pkgnum'      => 'N/A',
1945       'amount'      => sprintf("%.2f", $_->owed),
1946     };
1947   }
1948   @b;
1949
1950   #{
1951   #    'description'     => 'Previous Balance',
1952   #    #'pkgpart'         => 'N/A',
1953   #    'pkgnum'          => 'N/A',
1954   #    'amount'          => sprintf("%10.2f", $pr_total ),
1955   #    'ext_description' => [ map {
1956   #                                 "Invoice ". $_->invnum.
1957   #                                 " (". time2str("%x",$_->_date). ") ".
1958   #                                 sprintf("%10.2f", $_->owed)
1959   #                         } @pr_cust_bill ],
1960
1961   #};
1962 }
1963
1964 sub _items_pkg {
1965   my $self = shift;
1966   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1967   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1968 }
1969
1970 sub _items_tax {
1971   my $self = shift;
1972   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1973   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1974 }
1975
1976 sub _items_cust_bill_pkg {
1977   my $self = shift;
1978   my $cust_bill_pkg = shift;
1979
1980   my @b = ();
1981   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1982
1983     if ( $cust_bill_pkg->pkgnum > 0 ) {
1984
1985       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1986       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1987       my $pkg = $part_pkg->pkg;
1988
1989       if ( $cust_bill_pkg->setup != 0 ) {
1990         my $description = $pkg;
1991         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1992         my @d = $cust_pkg->h_labels_short($self->_date);
1993         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1994         push @b, {
1995           description     => $description,
1996           #pkgpart         => $part_pkg->pkgpart,
1997           pkgnum          => $cust_pkg->pkgnum,
1998           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
1999           ext_description => \@d,
2000         };
2001       }
2002
2003       if ( $cust_bill_pkg->recur != 0 ) {
2004         push @b, {
2005           description     => "$pkg (" .
2006                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
2007                                time2str('%x', $cust_bill_pkg->edate). ')',
2008           #pkgpart         => $part_pkg->pkgpart,
2009           pkgnum          => $cust_pkg->pkgnum,
2010           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2011           ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
2012                                                          $cust_bill_pkg->sdate),
2013                                $cust_bill_pkg->details,
2014                              ],
2015         };
2016       }
2017
2018     } else { #pkgnum tax or one-shot line item (??)
2019
2020       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
2021                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
2022                      : 'Tax';
2023       if ( $cust_bill_pkg->setup != 0 ) {
2024         push @b, {
2025           'description' => $itemdesc,
2026           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2027         };
2028       }
2029       if ( $cust_bill_pkg->recur != 0 ) {
2030         push @b, {
2031           'description' => "$itemdesc (".
2032                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2033                            time2str("%x", $cust_bill_pkg->edate). ')',
2034           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2035         };
2036       }
2037
2038     }
2039
2040   }
2041
2042   @b;
2043
2044 }
2045
2046 sub _items_credits {
2047   my $self = shift;
2048
2049   my @b;
2050   #credits
2051   foreach ( $self->cust_credited ) {
2052
2053     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2054
2055     my $reason = $_->cust_credit->reason;
2056     #my $reason = substr($_->cust_credit->reason,0,32);
2057     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2058     $reason = " ($reason) " if $reason;
2059     push @b, {
2060       #'description' => 'Credit ref\#'. $_->crednum.
2061       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2062       #                 $reason,
2063       'description' => 'Credit applied '.
2064                        time2str("%x",$_->cust_credit->_date). $reason,
2065       'amount'      => sprintf("%.2f",$_->amount),
2066     };
2067   }
2068   #foreach ( @cr_cust_credit ) {
2069   #  push @buf,[
2070   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2071   #    $money_char. sprintf("%10.2f",$_->credited)
2072   #  ];
2073   #}
2074
2075   @b;
2076
2077 }
2078
2079 sub _items_payments {
2080   my $self = shift;
2081
2082   my @b;
2083   #get & print payments
2084   foreach ( $self->cust_bill_pay ) {
2085
2086     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2087
2088     push @b, {
2089       'description' => "Payment received ".
2090                        time2str("%x",$_->cust_pay->_date ),
2091       'amount'      => sprintf("%.2f", $_->amount )
2092     };
2093   }
2094
2095   @b;
2096
2097 }
2098
2099 =back
2100
2101 =head1 BUGS
2102
2103 The delete method.
2104
2105 print_text formatting (and some logic :/) is in source, but needs to be
2106 slurped in from a file.  Also number of lines ($=).
2107
2108 =head1 SEE ALSO
2109
2110 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2111 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2112 documentation.
2113
2114 =cut
2115
2116 1;
2117