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