add multiple agent selection to agent-specific invoicing
[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 or agent(s).  AGENTNUM can be a scalar agentnum (for a
554 single agent) or an arrayref of agentnums.
555
556 INVOICE_FROM, if specified, overrides the default email invoice From: address.
557
558 =cut
559
560 sub send {
561   my $self = shift;
562   my $template = scalar(@_) ? shift : '';
563   if ( scalar(@_) && $_[0]  ) {
564     my $agentnums = ref($_[0]) ? shift : [ shift ];
565     return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
566   }
567
568   my $invoice_from =
569     scalar(@_)
570       ? shift
571       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
572
573   my @invoicing_list = $self->cust_main->invoicing_list;
574
575   $self->email($template, $invoice_from)
576     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
577
578   $self->print($template)
579     if grep { $_ eq 'POST' } @invoicing_list; #postal
580
581   $self->fax($template)
582     if grep { $_ eq 'FAX' } @invoicing_list; #fax
583
584   '';
585
586 }
587
588 =item email [ TEMPLATENAME  [ , INVOICE_FROM ] ] 
589
590 Emails this invoice.
591
592 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
593
594 INVOICE_FROM, if specified, overrides the default email invoice From: address.
595
596 =cut
597
598 sub email {
599   my $self = shift;
600   my $template = scalar(@_) ? shift : '';
601   my $invoice_from =
602     scalar(@_)
603       ? shift
604       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
605
606   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
607                             $self->cust_main->invoicing_list;
608
609   #better to notify this person than silence
610   @invoicing_list = ($invoice_from) unless @invoicing_list;
611
612   my $error = send_email(
613     $self->generate_email(
614       'from'       => $invoice_from,
615       'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
616       'template'   => $template,
617     )
618   );
619   die "can't email invoice: $error\n" if $error;
620   #die "$error\n" if $error;
621
622 }
623
624 =item lpr_data [ TEMPLATENAME ]
625
626 Returns the postscript or plaintext for this invoice.
627
628 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
629
630 =cut
631
632 sub lpr_data {
633   my( $self, $template) = @_;
634   $conf->exists('invoice_latex')
635     ? [ $self->print_ps('', $template) ]
636     : [ $self->print_text('', $template) ];
637 }
638
639 =item print [ TEMPLATENAME ]
640
641 Prints this invoice.
642
643 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
644
645 =cut
646
647 sub print {
648   my $self = shift;
649   my $template = scalar(@_) ? shift : '';
650
651   my $lpr = $conf->config('lpr');
652   open(LPR, "|$lpr")
653     or die "Can't open pipe to $lpr: $!\n";
654   print LPR @{ $self->lpr_data($template) };
655   close LPR
656     or die $! ? "Error closing $lpr: $!\n"
657               : "Exit status $? from $lpr\n";
658 }
659
660 =item fax [ TEMPLATENAME ] 
661
662 Faxes this invoice.
663
664 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
665
666 =cut
667
668 sub fax {
669   my $self = shift;
670   my $template = scalar(@_) ? shift : '';
671
672   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
673     unless $conf->exists('invoice_latex');
674
675   my $dialstring = $self->cust_main->getfield('fax');
676   #Check $dialstring?
677
678   my $error = send_fax( 'docdata'    => $self->lpr_data($template),
679                         'dialstring' => $dialstring,
680                       );
681   die $error if $error;
682
683 }
684
685 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
686
687 Like B<send>, but only sends the invoice if it is the newest open invoice for
688 this customer.
689
690 =cut
691
692 sub send_if_newest {
693   my $self = shift;
694
695   return ''
696     if scalar(
697                grep { $_->owed > 0 } 
698                     qsearch('cust_bill', {
699                       'custnum' => $self->custnum,
700                       #'_date'   => { op=>'>', value=>$self->_date },
701                       'invnum'  => { op=>'>', value=>$self->invnum },
702                     } )
703              );
704     
705   $self->send(@_);
706 }
707
708 =item send_csv OPTIONS
709
710 Sends invoice as a CSV data-file to a remote host with the specified protocol.
711
712 Options are:
713
714 protocol - currently only "ftp"
715 server
716 username
717 password
718 dir
719
720 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
721 and YYMMDDHHMMSS is a timestamp.
722
723 The fields of the CSV file is as follows:
724
725 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
726
727 =over 4
728
729 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
730
731 If B<record_type> is C<cust_bill>, this is a primary invoice record.  The
732 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
733 fields are filled in.
734
735 If B<record_type> is C<cust_bill_pkg>, this is a line item record.  Only the
736 first two fields (B<record_type> and B<invnum>) and the last five fields
737 (B<pkg> through B<edate>) are filled in.
738
739 =item invnum - invoice number
740
741 =item custnum - customer number
742
743 =item _date - invoice date
744
745 =item charged - total invoice amount
746
747 =item first - customer first name
748
749 =item last - customer first name
750
751 =item company - company name
752
753 =item address1 - address line 1
754
755 =item address2 - address line 1
756
757 =item city
758
759 =item state
760
761 =item zip
762
763 =item country
764
765 =item pkg - line item description
766
767 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
768
769 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
770
771 =item sdate - start date for recurring fee
772
773 =item edate - end date for recurring fee
774
775 =back
776
777 =cut
778
779 sub send_csv {
780   my($self, %opt) = @_;
781
782   #part one: create file
783
784   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
785   mkdir $spooldir, 0700 unless -d $spooldir;
786
787   my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
788
789   open(CSV, ">$file") or die "can't open $file: $!";
790
791   eval "use Text::CSV_XS";
792   die $@ if $@;
793
794   my $csv = Text::CSV_XS->new({'always_quote'=>1});
795
796   my $cust_main = $self->cust_main;
797
798   $csv->combine(
799     'cust_bill',
800     $self->invnum,
801     $self->custnum,
802     time2str("%x", $self->_date),
803     sprintf("%.2f", $self->charged),
804     ( map { $cust_main->getfield($_) }
805         qw( first last company address1 address2 city state zip country ) ),
806     map { '' } (1..5),
807   ) or die "can't create csv";
808   print CSV $csv->string. "\n";
809
810   #new charges (false laziness w/print_text)
811   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
812
813     my($pkg, $setup, $recur, $sdate, $edate);
814     if ( $cust_bill_pkg->pkgnum ) {
815     
816       ($pkg, $setup, $recur, $sdate, $edate) = (
817         $cust_bill_pkg->cust_pkg->part_pkg->pkg,
818         ( $cust_bill_pkg->setup != 0
819           ? sprintf("%.2f", $cust_bill_pkg->setup )
820           : '' ),
821         ( $cust_bill_pkg->recur != 0
822           ? sprintf("%.2f", $cust_bill_pkg->recur )
823           : '' ),
824         time2str("%x", $cust_bill_pkg->sdate),
825         time2str("%x", $cust_bill_pkg->edate),
826       );
827
828     } else { #pkgnum tax
829       next unless $cust_bill_pkg->setup != 0;
830       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
831                        ? ( $cust_bill_pkg->itemdesc || 'Tax' )
832                        : 'Tax';
833       ($pkg, $setup, $recur, $sdate, $edate) =
834         ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
835     }
836
837     $csv->combine(
838       'cust_bill_pkg',
839       $self->invnum,
840       ( map { '' } (1..11) ),
841       ($pkg, $setup, $recur, $sdate, $edate)
842     ) or die "can't create csv";
843     print CSV $csv->string. "\n";
844
845   }
846
847   close CSV or die "can't close CSV: $!";
848
849   #part two: upload it
850
851   my $net;
852   if ( $opt{protocol} eq 'ftp' ) {
853     eval "use Net::FTP;";
854     die $@ if $@;
855     $net = Net::FTP->new($opt{server}) or die @$;
856   } else {
857     die "unknown protocol: $opt{protocol}";
858   }
859
860   $net->login( $opt{username}, $opt{password} )
861     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
862
863   $net->binary or die "can't set binary mode";
864
865   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
866
867   $net->put($file) or die "can't put $file: $!";
868
869   $net->quit;
870
871   unlink $file;
872
873 }
874
875 =item comp
876
877 Pays this invoice with a compliemntary payment.  If there is an error,
878 returns the error, otherwise returns false.
879
880 =cut
881
882 sub comp {
883   my $self = shift;
884   my $cust_pay = new FS::cust_pay ( {
885     'invnum'   => $self->invnum,
886     'paid'     => $self->owed,
887     '_date'    => '',
888     'payby'    => 'COMP',
889     'payinfo'  => $self->cust_main->payinfo,
890     'paybatch' => '',
891   } );
892   $cust_pay->insert;
893 }
894
895 =item realtime_card
896
897 Attempts to pay this invoice with a credit card payment via a
898 Business::OnlinePayment realtime gateway.  See
899 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
900 for supported processors.
901
902 =cut
903
904 sub realtime_card {
905   my $self = shift;
906   $self->realtime_bop( 'CC', @_ );
907 }
908
909 =item realtime_ach
910
911 Attempts to pay this invoice with an electronic check (ACH) payment via a
912 Business::OnlinePayment realtime gateway.  See
913 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
914 for supported processors.
915
916 =cut
917
918 sub realtime_ach {
919   my $self = shift;
920   $self->realtime_bop( 'ECHECK', @_ );
921 }
922
923 =item realtime_lec
924
925 Attempts to pay this invoice with phone bill (LEC) payment via a
926 Business::OnlinePayment realtime gateway.  See
927 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
928 for supported processors.
929
930 =cut
931
932 sub realtime_lec {
933   my $self = shift;
934   $self->realtime_bop( 'LEC', @_ );
935 }
936
937 sub realtime_bop {
938   my( $self, $method ) = @_;
939
940   my $cust_main = $self->cust_main;
941   my $balance = $cust_main->balance;
942   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
943   $amount = sprintf("%.2f", $amount);
944   return "not run (balance $balance)" unless $amount > 0;
945
946   my $description = 'Internet Services';
947   if ( $conf->exists('business-onlinepayment-description') ) {
948     my $dtempl = $conf->config('business-onlinepayment-description');
949
950     my $agent_obj = $cust_main->agent
951       or die "can't retreive agent for $cust_main (agentnum ".
952              $cust_main->agentnum. ")";
953     my $agent = $agent_obj->agent;
954     my $pkgs = join(', ',
955       map { $_->cust_pkg->part_pkg->pkg }
956         grep { $_->pkgnum } $self->cust_bill_pkg
957     );
958     $description = eval qq("$dtempl");
959   }
960
961   $cust_main->realtime_bop($method, $amount,
962     'description' => $description,
963     'invnum'      => $self->invnum,
964   );
965
966 }
967
968 =item batch_card
969
970 Adds a payment for this invoice to the pending credit card batch (see
971 L<FS::cust_pay_batch>).
972
973 =cut
974
975 sub batch_card {
976   my $self = shift;
977   my $cust_main = $self->cust_main;
978
979   my $cust_pay_batch = new FS::cust_pay_batch ( {
980     'invnum'   => $self->getfield('invnum'),
981     'custnum'  => $cust_main->getfield('custnum'),
982     'last'     => $cust_main->getfield('last'),
983     'first'    => $cust_main->getfield('first'),
984     'address1' => $cust_main->getfield('address1'),
985     'address2' => $cust_main->getfield('address2'),
986     'city'     => $cust_main->getfield('city'),
987     'state'    => $cust_main->getfield('state'),
988     'zip'      => $cust_main->getfield('zip'),
989     'country'  => $cust_main->getfield('country'),
990     'cardnum'  => $cust_main->payinfo,
991     'exp'      => $cust_main->getfield('paydate'),
992     'payname'  => $cust_main->getfield('payname'),
993     'amount'   => $self->owed,
994   } );
995   my $error = $cust_pay_batch->insert;
996   die $error if $error;
997
998   '';
999 }
1000
1001 sub _agent_template {
1002   my $self = shift;
1003   $self->_agent_plandata('agent_templatename');
1004 }
1005
1006 sub _agent_invoice_from {
1007   my $self = shift;
1008   $self->_agent_plandata('agent_invoice_from');
1009 }
1010
1011 sub _agent_plandata {
1012   my( $self, $option ) = @_;
1013
1014   my $part_bill_event = qsearchs( 'part_bill_event',
1015     {
1016       'payby'     => $self->cust_main->payby,
1017       'plan'      => 'send_agent',
1018       'plandata'  => { 'op'    => '~',
1019                        'value' => "(^|\n)agentnum ".
1020                                   $self->cust_main->agentnum.
1021                                   "(\n|\$)",
1022                      },
1023     },
1024     '',
1025     'ORDER BY seconds LIMIT 1'
1026   );
1027
1028   return '' unless $part_bill_event;
1029
1030   if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1031     return $1;
1032   } else {
1033     warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1034          " plandata for $option";
1035     return '';
1036   }
1037
1038 }
1039
1040 =item print_text [ TIME [ , TEMPLATE ] ]
1041
1042 Returns an text invoice, as a list of lines.
1043
1044 TIME an optional value used to control the printing of overdue messages.  The
1045 default is now.  It isn't the date of the invoice; that's the `_date' field.
1046 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1047 L<Time::Local> and L<Date::Parse> for conversion functions.
1048
1049 =cut
1050
1051 #still some false laziness w/print_text
1052 sub print_text {
1053
1054   my( $self, $today, $template ) = @_;
1055   $today ||= time;
1056
1057 #  my $invnum = $self->invnum;
1058   my $cust_main = $self->cust_main;
1059   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1060     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1061
1062   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1063 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1064   #my $balance_due = $self->owed + $pr_total - $cr_total;
1065   my $balance_due = $self->owed + $pr_total;
1066
1067   #my @collect = ();
1068   #my($description,$amount);
1069   @buf = ();
1070
1071   #previous balance
1072   foreach ( @pr_cust_bill ) {
1073     push @buf, [
1074       "Previous Balance, Invoice #". $_->invnum. 
1075                  " (". time2str("%x",$_->_date). ")",
1076       $money_char. sprintf("%10.2f",$_->owed)
1077     ];
1078   }
1079   if (@pr_cust_bill) {
1080     push @buf,['','-----------'];
1081     push @buf,[ 'Total Previous Balance',
1082                 $money_char. sprintf("%10.2f",$pr_total ) ];
1083     push @buf,['',''];
1084   }
1085
1086   #new charges
1087   foreach my $cust_bill_pkg (
1088     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1089     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1090   ) {
1091
1092     if ( $cust_bill_pkg->pkgnum > 0 ) {
1093
1094       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1095       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1096       my $pkg = $part_pkg->pkg;
1097
1098       if ( $cust_bill_pkg->setup != 0 ) {
1099         my $description = $pkg;
1100         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1101         push @buf, [ $description,
1102                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1103         push @buf,
1104           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1105               $cust_pkg->h_labels($self->_date);
1106       }
1107
1108       if ( $cust_bill_pkg->recur != 0 ) {
1109         push @buf, [
1110           "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1111                                 time2str("%x", $cust_bill_pkg->edate) . ")",
1112           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1113         ];
1114         push @buf,
1115           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1116               $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
1117       }
1118
1119       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1120
1121     } else { #pkgnum tax or one-shot line item
1122       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1123                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1124                      : 'Tax';
1125       if ( $cust_bill_pkg->setup != 0 ) {
1126         push @buf, [ $itemdesc,
1127                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1128       }
1129       if ( $cust_bill_pkg->recur != 0 ) {
1130         push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1131                                   . time2str("%x", $cust_bill_pkg->edate). ")",
1132                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1133                    ];
1134       }
1135     }
1136   }
1137
1138   push @buf,['','-----------'];
1139   push @buf,['Total New Charges',
1140              $money_char. sprintf("%10.2f",$self->charged) ];
1141   push @buf,['',''];
1142
1143   push @buf,['','-----------'];
1144   push @buf,['Total Charges',
1145              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1146   push @buf,['',''];
1147
1148   #credits
1149   foreach ( $self->cust_credited ) {
1150
1151     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1152
1153     my $reason = substr($_->cust_credit->reason,0,32);
1154     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1155     $reason = " ($reason) " if $reason;
1156     push @buf,[
1157       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1158         $reason,
1159       $money_char. sprintf("%10.2f",$_->amount)
1160     ];
1161   }
1162   #foreach ( @cr_cust_credit ) {
1163   #  push @buf,[
1164   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1165   #    $money_char. sprintf("%10.2f",$_->credited)
1166   #  ];
1167   #}
1168
1169   #get & print payments
1170   foreach ( $self->cust_bill_pay ) {
1171
1172     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1173
1174     push @buf,[
1175       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1176       $money_char. sprintf("%10.2f",$_->amount )
1177     ];
1178   }
1179
1180   #balance due
1181   my $balance_due_msg = $self->balance_due_msg;
1182
1183   push @buf,['','-----------'];
1184   push @buf,[$balance_due_msg, $money_char. 
1185     sprintf("%10.2f", $balance_due ) ];
1186
1187   #create the template
1188   $template ||= $self->_agent_template;
1189   my $templatefile = 'invoice_template';
1190   $templatefile .= "_$template" if length($template);
1191   my @invoice_template = $conf->config($templatefile)
1192     or die "cannot load config file $templatefile";
1193   $invoice_lines = 0;
1194   my $wasfunc = 0;
1195   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1196     /invoice_lines\((\d*)\)/;
1197     $invoice_lines += $1 || scalar(@buf);
1198     $wasfunc=1;
1199   }
1200   die "no invoice_lines() functions in template?" unless $wasfunc;
1201   my $invoice_template = new Text::Template (
1202     TYPE   => 'ARRAY',
1203     SOURCE => [ map "$_\n", @invoice_template ],
1204   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1205   $invoice_template->compile()
1206     or die "can't compile template: $Text::Template::ERROR";
1207
1208   #setup template variables
1209   package FS::cust_bill::_template; #!
1210   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1211
1212   $invnum = $self->invnum;
1213   $date = $self->_date;
1214   $page = 1;
1215   $agent = $self->cust_main->agent->agent;
1216
1217   if ( $FS::cust_bill::invoice_lines ) {
1218     $total_pages =
1219       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1220     $total_pages++
1221       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1222   } else {
1223     $total_pages = 1;
1224   }
1225
1226   #format address (variable for the template)
1227   my $l = 0;
1228   @address = ( '', '', '', '', '', '' );
1229   package FS::cust_bill; #!
1230   $FS::cust_bill::_template::address[$l++] =
1231     $cust_main->payname.
1232       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1233         ? " (P.O. #". $cust_main->payinfo. ")"
1234         : ''
1235       )
1236   ;
1237   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1238     if $cust_main->company;
1239   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1240   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1241     if $cust_main->address2;
1242   $FS::cust_bill::_template::address[$l++] =
1243     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1244
1245   my $countrydefault = $conf->config('countrydefault') || 'US';
1246   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1247     unless $cust_main->country eq $countrydefault;
1248
1249         #  #overdue? (variable for the template)
1250         #  $FS::cust_bill::_template::overdue = ( 
1251         #    $balance_due > 0
1252         #    && $today > $self->_date 
1253         ##    && $self->printed > 1
1254         #    && $self->printed > 0
1255         #  );
1256
1257   #and subroutine for the template
1258   sub FS::cust_bill::_template::invoice_lines {
1259     my $lines = shift || scalar(@buf);
1260     map { 
1261       scalar(@buf) ? shift @buf : [ '', '' ];
1262     }
1263     ( 1 .. $lines );
1264   }
1265
1266   #and fill it in
1267   $FS::cust_bill::_template::page = 1;
1268   my $lines;
1269   my @collect;
1270   while (@buf) {
1271     push @collect, split("\n",
1272       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1273     );
1274     $FS::cust_bill::_template::page++;
1275   }
1276
1277   map "$_\n", @collect;
1278
1279 }
1280
1281 =item print_latex [ TIME [ , TEMPLATE ] ]
1282
1283 Internal method - returns a filename of a filled-in LaTeX template for this
1284 invoice (Note: add ".tex" to get the actual filename).
1285
1286 See print_ps and print_pdf for methods that return PostScript and PDF output.
1287
1288 TIME an optional value used to control the printing of overdue messages.  The
1289 default is now.  It isn't the date of the invoice; that's the `_date' field.
1290 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1291 L<Time::Local> and L<Date::Parse> for conversion functions.
1292
1293 =cut
1294
1295 #still some false laziness w/print_text
1296 sub print_latex {
1297
1298   my( $self, $today, $template ) = @_;
1299   $today ||= time;
1300   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1301     if $DEBUG;
1302
1303   my $cust_main = $self->cust_main;
1304   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1305     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1306
1307   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1308 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1309   #my $balance_due = $self->owed + $pr_total - $cr_total;
1310   my $balance_due = $self->owed + $pr_total;
1311
1312   #create the template
1313   $template ||= $self->_agent_template;
1314   my $templatefile = 'invoice_latex';
1315   my $suffix = length($template) ? "_$template" : '';
1316   $templatefile .= $suffix;
1317   my @invoice_template = map "$_\n", $conf->config($templatefile)
1318     or die "cannot load config file $templatefile";
1319
1320   my($format, $text_template);
1321   if ( grep { /^%%Detail/ } @invoice_template ) {
1322     #change this to a die when the old code is removed
1323     warn "old-style invoice template $templatefile; ".
1324          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1325     $format = 'old';
1326   } else {
1327     $format = 'Text::Template';
1328     $text_template = new Text::Template(
1329       TYPE => 'ARRAY',
1330       SOURCE => \@invoice_template,
1331       DELIMITERS => [ '[@--', '--@]' ],
1332     );
1333
1334     $text_template->compile()
1335       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1336   }
1337
1338   my $returnaddress;
1339   if ( $conf->exists('invoice_latexreturnaddress')
1340        && length($conf->exists('invoice_latexreturnaddress'))
1341      )
1342   {
1343     $returnaddress = join("\n", $conf->config('invoice_latexreturnaddress') );
1344   } else {
1345     $returnaddress = '~';
1346   }
1347
1348   my %invoice_data = (
1349     'invnum'       => $self->invnum,
1350     'date'         => time2str('%b %o, %Y', $self->_date),
1351     'today'        => time2str('%b %o, %Y', $today),
1352     'agent'        => _latex_escape($cust_main->agent->agent),
1353     'payname'      => _latex_escape($cust_main->payname),
1354     'company'      => _latex_escape($cust_main->company),
1355     'address1'     => _latex_escape($cust_main->address1),
1356     'address2'     => _latex_escape($cust_main->address2),
1357     'city'         => _latex_escape($cust_main->city),
1358     'state'        => _latex_escape($cust_main->state),
1359     'zip'          => _latex_escape($cust_main->zip),
1360     'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
1361     'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
1362     'returnaddress' => $returnaddress,
1363     'quantity'     => 1,
1364     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1365     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1366     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1367   );
1368
1369   my $countrydefault = $conf->config('countrydefault') || 'US';
1370   if ( $cust_main->country eq $countrydefault ) {
1371     $invoice_data{'country'} = '';
1372   } else {
1373     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1374   }
1375
1376   $invoice_data{'notes'} =
1377     join("\n",
1378 #  #do variable substitutions in notes
1379 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1380         $conf->config_orbase('invoice_latexnotes', $template)
1381     );
1382   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1383     if $DEBUG;
1384
1385   $invoice_data{'footer'} =~ s/\n+$//;
1386   $invoice_data{'smallfooter'} =~ s/\n+$//;
1387   $invoice_data{'notes'} =~ s/\n+$//;
1388
1389   $invoice_data{'po_line'} =
1390     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1391       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1392       : '~';
1393
1394   my @filled_in = ();
1395   if ( $format eq 'old' ) {
1396   
1397     my @line_item = ();
1398     my @total_item = ();
1399     while ( @invoice_template ) {
1400       my $line = shift @invoice_template;
1401   
1402       if ( $line =~ /^%%Detail\s*$/ ) {
1403   
1404         while ( ( my $line_item_line = shift @invoice_template )
1405                 !~ /^%%EndDetail\s*$/                            ) {
1406           push @line_item, $line_item_line;
1407         }
1408         foreach my $line_item ( $self->_items ) {
1409         #foreach my $line_item ( $self->_items_pkg ) {
1410           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1411           $invoice_data{'description'} =
1412             _latex_escape($line_item->{'description'});
1413           if ( exists $line_item->{'ext_description'} ) {
1414             $invoice_data{'description'} .=
1415               "\\tabularnewline\n~~".
1416               join( "\\tabularnewline\n~~",
1417                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1418                   );
1419           }
1420           $invoice_data{'amount'} = $line_item->{'amount'};
1421           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1422           push @filled_in,
1423             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1424         }
1425   
1426       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1427   
1428         while ( ( my $total_item_line = shift @invoice_template )
1429                 !~ /^%%EndTotalDetails\s*$/                      ) {
1430           push @total_item, $total_item_line;
1431         }
1432   
1433         my @total_fill = ();
1434   
1435         my $taxtotal = 0;
1436         foreach my $tax ( $self->_items_tax ) {
1437           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1438           $taxtotal += $tax->{'amount'};
1439           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1440           push @total_fill,
1441             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1442                 @total_item;
1443         }
1444
1445         if ( $taxtotal ) {
1446           $invoice_data{'total_item'} = 'Sub-total';
1447           $invoice_data{'total_amount'} =
1448             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1449           unshift @total_fill,
1450             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1451                 @total_item;
1452         }
1453   
1454         $invoice_data{'total_item'} = '\textbf{Total}';
1455         $invoice_data{'total_amount'} =
1456           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1457         push @total_fill,
1458           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1459               @total_item;
1460   
1461         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1462   
1463         # credits
1464         foreach my $credit ( $self->_items_credits ) {
1465           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1466           #$credittotal
1467           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1468           push @total_fill, 
1469             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1470                 @total_item;
1471         }
1472   
1473         # payments
1474         foreach my $payment ( $self->_items_payments ) {
1475           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1476           #$paymenttotal
1477           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1478           push @total_fill, 
1479             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1480                 @total_item;
1481         }
1482   
1483         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1484         $invoice_data{'total_amount'} =
1485           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1486         push @total_fill,
1487           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1488               @total_item;
1489   
1490         push @filled_in, @total_fill;
1491   
1492       } else {
1493         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1494         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1495         push @filled_in, $line;
1496       }
1497   
1498     }
1499
1500     sub nounder {
1501       my $var = $1;
1502       $var =~ s/_/\-/g;
1503       $var;
1504     }
1505
1506   } elsif ( $format eq 'Text::Template' ) {
1507
1508     my @detail_items = ();
1509     my @total_items = ();
1510
1511     $invoice_data{'detail_items'} = \@detail_items;
1512     $invoice_data{'total_items'} = \@total_items;
1513   
1514     foreach my $line_item ( $self->_items ) {
1515       my $detail = {
1516         ext_description => [],
1517       };
1518       $detail->{'ref'} = $line_item->{'pkgnum'};
1519       $detail->{'quantity'} = 1;
1520       $detail->{'description'} = _latex_escape($line_item->{'description'});
1521       if ( exists $line_item->{'ext_description'} ) {
1522         @{$detail->{'ext_description'}} = map {
1523           _latex_escape($_);
1524         } @{$line_item->{'ext_description'}};
1525       }
1526       $detail->{'amount'} = $line_item->{'amount'};
1527       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1528   
1529       push @detail_items, $detail;
1530     }
1531   
1532   
1533     my $taxtotal = 0;
1534     foreach my $tax ( $self->_items_tax ) {
1535       my $total = {};
1536       $total->{'total_item'} = _latex_escape($tax->{'description'});
1537       $taxtotal += $tax->{'amount'};
1538       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1539       push @total_items, $total;
1540     }
1541   
1542     if ( $taxtotal ) {
1543       my $total = {};
1544       $total->{'total_item'} = 'Sub-total';
1545       $total->{'total_amount'} =
1546         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1547       unshift @total_items, $total;
1548     }
1549   
1550     {
1551       my $total = {};
1552       $total->{'total_item'} = '\textbf{Total}';
1553       $total->{'total_amount'} =
1554         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1555       push @total_items, $total;
1556     }
1557   
1558     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1559   
1560     # credits
1561     foreach my $credit ( $self->_items_credits ) {
1562       my $total;
1563       $total->{'total_item'} = _latex_escape($credit->{'description'});
1564       #$credittotal
1565       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1566       push @total_items, $total;
1567     }
1568   
1569     # payments
1570     foreach my $payment ( $self->_items_payments ) {
1571       my $total = {};
1572       $total->{'total_item'} = _latex_escape($payment->{'description'});
1573       #$paymenttotal
1574       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1575       push @total_items, $total;
1576     }
1577   
1578     { 
1579       my $total;
1580       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1581       $total->{'total_amount'} =
1582         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1583       push @total_items, $total;
1584     }
1585
1586   } else {
1587     die "guru meditation #54";
1588   }
1589
1590   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1591   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1592                            DIR      => $dir,
1593                            SUFFIX   => '.tex',
1594                            UNLINK   => 0,
1595                          ) or die "can't open temp file: $!\n";
1596   if ( $format eq 'old' ) {
1597     print $fh join('', @filled_in );
1598   } elsif ( $format eq 'Text::Template' ) {
1599     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1600   } else {
1601     die "guru meditation #32";
1602   }
1603   close $fh;
1604
1605   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1606   return $1;
1607
1608 }
1609
1610 =item print_ps [ TIME [ , TEMPLATE ] ]
1611
1612 Returns an postscript invoice, as a scalar.
1613
1614 TIME an optional value used to control the printing of overdue messages.  The
1615 default is now.  It isn't the date of the invoice; that's the `_date' field.
1616 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1617 L<Time::Local> and L<Date::Parse> for conversion functions.
1618
1619 =cut
1620
1621 sub print_ps {
1622   my $self = shift;
1623
1624   my $file = $self->print_latex(@_);
1625
1626   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1627   chdir($dir);
1628
1629   my $sfile = shell_quote $file;
1630
1631   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1632     or die "pslatex $file.tex failed; see $file.log for details?\n";
1633   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1634     or die "pslatex $file.tex failed; see $file.log for details?\n";
1635
1636   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1637     or die "dvips failed";
1638
1639   open(POSTSCRIPT, "<$file.ps")
1640     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1641
1642   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1643
1644   my $ps = '';
1645   while (<POSTSCRIPT>) {
1646     $ps .= $_;
1647   }
1648
1649   close POSTSCRIPT;
1650
1651   return $ps;
1652
1653 }
1654
1655 =item print_pdf [ TIME [ , TEMPLATE ] ]
1656
1657 Returns an PDF invoice, as a scalar.
1658
1659 TIME an optional value used to control the printing of overdue messages.  The
1660 default is now.  It isn't the date of the invoice; that's the `_date' field.
1661 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1662 L<Time::Local> and L<Date::Parse> for conversion functions.
1663
1664 =cut
1665
1666 sub print_pdf {
1667   my $self = shift;
1668
1669   my $file = $self->print_latex(@_);
1670
1671   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1672   chdir($dir);
1673
1674   #system('pdflatex', "$file.tex");
1675   #system('pdflatex', "$file.tex");
1676   #! LaTeX Error: Unknown graphics extension: .eps.
1677
1678   my $sfile = shell_quote $file;
1679
1680   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1681     or die "pslatex $file.tex failed; see $file.log for details?\n";
1682   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1683     or die "pslatex $file.tex failed; see $file.log for details?\n";
1684
1685   #system('dvipdf', "$file.dvi", "$file.pdf" );
1686   system(
1687     "dvips -q -t letter -f $sfile.dvi ".
1688     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1689     "     -c save pop -"
1690   ) == 0
1691     or die "dvips | gs failed: $!";
1692
1693   open(PDF, "<$file.pdf")
1694     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1695
1696   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1697
1698   my $pdf = '';
1699   while (<PDF>) {
1700     $pdf .= $_;
1701   }
1702
1703   close PDF;
1704
1705   return $pdf;
1706
1707 }
1708
1709 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1710
1711 Returns an HTML invoice, as a scalar.
1712
1713 TIME an optional value used to control the printing of overdue messages.  The
1714 default is now.  It isn't the date of the invoice; that's the `_date' field.
1715 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1716 L<Time::Local> and L<Date::Parse> for conversion functions.
1717
1718 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1719 when emailing the invoice as part of a multipart/related MIME email.
1720
1721 =cut
1722
1723 sub print_html {
1724   my( $self, $today, $template, $cid ) = @_;
1725   $today ||= time;
1726
1727   my $cust_main = $self->cust_main;
1728   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1729     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1730
1731   $template ||= $self->_agent_template;
1732   my $templatefile = 'invoice_html';
1733   my $suffix = length($template) ? "_$template" : '';
1734   $templatefile .= $suffix;
1735   my @html_template = map "$_\n", $conf->config($templatefile)
1736     or die "cannot load config file $templatefile";
1737
1738   my $html_template = new Text::Template(
1739     TYPE   => 'ARRAY',
1740     SOURCE => \@html_template,
1741     DELIMITERS => [ '<%=', '%>' ],
1742   );
1743
1744   $html_template->compile()
1745     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1746
1747   my %invoice_data = (
1748     'invnum'       => $self->invnum,
1749     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
1750     'today'        => time2str('%b %o, %Y', $today),
1751     'agent'        => encode_entities($cust_main->agent->agent),
1752     'payname'      => encode_entities($cust_main->payname),
1753     'company'      => encode_entities($cust_main->company),
1754     'address1'     => encode_entities($cust_main->address1),
1755     'address2'     => encode_entities($cust_main->address2),
1756     'city'         => encode_entities($cust_main->city),
1757     'state'        => encode_entities($cust_main->state),
1758     'zip'          => encode_entities($cust_main->zip),
1759     'terms'        => $conf->config('invoice_default_terms')
1760                       || 'Payable upon receipt',
1761     'cid'          => $cid,
1762 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1763   );
1764
1765   $invoice_data{'returnaddress'} = $conf->exists('invoice_htmlreturnaddress')
1766     ? join("\n", $conf->config('invoice_htmlreturnaddress') )
1767     : join("\n", map { 
1768                        s/~/&nbsp;/g;
1769                        s/\\\\\*?\s*$/<BR>/;
1770                        s/\\hyphenation\{[\w\s\-]+\}//;
1771                        $_;
1772                      }
1773                      $conf->config('invoice_latexreturnaddress')
1774           );
1775
1776   my $countrydefault = $conf->config('countrydefault') || 'US';
1777   if ( $cust_main->country eq $countrydefault ) {
1778     $invoice_data{'country'} = '';
1779   } else {
1780     $invoice_data{'country'} =
1781       encode_entities(code2country($cust_main->country));
1782   }
1783
1784   $invoice_data{'notes'} =
1785     length($conf->config_orbase('invoice_htmlnotes', $template))
1786       ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1787       : join("\n", map { 
1788                          s/%%(.*)$/<!-- $1 -->/;
1789                          s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1790                          s/\\begin\{enumerate\}/<ol>/;
1791                          s/\\item /  <li>/;
1792                          s/\\end\{enumerate\}/<\/ol>/;
1793                          s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1794                          $_;
1795                        } 
1796                        $conf->config_orbase('invoice_latexnotes', $template)
1797             );
1798
1799 #  #do variable substitutions in notes
1800 #  $invoice_data{'notes'} =
1801 #    join("\n",
1802 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1803 #        $conf->config_orbase('invoice_latexnotes', $suffix)
1804 #    );
1805
1806    $invoice_data{'footer'} = $conf->exists('invoice_htmlfooter')
1807      ? join("\n", $conf->config('invoice_htmlfooter') )
1808      : join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
1809                       $conf->config('invoice_latexfooter')
1810            );
1811
1812   $invoice_data{'po_line'} =
1813     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1814       ? encode_entities("Purchase Order #". $cust_main->payinfo)
1815       : '';
1816
1817   my $money_char = $conf->config('money_char') || '$';
1818
1819   foreach my $line_item ( $self->_items ) {
1820     my $detail = {
1821       ext_description => [],
1822     };
1823     $detail->{'ref'} = $line_item->{'pkgnum'};
1824     $detail->{'description'} = encode_entities($line_item->{'description'});
1825     if ( exists $line_item->{'ext_description'} ) {
1826       @{$detail->{'ext_description'}} = map {
1827         encode_entities($_);
1828       } @{$line_item->{'ext_description'}};
1829     }
1830     $detail->{'amount'} = $money_char. $line_item->{'amount'};
1831     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1832
1833     push @{$invoice_data{'detail_items'}}, $detail;
1834   }
1835
1836
1837   my $taxtotal = 0;
1838   foreach my $tax ( $self->_items_tax ) {
1839     my $total = {};
1840     $total->{'total_item'} = encode_entities($tax->{'description'});
1841     $taxtotal += $tax->{'amount'};
1842     $total->{'total_amount'} = $money_char. $tax->{'amount'};
1843     push @{$invoice_data{'total_items'}}, $total;
1844   }
1845
1846   if ( $taxtotal ) {
1847     my $total = {};
1848     $total->{'total_item'} = 'Sub-total';
1849     $total->{'total_amount'} =
1850       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1851     unshift @{$invoice_data{'total_items'}}, $total;
1852   }
1853
1854   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1855   {
1856     my $total = {};
1857     $total->{'total_item'} = '<b>Total</b>';
1858     $total->{'total_amount'} =
1859       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1860     push @{$invoice_data{'total_items'}}, $total;
1861   }
1862
1863   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1864
1865   # credits
1866   foreach my $credit ( $self->_items_credits ) {
1867     my $total;
1868     $total->{'total_item'} = encode_entities($credit->{'description'});
1869     #$credittotal
1870     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1871     push @{$invoice_data{'total_items'}}, $total;
1872   }
1873
1874   # payments
1875   foreach my $payment ( $self->_items_payments ) {
1876     my $total = {};
1877     $total->{'total_item'} = encode_entities($payment->{'description'});
1878     #$paymenttotal
1879     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1880     push @{$invoice_data{'total_items'}}, $total;
1881   }
1882
1883   { 
1884     my $total;
1885     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1886     $total->{'total_amount'} =
1887       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1888     push @{$invoice_data{'total_items'}}, $total;
1889   }
1890
1891   $html_template->fill_in( HASH => \%invoice_data);
1892 }
1893
1894 # quick subroutine for print_latex
1895 #
1896 # There are ten characters that LaTeX treats as special characters, which
1897 # means that they do not simply typeset themselves: 
1898 #      # $ % & ~ _ ^ \ { }
1899 #
1900 # TeX ignores blanks following an escaped character; if you want a blank (as
1901 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1902
1903 sub _latex_escape {
1904   my $value = shift;
1905   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1906   $value =~ s/([<>])/\$$1\$/g;
1907   $value;
1908 }
1909
1910 #utility methods for print_*
1911
1912 sub balance_due_msg {
1913   my $self = shift;
1914   my $msg = 'Balance Due';
1915   return $msg unless $conf->exists('invoice_default_terms');
1916   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1917     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1918   } elsif ( $conf->config('invoice_default_terms') ) {
1919     $msg .= ' - '. $conf->config('invoice_default_terms');
1920   }
1921   $msg;
1922 }
1923
1924 sub _items {
1925   my $self = shift;
1926   my @display = scalar(@_)
1927                 ? @_
1928                 : qw( _items_previous _items_pkg );
1929                 #: qw( _items_pkg );
1930                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1931   my @b = ();
1932   foreach my $display ( @display ) {
1933     push @b, $self->$display(@_);
1934   }
1935   @b;
1936 }
1937
1938 sub _items_previous {
1939   my $self = shift;
1940   my $cust_main = $self->cust_main;
1941   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1942   my @b = ();
1943   foreach ( @pr_cust_bill ) {
1944     push @b, {
1945       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
1946                        ' ('. time2str('%x',$_->_date). ')',
1947       #'pkgpart'     => 'N/A',
1948       'pkgnum'      => 'N/A',
1949       'amount'      => sprintf("%.2f", $_->owed),
1950     };
1951   }
1952   @b;
1953
1954   #{
1955   #    'description'     => 'Previous Balance',
1956   #    #'pkgpart'         => 'N/A',
1957   #    'pkgnum'          => 'N/A',
1958   #    'amount'          => sprintf("%10.2f", $pr_total ),
1959   #    'ext_description' => [ map {
1960   #                                 "Invoice ". $_->invnum.
1961   #                                 " (". time2str("%x",$_->_date). ") ".
1962   #                                 sprintf("%10.2f", $_->owed)
1963   #                         } @pr_cust_bill ],
1964
1965   #};
1966 }
1967
1968 sub _items_pkg {
1969   my $self = shift;
1970   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1971   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1972 }
1973
1974 sub _items_tax {
1975   my $self = shift;
1976   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1977   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1978 }
1979
1980 sub _items_cust_bill_pkg {
1981   my $self = shift;
1982   my $cust_bill_pkg = shift;
1983
1984   my @b = ();
1985   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1986
1987     if ( $cust_bill_pkg->pkgnum > 0 ) {
1988
1989       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1990       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1991       my $pkg = $part_pkg->pkg;
1992
1993       if ( $cust_bill_pkg->setup != 0 ) {
1994         my $description = $pkg;
1995         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1996         my @d = $cust_pkg->h_labels_short($self->_date);
1997         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1998         push @b, {
1999           description     => $description,
2000           #pkgpart         => $part_pkg->pkgpart,
2001           pkgnum          => $cust_pkg->pkgnum,
2002           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2003           ext_description => \@d,
2004         };
2005       }
2006
2007       if ( $cust_bill_pkg->recur != 0 ) {
2008         push @b, {
2009           description     => "$pkg (" .
2010                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
2011                                time2str('%x', $cust_bill_pkg->edate). ')',
2012           #pkgpart         => $part_pkg->pkgpart,
2013           pkgnum          => $cust_pkg->pkgnum,
2014           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2015           ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
2016                                                          $cust_bill_pkg->sdate),
2017                                $cust_bill_pkg->details,
2018                              ],
2019         };
2020       }
2021
2022     } else { #pkgnum tax or one-shot line item (??)
2023
2024       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
2025                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
2026                      : 'Tax';
2027       if ( $cust_bill_pkg->setup != 0 ) {
2028         push @b, {
2029           'description' => $itemdesc,
2030           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2031         };
2032       }
2033       if ( $cust_bill_pkg->recur != 0 ) {
2034         push @b, {
2035           'description' => "$itemdesc (".
2036                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2037                            time2str("%x", $cust_bill_pkg->edate). ')',
2038           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2039         };
2040       }
2041
2042     }
2043
2044   }
2045
2046   @b;
2047
2048 }
2049
2050 sub _items_credits {
2051   my $self = shift;
2052
2053   my @b;
2054   #credits
2055   foreach ( $self->cust_credited ) {
2056
2057     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2058
2059     my $reason = $_->cust_credit->reason;
2060     #my $reason = substr($_->cust_credit->reason,0,32);
2061     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2062     $reason = " ($reason) " if $reason;
2063     push @b, {
2064       #'description' => 'Credit ref\#'. $_->crednum.
2065       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2066       #                 $reason,
2067       'description' => 'Credit applied '.
2068                        time2str("%x",$_->cust_credit->_date). $reason,
2069       'amount'      => sprintf("%.2f",$_->amount),
2070     };
2071   }
2072   #foreach ( @cr_cust_credit ) {
2073   #  push @buf,[
2074   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2075   #    $money_char. sprintf("%10.2f",$_->credited)
2076   #  ];
2077   #}
2078
2079   @b;
2080
2081 }
2082
2083 sub _items_payments {
2084   my $self = shift;
2085
2086   my @b;
2087   #get & print payments
2088   foreach ( $self->cust_bill_pay ) {
2089
2090     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2091
2092     push @b, {
2093       'description' => "Payment received ".
2094                        time2str("%x",$_->cust_pay->_date ),
2095       'amount'      => sprintf("%.2f", $_->amount )
2096     };
2097   }
2098
2099   @b;
2100
2101 }
2102
2103 =back
2104
2105 =head1 BUGS
2106
2107 The delete method.
2108
2109 print_text formatting (and some logic :/) is in source, but needs to be
2110 slurped in from a file.  Also number of lines ($=).
2111
2112 =head1 SEE ALSO
2113
2114 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2115 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2116 documentation.
2117
2118 =cut
2119
2120 1;
2121