ugh, hopefully fixup agent_plandata regex for multiple agents
[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                                    '\[?[0-9 ,]*'.
1021                                   '[ ,]'. $self->cust_main->agentnum. '[ ,]'.
1022                                   '[0-9 ,]*\]?'.
1023                                   "(\n|\$)",
1024                      },
1025     },
1026     '',
1027     'ORDER BY seconds LIMIT 1'
1028   );
1029
1030   return '' unless $part_bill_event;
1031
1032   if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
1033     return $1;
1034   } else {
1035     warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
1036          " plandata for $option";
1037     return '';
1038   }
1039
1040 }
1041
1042 =item print_text [ TIME [ , TEMPLATE ] ]
1043
1044 Returns an text invoice, as a list of lines.
1045
1046 TIME an optional value used to control the printing of overdue messages.  The
1047 default is now.  It isn't the date of the invoice; that's the `_date' field.
1048 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1049 L<Time::Local> and L<Date::Parse> for conversion functions.
1050
1051 =cut
1052
1053 #still some false laziness w/print_text
1054 sub print_text {
1055
1056   my( $self, $today, $template ) = @_;
1057   $today ||= time;
1058
1059 #  my $invnum = $self->invnum;
1060   my $cust_main = $self->cust_main;
1061   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1062     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1063
1064   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1065 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1066   #my $balance_due = $self->owed + $pr_total - $cr_total;
1067   my $balance_due = $self->owed + $pr_total;
1068
1069   #my @collect = ();
1070   #my($description,$amount);
1071   @buf = ();
1072
1073   #previous balance
1074   foreach ( @pr_cust_bill ) {
1075     push @buf, [
1076       "Previous Balance, Invoice #". $_->invnum. 
1077                  " (". time2str("%x",$_->_date). ")",
1078       $money_char. sprintf("%10.2f",$_->owed)
1079     ];
1080   }
1081   if (@pr_cust_bill) {
1082     push @buf,['','-----------'];
1083     push @buf,[ 'Total Previous Balance',
1084                 $money_char. sprintf("%10.2f",$pr_total ) ];
1085     push @buf,['',''];
1086   }
1087
1088   #new charges
1089   foreach my $cust_bill_pkg (
1090     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1091     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1092   ) {
1093
1094     if ( $cust_bill_pkg->pkgnum > 0 ) {
1095
1096       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1097       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1098       my $pkg = $part_pkg->pkg;
1099
1100       if ( $cust_bill_pkg->setup != 0 ) {
1101         my $description = $pkg;
1102         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1103         push @buf, [ $description,
1104                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1105         push @buf,
1106           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1107               $cust_pkg->h_labels($self->_date);
1108       }
1109
1110       if ( $cust_bill_pkg->recur != 0 ) {
1111         push @buf, [
1112           "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1113                                 time2str("%x", $cust_bill_pkg->edate) . ")",
1114           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1115         ];
1116         push @buf,
1117           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
1118               $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
1119       }
1120
1121       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
1122
1123     } else { #pkgnum tax or one-shot line item
1124       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1125                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1126                      : 'Tax';
1127       if ( $cust_bill_pkg->setup != 0 ) {
1128         push @buf, [ $itemdesc,
1129                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1130       }
1131       if ( $cust_bill_pkg->recur != 0 ) {
1132         push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1133                                   . time2str("%x", $cust_bill_pkg->edate). ")",
1134                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1135                    ];
1136       }
1137     }
1138   }
1139
1140   push @buf,['','-----------'];
1141   push @buf,['Total New Charges',
1142              $money_char. sprintf("%10.2f",$self->charged) ];
1143   push @buf,['',''];
1144
1145   push @buf,['','-----------'];
1146   push @buf,['Total Charges',
1147              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1148   push @buf,['',''];
1149
1150   #credits
1151   foreach ( $self->cust_credited ) {
1152
1153     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1154
1155     my $reason = substr($_->cust_credit->reason,0,32);
1156     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1157     $reason = " ($reason) " if $reason;
1158     push @buf,[
1159       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1160         $reason,
1161       $money_char. sprintf("%10.2f",$_->amount)
1162     ];
1163   }
1164   #foreach ( @cr_cust_credit ) {
1165   #  push @buf,[
1166   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1167   #    $money_char. sprintf("%10.2f",$_->credited)
1168   #  ];
1169   #}
1170
1171   #get & print payments
1172   foreach ( $self->cust_bill_pay ) {
1173
1174     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1175
1176     push @buf,[
1177       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1178       $money_char. sprintf("%10.2f",$_->amount )
1179     ];
1180   }
1181
1182   #balance due
1183   my $balance_due_msg = $self->balance_due_msg;
1184
1185   push @buf,['','-----------'];
1186   push @buf,[$balance_due_msg, $money_char. 
1187     sprintf("%10.2f", $balance_due ) ];
1188
1189   #create the template
1190   $template ||= $self->_agent_template;
1191   my $templatefile = 'invoice_template';
1192   $templatefile .= "_$template" if length($template);
1193   my @invoice_template = $conf->config($templatefile)
1194     or die "cannot load config file $templatefile";
1195   $invoice_lines = 0;
1196   my $wasfunc = 0;
1197   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1198     /invoice_lines\((\d*)\)/;
1199     $invoice_lines += $1 || scalar(@buf);
1200     $wasfunc=1;
1201   }
1202   die "no invoice_lines() functions in template?" unless $wasfunc;
1203   my $invoice_template = new Text::Template (
1204     TYPE   => 'ARRAY',
1205     SOURCE => [ map "$_\n", @invoice_template ],
1206   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1207   $invoice_template->compile()
1208     or die "can't compile template: $Text::Template::ERROR";
1209
1210   #setup template variables
1211   package FS::cust_bill::_template; #!
1212   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1213
1214   $invnum = $self->invnum;
1215   $date = $self->_date;
1216   $page = 1;
1217   $agent = $self->cust_main->agent->agent;
1218
1219   if ( $FS::cust_bill::invoice_lines ) {
1220     $total_pages =
1221       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1222     $total_pages++
1223       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1224   } else {
1225     $total_pages = 1;
1226   }
1227
1228   #format address (variable for the template)
1229   my $l = 0;
1230   @address = ( '', '', '', '', '', '' );
1231   package FS::cust_bill; #!
1232   $FS::cust_bill::_template::address[$l++] =
1233     $cust_main->payname.
1234       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1235         ? " (P.O. #". $cust_main->payinfo. ")"
1236         : ''
1237       )
1238   ;
1239   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1240     if $cust_main->company;
1241   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1242   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1243     if $cust_main->address2;
1244   $FS::cust_bill::_template::address[$l++] =
1245     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1246
1247   my $countrydefault = $conf->config('countrydefault') || 'US';
1248   $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
1249     unless $cust_main->country eq $countrydefault;
1250
1251         #  #overdue? (variable for the template)
1252         #  $FS::cust_bill::_template::overdue = ( 
1253         #    $balance_due > 0
1254         #    && $today > $self->_date 
1255         ##    && $self->printed > 1
1256         #    && $self->printed > 0
1257         #  );
1258
1259   #and subroutine for the template
1260   sub FS::cust_bill::_template::invoice_lines {
1261     my $lines = shift || scalar(@buf);
1262     map { 
1263       scalar(@buf) ? shift @buf : [ '', '' ];
1264     }
1265     ( 1 .. $lines );
1266   }
1267
1268   #and fill it in
1269   $FS::cust_bill::_template::page = 1;
1270   my $lines;
1271   my @collect;
1272   while (@buf) {
1273     push @collect, split("\n",
1274       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1275     );
1276     $FS::cust_bill::_template::page++;
1277   }
1278
1279   map "$_\n", @collect;
1280
1281 }
1282
1283 =item print_latex [ TIME [ , TEMPLATE ] ]
1284
1285 Internal method - returns a filename of a filled-in LaTeX template for this
1286 invoice (Note: add ".tex" to get the actual filename).
1287
1288 See print_ps and print_pdf for methods that return PostScript and PDF output.
1289
1290 TIME an optional value used to control the printing of overdue messages.  The
1291 default is now.  It isn't the date of the invoice; that's the `_date' field.
1292 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1293 L<Time::Local> and L<Date::Parse> for conversion functions.
1294
1295 =cut
1296
1297 #still some false laziness w/print_text
1298 sub print_latex {
1299
1300   my( $self, $today, $template ) = @_;
1301   $today ||= time;
1302   warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
1303     if $DEBUG;
1304
1305   my $cust_main = $self->cust_main;
1306   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1307     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1308
1309   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1310 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1311   #my $balance_due = $self->owed + $pr_total - $cr_total;
1312   my $balance_due = $self->owed + $pr_total;
1313
1314   #create the template
1315   $template ||= $self->_agent_template;
1316   my $templatefile = 'invoice_latex';
1317   my $suffix = length($template) ? "_$template" : '';
1318   $templatefile .= $suffix;
1319   my @invoice_template = map "$_\n", $conf->config($templatefile)
1320     or die "cannot load config file $templatefile";
1321
1322   my($format, $text_template);
1323   if ( grep { /^%%Detail/ } @invoice_template ) {
1324     #change this to a die when the old code is removed
1325     warn "old-style invoice template $templatefile; ".
1326          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1327     $format = 'old';
1328   } else {
1329     $format = 'Text::Template';
1330     $text_template = new Text::Template(
1331       TYPE => 'ARRAY',
1332       SOURCE => \@invoice_template,
1333       DELIMITERS => [ '[@--', '--@]' ],
1334     );
1335
1336     $text_template->compile()
1337       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1338   }
1339
1340   my $returnaddress;
1341   if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
1342     $returnaddress = join("\n",
1343       $conf->config_orbase('invoice_latexreturnaddress', $template)
1344     );
1345   } else {
1346     $returnaddress = '~';
1347   }
1348
1349   my %invoice_data = (
1350     'invnum'       => $self->invnum,
1351     'date'         => time2str('%b %o, %Y', $self->_date),
1352     'today'        => time2str('%b %o, %Y', $today),
1353     'agent'        => _latex_escape($cust_main->agent->agent),
1354     'payname'      => _latex_escape($cust_main->payname),
1355     'company'      => _latex_escape($cust_main->company),
1356     'address1'     => _latex_escape($cust_main->address1),
1357     'address2'     => _latex_escape($cust_main->address2),
1358     'city'         => _latex_escape($cust_main->city),
1359     'state'        => _latex_escape($cust_main->state),
1360     'zip'          => _latex_escape($cust_main->zip),
1361     'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
1362     'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
1363     'returnaddress' => $returnaddress,
1364     'quantity'     => 1,
1365     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1366     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1367     'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1368   );
1369
1370   my $countrydefault = $conf->config('countrydefault') || 'US';
1371   if ( $cust_main->country eq $countrydefault ) {
1372     $invoice_data{'country'} = '';
1373   } else {
1374     $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
1375   }
1376
1377   $invoice_data{'notes'} =
1378     join("\n",
1379 #  #do variable substitutions in notes
1380 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1381         $conf->config_orbase('invoice_latexnotes', $template)
1382     );
1383   warn "invoice notes: ". $invoice_data{'notes'}. "\n"
1384     if $DEBUG;
1385
1386   $invoice_data{'footer'} =~ s/\n+$//;
1387   $invoice_data{'smallfooter'} =~ s/\n+$//;
1388   $invoice_data{'notes'} =~ s/\n+$//;
1389
1390   $invoice_data{'po_line'} =
1391     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1392       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1393       : '~';
1394
1395   my @filled_in = ();
1396   if ( $format eq 'old' ) {
1397   
1398     my @line_item = ();
1399     my @total_item = ();
1400     while ( @invoice_template ) {
1401       my $line = shift @invoice_template;
1402   
1403       if ( $line =~ /^%%Detail\s*$/ ) {
1404   
1405         while ( ( my $line_item_line = shift @invoice_template )
1406                 !~ /^%%EndDetail\s*$/                            ) {
1407           push @line_item, $line_item_line;
1408         }
1409         foreach my $line_item ( $self->_items ) {
1410         #foreach my $line_item ( $self->_items_pkg ) {
1411           $invoice_data{'ref'} = $line_item->{'pkgnum'};
1412           $invoice_data{'description'} =
1413             _latex_escape($line_item->{'description'});
1414           if ( exists $line_item->{'ext_description'} ) {
1415             $invoice_data{'description'} .=
1416               "\\tabularnewline\n~~".
1417               join( "\\tabularnewline\n~~",
1418                     map _latex_escape($_), @{$line_item->{'ext_description'}}
1419                   );
1420           }
1421           $invoice_data{'amount'} = $line_item->{'amount'};
1422           $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1423           push @filled_in,
1424             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1425         }
1426   
1427       } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1428   
1429         while ( ( my $total_item_line = shift @invoice_template )
1430                 !~ /^%%EndTotalDetails\s*$/                      ) {
1431           push @total_item, $total_item_line;
1432         }
1433   
1434         my @total_fill = ();
1435   
1436         my $taxtotal = 0;
1437         foreach my $tax ( $self->_items_tax ) {
1438           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1439           $taxtotal += $tax->{'amount'};
1440           $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
1441           push @total_fill,
1442             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1443                 @total_item;
1444         }
1445
1446         if ( $taxtotal ) {
1447           $invoice_data{'total_item'} = 'Sub-total';
1448           $invoice_data{'total_amount'} =
1449             '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1450           unshift @total_fill,
1451             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1452                 @total_item;
1453         }
1454   
1455         $invoice_data{'total_item'} = '\textbf{Total}';
1456         $invoice_data{'total_amount'} =
1457           '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1458         push @total_fill,
1459           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1460               @total_item;
1461   
1462         #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1463   
1464         # credits
1465         foreach my $credit ( $self->_items_credits ) {
1466           $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1467           #$credittotal
1468           $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1469           push @total_fill, 
1470             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1471                 @total_item;
1472         }
1473   
1474         # payments
1475         foreach my $payment ( $self->_items_payments ) {
1476           $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1477           #$paymenttotal
1478           $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1479           push @total_fill, 
1480             map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1481                 @total_item;
1482         }
1483   
1484         $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1485         $invoice_data{'total_amount'} =
1486           '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1487         push @total_fill,
1488           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1489               @total_item;
1490   
1491         push @filled_in, @total_fill;
1492   
1493       } else {
1494         #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1495         $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1496         push @filled_in, $line;
1497       }
1498   
1499     }
1500
1501     sub nounder {
1502       my $var = $1;
1503       $var =~ s/_/\-/g;
1504       $var;
1505     }
1506
1507   } elsif ( $format eq 'Text::Template' ) {
1508
1509     my @detail_items = ();
1510     my @total_items = ();
1511
1512     $invoice_data{'detail_items'} = \@detail_items;
1513     $invoice_data{'total_items'} = \@total_items;
1514   
1515     foreach my $line_item ( $self->_items ) {
1516       my $detail = {
1517         ext_description => [],
1518       };
1519       $detail->{'ref'} = $line_item->{'pkgnum'};
1520       $detail->{'quantity'} = 1;
1521       $detail->{'description'} = _latex_escape($line_item->{'description'});
1522       if ( exists $line_item->{'ext_description'} ) {
1523         @{$detail->{'ext_description'}} = map {
1524           _latex_escape($_);
1525         } @{$line_item->{'ext_description'}};
1526       }
1527       $detail->{'amount'} = $line_item->{'amount'};
1528       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1529   
1530       push @detail_items, $detail;
1531     }
1532   
1533   
1534     my $taxtotal = 0;
1535     foreach my $tax ( $self->_items_tax ) {
1536       my $total = {};
1537       $total->{'total_item'} = _latex_escape($tax->{'description'});
1538       $taxtotal += $tax->{'amount'};
1539       $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
1540       push @total_items, $total;
1541     }
1542   
1543     if ( $taxtotal ) {
1544       my $total = {};
1545       $total->{'total_item'} = 'Sub-total';
1546       $total->{'total_amount'} =
1547         '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1548       unshift @total_items, $total;
1549     }
1550   
1551     {
1552       my $total = {};
1553       $total->{'total_item'} = '\textbf{Total}';
1554       $total->{'total_amount'} =
1555         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1556       push @total_items, $total;
1557     }
1558   
1559     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1560   
1561     # credits
1562     foreach my $credit ( $self->_items_credits ) {
1563       my $total;
1564       $total->{'total_item'} = _latex_escape($credit->{'description'});
1565       #$credittotal
1566       $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
1567       push @total_items, $total;
1568     }
1569   
1570     # payments
1571     foreach my $payment ( $self->_items_payments ) {
1572       my $total = {};
1573       $total->{'total_item'} = _latex_escape($payment->{'description'});
1574       #$paymenttotal
1575       $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
1576       push @total_items, $total;
1577     }
1578   
1579     { 
1580       my $total;
1581       $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1582       $total->{'total_amount'} =
1583         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1584       push @total_items, $total;
1585     }
1586
1587   } else {
1588     die "guru meditation #54";
1589   }
1590
1591   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1592   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1593                            DIR      => $dir,
1594                            SUFFIX   => '.tex',
1595                            UNLINK   => 0,
1596                          ) or die "can't open temp file: $!\n";
1597   if ( $format eq 'old' ) {
1598     print $fh join('', @filled_in );
1599   } elsif ( $format eq 'Text::Template' ) {
1600     $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
1601   } else {
1602     die "guru meditation #32";
1603   }
1604   close $fh;
1605
1606   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1607   return $1;
1608
1609 }
1610
1611 =item print_ps [ TIME [ , TEMPLATE ] ]
1612
1613 Returns an postscript invoice, as a scalar.
1614
1615 TIME an optional value used to control the printing of overdue messages.  The
1616 default is now.  It isn't the date of the invoice; that's the `_date' field.
1617 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1618 L<Time::Local> and L<Date::Parse> for conversion functions.
1619
1620 =cut
1621
1622 sub print_ps {
1623   my $self = shift;
1624
1625   my $file = $self->print_latex(@_);
1626
1627   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1628   chdir($dir);
1629
1630   my $sfile = shell_quote $file;
1631
1632   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1633     or die "pslatex $file.tex failed; see $file.log for details?\n";
1634   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1635     or die "pslatex $file.tex failed; see $file.log for details?\n";
1636
1637   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
1638     or die "dvips failed";
1639
1640   open(POSTSCRIPT, "<$file.ps")
1641     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
1642
1643   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1644
1645   my $ps = '';
1646   while (<POSTSCRIPT>) {
1647     $ps .= $_;
1648   }
1649
1650   close POSTSCRIPT;
1651
1652   return $ps;
1653
1654 }
1655
1656 =item print_pdf [ TIME [ , TEMPLATE ] ]
1657
1658 Returns an PDF invoice, as a scalar.
1659
1660 TIME an optional value used to control the printing of overdue messages.  The
1661 default is now.  It isn't the date of the invoice; that's the `_date' field.
1662 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1663 L<Time::Local> and L<Date::Parse> for conversion functions.
1664
1665 =cut
1666
1667 sub print_pdf {
1668   my $self = shift;
1669
1670   my $file = $self->print_latex(@_);
1671
1672   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1673   chdir($dir);
1674
1675   #system('pdflatex', "$file.tex");
1676   #system('pdflatex', "$file.tex");
1677   #! LaTeX Error: Unknown graphics extension: .eps.
1678
1679   my $sfile = shell_quote $file;
1680
1681   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1682     or die "pslatex $file.tex failed; see $file.log for details?\n";
1683   system("pslatex $sfile.tex >/dev/null 2>&1") == 0
1684     or die "pslatex $file.tex failed; see $file.log for details?\n";
1685
1686   #system('dvipdf', "$file.dvi", "$file.pdf" );
1687   system(
1688     "dvips -q -t letter -f $sfile.dvi ".
1689     "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
1690     "     -c save pop -"
1691   ) == 0
1692     or die "dvips | gs failed: $!";
1693
1694   open(PDF, "<$file.pdf")
1695     or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
1696
1697   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1698
1699   my $pdf = '';
1700   while (<PDF>) {
1701     $pdf .= $_;
1702   }
1703
1704   close PDF;
1705
1706   return $pdf;
1707
1708 }
1709
1710 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
1711
1712 Returns an HTML invoice, as a scalar.
1713
1714 TIME an optional value used to control the printing of overdue messages.  The
1715 default is now.  It isn't the date of the invoice; that's the `_date' field.
1716 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1717 L<Time::Local> and L<Date::Parse> for conversion functions.
1718
1719 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
1720 when emailing the invoice as part of a multipart/related MIME email.
1721
1722 =cut
1723
1724 sub print_html {
1725   my( $self, $today, $template, $cid ) = @_;
1726   $today ||= time;
1727
1728   my $cust_main = $self->cust_main;
1729   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1730     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
1731
1732   $template ||= $self->_agent_template;
1733   my $templatefile = 'invoice_html';
1734   my $suffix = length($template) ? "_$template" : '';
1735   $templatefile .= $suffix;
1736   my @html_template = map "$_\n", $conf->config($templatefile)
1737     or die "cannot load config file $templatefile";
1738
1739   my $html_template = new Text::Template(
1740     TYPE   => 'ARRAY',
1741     SOURCE => \@html_template,
1742     DELIMITERS => [ '<%=', '%>' ],
1743   );
1744
1745   $html_template->compile()
1746     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
1747
1748   my %invoice_data = (
1749     'invnum'       => $self->invnum,
1750     'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
1751     'today'        => time2str('%b %o, %Y', $today),
1752     'agent'        => encode_entities($cust_main->agent->agent),
1753     'payname'      => encode_entities($cust_main->payname),
1754     'company'      => encode_entities($cust_main->company),
1755     'address1'     => encode_entities($cust_main->address1),
1756     'address2'     => encode_entities($cust_main->address2),
1757     'city'         => encode_entities($cust_main->city),
1758     'state'        => encode_entities($cust_main->state),
1759     'zip'          => encode_entities($cust_main->zip),
1760     'terms'        => $conf->config('invoice_default_terms')
1761                       || 'Payable upon receipt',
1762     'cid'          => $cid,
1763 #    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
1764   );
1765
1766   $invoice_data{'returnaddress'} =
1767     length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
1768       ? join("\n", $conf->config('invoice_htmlreturnaddress', $template) )
1769       : join("\n", map { 
1770                          s/~/&nbsp;/g;
1771                          s/\\\\\*?\s*$/<BR>/;
1772                          s/\\hyphenation\{[\w\s\-]+\}//;
1773                          $_;
1774                        }
1775                        $conf->config_orbase('invoice_latexreturnaddress', $template)
1776             );
1777
1778   my $countrydefault = $conf->config('countrydefault') || 'US';
1779   if ( $cust_main->country eq $countrydefault ) {
1780     $invoice_data{'country'} = '';
1781   } else {
1782     $invoice_data{'country'} =
1783       encode_entities(code2country($cust_main->country));
1784   }
1785
1786   $invoice_data{'notes'} =
1787     length($conf->config_orbase('invoice_htmlnotes', $template))
1788       ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
1789       : join("\n", map { 
1790                          s/%%(.*)$/<!-- $1 -->/;
1791                          s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
1792                          s/\\begin\{enumerate\}/<ol>/;
1793                          s/\\item /  <li>/;
1794                          s/\\end\{enumerate\}/<\/ol>/;
1795                          s/\\textbf\{(.*)\}/<b>$1<\/b>/;
1796                          $_;
1797                        } 
1798                        $conf->config_orbase('invoice_latexnotes', $template)
1799             );
1800
1801 #  #do variable substitutions in notes
1802 #  $invoice_data{'notes'} =
1803 #    join("\n",
1804 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1805 #        $conf->config_orbase('invoice_latexnotes', $suffix)
1806 #    );
1807
1808    $invoice_data{'footer'} = $conf->exists('invoice_htmlfooter')
1809      ? join("\n", $conf->config('invoice_htmlfooter') )
1810      : join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
1811                       $conf->config('invoice_latexfooter')
1812            );
1813
1814   $invoice_data{'po_line'} =
1815     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1816       ? encode_entities("Purchase Order #". $cust_main->payinfo)
1817       : '';
1818
1819   my $money_char = $conf->config('money_char') || '$';
1820
1821   foreach my $line_item ( $self->_items ) {
1822     my $detail = {
1823       ext_description => [],
1824     };
1825     $detail->{'ref'} = $line_item->{'pkgnum'};
1826     $detail->{'description'} = encode_entities($line_item->{'description'});
1827     if ( exists $line_item->{'ext_description'} ) {
1828       @{$detail->{'ext_description'}} = map {
1829         encode_entities($_);
1830       } @{$line_item->{'ext_description'}};
1831     }
1832     $detail->{'amount'} = $money_char. $line_item->{'amount'};
1833     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1834
1835     push @{$invoice_data{'detail_items'}}, $detail;
1836   }
1837
1838
1839   my $taxtotal = 0;
1840   foreach my $tax ( $self->_items_tax ) {
1841     my $total = {};
1842     $total->{'total_item'} = encode_entities($tax->{'description'});
1843     $taxtotal += $tax->{'amount'};
1844     $total->{'total_amount'} = $money_char. $tax->{'amount'};
1845     push @{$invoice_data{'total_items'}}, $total;
1846   }
1847
1848   if ( $taxtotal ) {
1849     my $total = {};
1850     $total->{'total_item'} = 'Sub-total';
1851     $total->{'total_amount'} =
1852       $money_char. sprintf('%.2f', $self->charged - $taxtotal );
1853     unshift @{$invoice_data{'total_items'}}, $total;
1854   }
1855
1856   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1857   {
1858     my $total = {};
1859     $total->{'total_item'} = '<b>Total</b>';
1860     $total->{'total_amount'} =
1861       "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
1862     push @{$invoice_data{'total_items'}}, $total;
1863   }
1864
1865   #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1866
1867   # credits
1868   foreach my $credit ( $self->_items_credits ) {
1869     my $total;
1870     $total->{'total_item'} = encode_entities($credit->{'description'});
1871     #$credittotal
1872     $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
1873     push @{$invoice_data{'total_items'}}, $total;
1874   }
1875
1876   # payments
1877   foreach my $payment ( $self->_items_payments ) {
1878     my $total = {};
1879     $total->{'total_item'} = encode_entities($payment->{'description'});
1880     #$paymenttotal
1881     $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
1882     push @{$invoice_data{'total_items'}}, $total;
1883   }
1884
1885   { 
1886     my $total;
1887     $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
1888     $total->{'total_amount'} =
1889       "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
1890     push @{$invoice_data{'total_items'}}, $total;
1891   }
1892
1893   $html_template->fill_in( HASH => \%invoice_data);
1894 }
1895
1896 # quick subroutine for print_latex
1897 #
1898 # There are ten characters that LaTeX treats as special characters, which
1899 # means that they do not simply typeset themselves: 
1900 #      # $ % & ~ _ ^ \ { }
1901 #
1902 # TeX ignores blanks following an escaped character; if you want a blank (as
1903 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1904
1905 sub _latex_escape {
1906   my $value = shift;
1907   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
1908   $value =~ s/([<>])/\$$1\$/g;
1909   $value;
1910 }
1911
1912 #utility methods for print_*
1913
1914 sub balance_due_msg {
1915   my $self = shift;
1916   my $msg = 'Balance Due';
1917   return $msg unless $conf->exists('invoice_default_terms');
1918   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1919     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1920   } elsif ( $conf->config('invoice_default_terms') ) {
1921     $msg .= ' - '. $conf->config('invoice_default_terms');
1922   }
1923   $msg;
1924 }
1925
1926 sub _items {
1927   my $self = shift;
1928   my @display = scalar(@_)
1929                 ? @_
1930                 : qw( _items_previous _items_pkg );
1931                 #: qw( _items_pkg );
1932                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1933   my @b = ();
1934   foreach my $display ( @display ) {
1935     push @b, $self->$display(@_);
1936   }
1937   @b;
1938 }
1939
1940 sub _items_previous {
1941   my $self = shift;
1942   my $cust_main = $self->cust_main;
1943   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1944   my @b = ();
1945   foreach ( @pr_cust_bill ) {
1946     push @b, {
1947       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
1948                        ' ('. time2str('%x',$_->_date). ')',
1949       #'pkgpart'     => 'N/A',
1950       'pkgnum'      => 'N/A',
1951       'amount'      => sprintf("%.2f", $_->owed),
1952     };
1953   }
1954   @b;
1955
1956   #{
1957   #    'description'     => 'Previous Balance',
1958   #    #'pkgpart'         => 'N/A',
1959   #    'pkgnum'          => 'N/A',
1960   #    'amount'          => sprintf("%10.2f", $pr_total ),
1961   #    'ext_description' => [ map {
1962   #                                 "Invoice ". $_->invnum.
1963   #                                 " (". time2str("%x",$_->_date). ") ".
1964   #                                 sprintf("%10.2f", $_->owed)
1965   #                         } @pr_cust_bill ],
1966
1967   #};
1968 }
1969
1970 sub _items_pkg {
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_tax {
1977   my $self = shift;
1978   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1979   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1980 }
1981
1982 sub _items_cust_bill_pkg {
1983   my $self = shift;
1984   my $cust_bill_pkg = shift;
1985
1986   my @b = ();
1987   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1988
1989     if ( $cust_bill_pkg->pkgnum > 0 ) {
1990
1991       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1992       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1993       my $pkg = $part_pkg->pkg;
1994
1995       if ( $cust_bill_pkg->setup != 0 ) {
1996         my $description = $pkg;
1997         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1998         my @d = $cust_pkg->h_labels_short($self->_date);
1999         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
2000         push @b, {
2001           description     => $description,
2002           #pkgpart         => $part_pkg->pkgpart,
2003           pkgnum          => $cust_pkg->pkgnum,
2004           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
2005           ext_description => \@d,
2006         };
2007       }
2008
2009       if ( $cust_bill_pkg->recur != 0 ) {
2010         push @b, {
2011           description     => "$pkg (" .
2012                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
2013                                time2str('%x', $cust_bill_pkg->edate). ')',
2014           #pkgpart         => $part_pkg->pkgpart,
2015           pkgnum          => $cust_pkg->pkgnum,
2016           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
2017           ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
2018                                                          $cust_bill_pkg->sdate),
2019                                $cust_bill_pkg->details,
2020                              ],
2021         };
2022       }
2023
2024     } else { #pkgnum tax or one-shot line item (??)
2025
2026       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
2027                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
2028                      : 'Tax';
2029       if ( $cust_bill_pkg->setup != 0 ) {
2030         push @b, {
2031           'description' => $itemdesc,
2032           'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
2033         };
2034       }
2035       if ( $cust_bill_pkg->recur != 0 ) {
2036         push @b, {
2037           'description' => "$itemdesc (".
2038                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
2039                            time2str("%x", $cust_bill_pkg->edate). ')',
2040           'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
2041         };
2042       }
2043
2044     }
2045
2046   }
2047
2048   @b;
2049
2050 }
2051
2052 sub _items_credits {
2053   my $self = shift;
2054
2055   my @b;
2056   #credits
2057   foreach ( $self->cust_credited ) {
2058
2059     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
2060
2061     my $reason = $_->cust_credit->reason;
2062     #my $reason = substr($_->cust_credit->reason,0,32);
2063     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
2064     $reason = " ($reason) " if $reason;
2065     push @b, {
2066       #'description' => 'Credit ref\#'. $_->crednum.
2067       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2068       #                 $reason,
2069       'description' => 'Credit applied '.
2070                        time2str("%x",$_->cust_credit->_date). $reason,
2071       'amount'      => sprintf("%.2f",$_->amount),
2072     };
2073   }
2074   #foreach ( @cr_cust_credit ) {
2075   #  push @buf,[
2076   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
2077   #    $money_char. sprintf("%10.2f",$_->credited)
2078   #  ];
2079   #}
2080
2081   @b;
2082
2083 }
2084
2085 sub _items_payments {
2086   my $self = shift;
2087
2088   my @b;
2089   #get & print payments
2090   foreach ( $self->cust_bill_pay ) {
2091
2092     #something more elaborate if $_->amount ne ->cust_pay->paid ?
2093
2094     push @b, {
2095       'description' => "Payment received ".
2096                        time2str("%x",$_->cust_pay->_date ),
2097       'amount'      => sprintf("%.2f", $_->amount )
2098     };
2099   }
2100
2101   @b;
2102
2103 }
2104
2105 =back
2106
2107 =head1 BUGS
2108
2109 The delete method.
2110
2111 print_text formatting (and some logic :/) is in source, but needs to be
2112 slurped in from a file.  Also number of lines ($=).
2113
2114 =head1 SEE ALSO
2115
2116 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
2117 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
2118 documentation.
2119
2120 =cut
2121
2122 1;
2123