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