pod typo
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $conf $invoice_template $money_char );
5 use vars qw( $lpr $invoice_from $smtpmachine );
6 use vars qw( $processor );
7 use vars qw( $xaction $E_NoErr );
8 use vars qw( $bop_processor $bop_login $bop_password $bop_action @bop_options );
9 use vars qw( $invoice_lines @buf ); #yuck
10 use Date::Format;
11 use Mail::Internet;
12 use Mail::Header;
13 use Text::Template;
14 use FS::Record qw( qsearch qsearchs );
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
23 @ISA = qw( FS::Record );
24
25 #ask FS::UID to run this stuff for us later
26 $FS::UID::callback{'FS::cust_bill'} = sub { 
27
28   $conf = new FS::Conf;
29
30   $money_char = $conf->config('money_char') || '$';  
31
32   my @invoice_template = $conf->config('invoice_template')
33     or die "cannot load config file invoice_template";
34   $invoice_lines = 0;
35   foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
36     /invoice_lines\((\d+)\)/;
37     $invoice_lines += $1;
38   }
39   die "no invoice_lines() functions in template?" unless $invoice_lines;
40   $invoice_template = new Text::Template (
41     TYPE   => 'ARRAY',
42     SOURCE => [ map "$_\n", @invoice_template ],
43   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
44   $invoice_template->compile()
45     or die "can't compile template: $Text::Template::ERROR";
46
47   $lpr = $conf->config('lpr');
48   $invoice_from = $conf->config('invoice_from');
49   $smtpmachine = $conf->config('smtpmachine');
50
51   if ( $conf->exists('cybercash3.2') ) {
52     require CCMckLib3_2;
53       #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
54     require CCMckDirectLib3_2;
55       #qw(SendCC2_1Server);
56     require CCMckErrno3_2;
57       #qw(MCKGetErrorMessage $E_NoErr);
58     import CCMckErrno3_2 qw($E_NoErr);
59
60     my $merchant_conf;
61     ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
62     my $status = &CCMckLib3_2::InitConfig($merchant_conf);
63     if ( $status != $E_NoErr ) {
64       warn "CCMckLib3_2::InitConfig error:\n";
65       foreach my $key (keys %CCMckLib3_2::Config) {
66         warn "  $key => $CCMckLib3_2::Config{$key}\n"
67       }
68       my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
69       die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
70     }
71     $processor='cybercash3.2';
72   } elsif ( $conf->exists('business-onlinepayment') ) {
73     ( $bop_processor,
74       $bop_login,
75       $bop_password,
76       $bop_action,
77       @bop_options
78     ) = $conf->config('business-onlinepayment');
79     $bop_action ||= 'normal authorization';
80     eval "use Business::OnlinePayment";  
81     $processor="Business::OnlinePayment::$bop_processor";
82   }
83
84 };
85
86 =head1 NAME
87
88 FS::cust_bill - Object methods for cust_bill records
89
90 =head1 SYNOPSIS
91
92   use FS::cust_bill;
93
94   $record = new FS::cust_bill \%hash;
95   $record = new FS::cust_bill { 'column' => 'value' };
96
97   $error = $record->insert;
98
99   $error = $new_record->replace($old_record);
100
101   $error = $record->delete;
102
103   $error = $record->check;
104
105   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
106
107   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
108
109   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
110
111   @cust_pay_objects = $cust_bill->cust_pay;
112
113   $tax_amount = $record->tax;
114
115   @lines = $cust_bill->print_text;
116   @lines = $cust_bill->print_text $time;
117
118 =head1 DESCRIPTION
119
120 An FS::cust_bill object represents an invoice; a declaration that a customer
121 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
122 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
123 following fields are currently supported:
124
125 =over 4
126
127 =item invnum - primary key (assigned automatically for new invoices)
128
129 =item custnum - customer (see L<FS::cust_main>)
130
131 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
132 L<Time::Local> and L<Date::Parse> for conversion functions.
133
134 =item charged - amount of this invoice
135
136 =item printed - deprecated
137
138 =item closed - books closed flag, empty or `Y'
139
140 =back
141
142 =head1 METHODS
143
144 =over 4
145
146 =item new HASHREF
147
148 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
149 Invoices are normally created by calling the bill method of a customer object
150 (see L<FS::cust_main>).
151
152 =cut
153
154 sub table { 'cust_bill'; }
155
156 =item insert
157
158 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
159 returns the error, otherwise returns false.
160
161 =item delete
162
163 Currently unimplemented.  I don't remove invoices because there would then be
164 no record you ever posted this invoice (which is bad, no?)
165
166 =cut
167
168 sub delete {
169   my $self = shift;
170   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
171   $self->SUPER::delete(@_);
172 }
173
174 =item replace OLD_RECORD
175
176 Replaces the OLD_RECORD with this one in the database.  If there is an error,
177 returns the error, otherwise returns false.
178
179 Only printed may be changed.  printed is normally updated by calling the
180 collect method of a customer object (see L<FS::cust_main>).
181
182 =cut
183
184 sub replace {
185   my( $new, $old ) = ( shift, shift );
186   return "Can't change custnum!" unless $old->custnum == $new->custnum;
187   #return "Can't change _date!" unless $old->_date eq $new->_date;
188   return "Can't change _date!" unless $old->_date == $new->_date;
189   return "Can't change charged!" unless $old->charged == $new->charged;
190
191   $new->SUPER::replace($old);
192 }
193
194 =item check
195
196 Checks all fields to make sure this is a valid invoice.  If there is an error,
197 returns the error, otherwise returns false.  Called by the insert and replace
198 methods.
199
200 =cut
201
202 sub check {
203   my $self = shift;
204
205   my $error =
206     $self->ut_numbern('invnum')
207     || $self->ut_number('custnum')
208     || $self->ut_numbern('_date')
209     || $self->ut_money('charged')
210     || $self->ut_numbern('printed')
211     || $self->ut_enum('closed', [ '', 'Y' ])
212   ;
213   return $error if $error;
214
215   return "Unknown customer"
216     unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
217
218   $self->_date(time) unless $self->_date;
219
220   $self->printed(0) if $self->printed eq '';
221
222   ''; #no error
223 }
224
225 =item previous
226
227 Returns a list consisting of the total previous balance for this customer, 
228 followed by the previous outstanding invoices (as FS::cust_bill objects also).
229
230 =cut
231
232 sub previous {
233   my $self = shift;
234   my $total = 0;
235   my @cust_bill = sort { $a->_date <=> $b->_date }
236     grep { $_->owed != 0 && $_->_date < $self->_date }
237       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
238   ;
239   foreach ( @cust_bill ) { $total += $_->owed; }
240   $total, @cust_bill;
241 }
242
243 =item cust_bill_pkg
244
245 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
246
247 =cut
248
249 sub cust_bill_pkg {
250   my $self = shift;
251   qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
252 }
253
254 =item cust_main
255
256 Returns the customer (see L<FS::cust_main>) for this invoice.
257
258 =cut
259
260 sub cust_main {
261   my $self = shift;
262   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
263 }
264
265 =item cust_credit
266
267 Depreciated.  See the cust_credited method.
268
269  #Returns a list consisting of the total previous credited (see
270  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
271  #outstanding credits (FS::cust_credit objects).
272
273 =cut
274
275 sub cust_credit {
276   use Carp;
277   croak "FS::cust_bill->cust_credit depreciated; see ".
278         "FS::cust_bill->cust_credit_bill";
279   #my $self = shift;
280   #my $total = 0;
281   #my @cust_credit = sort { $a->_date <=> $b->_date }
282   #  grep { $_->credited != 0 && $_->_date < $self->_date }
283   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
284   #;
285   #foreach (@cust_credit) { $total += $_->credited; }
286   #$total, @cust_credit;
287 }
288
289 =item cust_pay
290
291 Depreciated.  See the cust_bill_pay method.
292
293 #Returns all payments (see L<FS::cust_pay>) for this invoice.
294
295 =cut
296
297 sub cust_pay {
298   use Carp;
299   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
300   #my $self = shift;
301   #sort { $a->_date <=> $b->_date }
302   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
303   #;
304 }
305
306 =item cust_bill_pay
307
308 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
309
310 =cut
311
312 sub cust_bill_pay {
313   my $self = shift;
314   sort { $a->_date <=> $b->_date }
315     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
316 }
317
318 =item cust_credited
319
320 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
321
322 =cut
323
324 sub cust_credited {
325   my $self = shift;
326   sort { $a->_date <=> $b->_date }
327     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
328   ;
329 }
330
331 =item tax
332
333 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
334
335 =cut
336
337 sub tax {
338   my $self = shift;
339   my $total = 0;
340   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
341                                              'pkgnum' => 0 } );
342   foreach (@taxlines) { $total += $_->setup; }
343   $total;
344 }
345
346 =item owed
347
348 Returns the amount owed (still outstanding) on this invoice, which is charged
349 minus all payment applications (see L<FS::cust_bill_pay>) and credit
350 applications (see L<FS::cust_credit_bill>).
351
352 =cut
353
354 sub owed {
355   my $self = shift;
356   my $balance = $self->charged;
357   $balance -= $_->amount foreach ( $self->cust_bill_pay );
358   $balance -= $_->amount foreach ( $self->cust_credited );
359   $balance = sprintf( "%.2f", $balance);
360   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
361   $balance;
362 }
363
364 =item send
365
366 Sends this invoice to the destinations configured for this customer: send
367 emails or print.  See L<FS::cust_main_invoice>.
368
369 =cut
370
371 sub send {
372   my $self = shift;
373
374   #my @print_text = $cust_bill->print_text; #( date )
375   my @invoicing_list = $self->cust_main->invoicing_list;
376   if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
377     $ENV{SMTPHOSTS} = $smtpmachine;
378     $ENV{MAILADDRESS} = $invoice_from;
379     my $header = new Mail::Header ( [
380       "From: $invoice_from",
381       "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
382       "Sender: $invoice_from",
383       "Reply-To: $invoice_from",
384       "Date: ". time2str("%a, %d %b %Y %X %z", time),
385       "Subject: Invoice",
386     ] );
387     my $message = new Mail::Internet (
388       'Header' => $header,
389       'Body' => [ $self->print_text ], #( date)
390     );
391     $message->smtpsend
392       or return "Can't send invoice email to server $smtpmachine!";
393
394   #} elsif ( grep { $_ eq 'POST' } @invoicing_list ) {
395   } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
396     open(LPR, "|$lpr")
397       or return "Can't open pipe to $lpr: $!";
398     print LPR $self->print_text; #( date )
399     close LPR
400       or return $! ? "Error closing $lpr: $!"
401                    : "Exit status $? from $lpr";
402   }
403
404   '';
405
406 }
407
408 =item comp
409
410 Pays this invoice with a compliemntary payment.  If there is an error,
411 returns the error, otherwise returns false.
412
413 =cut
414
415 sub comp {
416   my $self = shift;
417   my $cust_pay = new FS::cust_pay ( {
418     'invnum'   => $self->invnum,
419     'paid'     => $self->owed,
420     '_date'    => '',
421     'payby'    => 'COMP',
422     'payinfo'  => $self->cust_main->payinfo,
423     'paybatch' => '',
424   } );
425   $cust_pay->insert;
426 }
427
428 =item realtime_card
429
430 Attempts to pay this invoice with a Business::OnlinePayment realtime gateway.
431 See http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
432 for supproted processors.
433
434 =cut
435
436 sub realtime_card {
437   my $self = shift;
438   my $cust_main = $self->cust_main;
439   my $amount = $self->owed;
440
441   unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
442     return "Real-time card processing not enabled (processor $processor)";
443   }
444   my $bop_processor = $1; #hmm?
445
446   my $address = $cust_main->address1;
447   $address .= ", ". $cust_main->address2 if $cust_main->address2;
448
449   #fix exp. date
450   #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
451   $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
452   my $exp = "$2/$1";
453
454   my($payname, $payfirst, $paylast);
455   if ( $cust_main->payname ) {
456     $payname = $cust_main->payname;
457     $payname =~ /^\s*([\w \,\.\-\']*\w)?\s+([\w\,\.\-\']+)$/
458       or do {
459               #$dbh->rollback if $oldAutoCommit;
460               return "Illegal payname $payname";
461             };
462     ($payfirst, $paylast) = ($1, $2);
463   } else {
464     $payfirst = $cust_main->getfield('first');
465     $paylast = $cust_main->getfield('first');
466     $payname =  "$payfirst $paylast";
467   }
468
469   my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
470   if ( $conf->exists('emailinvoiceauto')
471        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
472     push @invoicing_list, $cust_main->default_invoicing_list;
473   }
474   my $email = $invoicing_list[0];
475
476   my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
477   
478   my $transaction =
479     new Business::OnlinePayment( $bop_processor, @bop_options );
480   $transaction->content(
481     'type'           => 'CC',
482     'login'          => $bop_login,
483     'password'       => $bop_password,
484     'action'         => $action1,
485     'description'    => 'Internet Services',
486     'amount'         => $amount,
487     'invoice_number' => $self->invnum,
488     'customer_id'    => $self->custnum,
489     'last_name'      => $paylast,
490     'first_name'     => $payfirst,
491     'name'           => $payname,
492     'address'        => $address,
493     'city'           => $cust_main->city,
494     'state'          => $cust_main->state,
495     'zip'            => $cust_main->zip,
496     'country'        => $cust_main->country,
497     'card_number'    => $cust_main->payinfo,
498     'expiration'     => $exp,
499     'referer'        => 'http://cleanwhisker.420.am/',
500     'email'          => $email,
501   );
502   $transaction->submit();
503
504   if ( $transaction->is_success() && $action2 ) {
505     my $auth = $transaction->authorization;
506     my $ordernum = $transaction->order_number;
507     #warn "********* $auth ***********\n";
508     #warn "********* $ordernum ***********\n";
509     my $capture =
510       new Business::OnlinePayment( $bop_processor, @bop_options );
511
512     $capture->content(
513       action         => $action2,
514       login          => $bop_login,
515       password       => $bop_password,
516       order_number   => $ordernum,
517       amount         => $amount,
518       authorization  => $auth,
519       description    => 'Internet Services',
520     );
521
522     $capture->submit();
523
524     unless ( $capture->is_success ) {
525       my $e = "Authorization sucessful but capture failed, invnum #".
526               $self->invnum. ': '.  $capture->result_code.
527               ": ". $capture->error_message;
528       warn $e;
529       return $e;
530     }
531
532   }
533
534   if ( $transaction->is_success() ) {
535
536     my $cust_pay = new FS::cust_pay ( {
537        'invnum'   => $self->invnum,
538        'paid'     => $amount,
539        '_date'     => '',
540        'payby'    => 'CARD',
541        'payinfo'  => $cust_main->payinfo,
542        'paybatch' => "$processor:". $transaction->authorization,
543     } );
544     my $error = $cust_pay->insert;
545     if ( $error ) {
546       # gah, even with transactions.
547       my $e = 'WARNING: Card debited but database not updated - '.
548               'error applying payment, invnum #' . $self->invnum.
549               " ($processor): $error";
550       warn $e;
551       return $e;
552     } else {
553       return '';
554     }
555   #} elsif ( $options{'report_badcard'} ) {
556   } else {
557     return "$processor error, invnum #". $self->invnum. ': '.
558            $transaction->result_code. ": ". $transaction->error_message;
559   }
560
561 }
562
563 =item realtime_card_cybercash
564
565 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
566
567 =cut
568
569 sub realtime_card_cybercash {
570   my $self = shift;
571   my $cust_main = $self->cust_main;
572   my $amount = $self->owed;
573
574   return "CyberCash CashRegister real-time card processing not enabled!"
575     unless $processor eq 'cybercash3.2';
576
577   my $address = $cust_main->address1;
578   $address .= ", ". $cust_main->address2 if $cust_main->address2;
579
580   #fix exp. date
581   #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
582   $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
583   my $exp = "$2/$1";
584
585   #
586
587   my $paybatch = $self->invnum. 
588                   '-' . time2str("%y%m%d%H%M%S", time);
589
590   my $payname = $cust_main->payname ||
591                 $cust_main->getfield('first').' '.$cust_main->getfield('last');
592
593   my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
594
595   my @full_xaction = ( $xaction,
596     'Order-ID'     => $paybatch,
597     'Amount'       => "usd $amount",
598     'Card-Number'  => $cust_main->getfield('payinfo'),
599     'Card-Name'    => $payname,
600     'Card-Address' => $address,
601     'Card-City'    => $cust_main->getfield('city'),
602     'Card-State'   => $cust_main->getfield('state'),
603     'Card-Zip'     => $cust_main->getfield('zip'),
604     'Card-Country' => $country,
605     'Card-Exp'     => $exp,
606   );
607
608   my %result;
609   %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
610   
611   if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
612     my $cust_pay = new FS::cust_pay ( {
613        'invnum'   => $self->invnum,
614        'paid'     => $amount,
615        '_date'     => '',
616        'payby'    => 'CARD',
617        'payinfo'  => $cust_main->payinfo,
618        'paybatch' => "$processor:$paybatch",
619     } );
620     my $error = $cust_pay->insert;
621     if ( $error ) {
622       # gah, even with transactions.
623       my $e = 'WARNING: Card debited but database not updated - '.
624               'error applying payment, invnum #' . $self->invnum.
625               " (CyberCash Order-ID $paybatch): $error";
626       warn $e;
627       return $e;
628     } else {
629       return '';
630     }
631 #  } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
632 #            || $options{'report_badcard'}
633 #          ) {
634   } else {
635      return 'Cybercash error, invnum #' . 
636        $self->invnum. ':'. $result{'MErrMsg'};
637   }
638
639 }
640
641 =item batch_card
642
643 Adds a payment for this invoice to the pending credit card batch (see
644 L<FS::cust_pay_batch>).
645
646 =cut
647
648 sub batch_card {
649   my $self = shift;
650   my $cust_main = $self->cust_main;
651
652   my $cust_pay_batch = new FS::cust_pay_batch ( {
653     'invnum'   => $self->getfield('invnum'),
654     'custnum'  => $cust_main->getfield('custnum'),
655     'last'     => $cust_main->getfield('last'),
656     'first'    => $cust_main->getfield('first'),
657     'address1' => $cust_main->getfield('address1'),
658     'address2' => $cust_main->getfield('address2'),
659     'city'     => $cust_main->getfield('city'),
660     'state'    => $cust_main->getfield('state'),
661     'zip'      => $cust_main->getfield('zip'),
662     'country'  => $cust_main->getfield('country'),
663     'trancode' => 77,
664     'cardnum'  => $cust_main->getfield('payinfo'),
665     'exp'      => $cust_main->getfield('paydate'),
666     'payname'  => $cust_main->getfield('payname'),
667     'amount'   => $self->owed,
668   } );
669   $cust_pay_batch->insert;
670
671 }
672
673 =item print_text [TIME];
674
675 Returns an text invoice, as a list of lines.
676
677 TIME an optional value used to control the printing of overdue messages.  The
678 default is now.  It isn't the date of the invoice; that's the `_date' field.
679 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
680 L<Time::Local> and L<Date::Parse> for conversion functions.
681
682 =cut
683
684 sub print_text {
685
686   my( $self, $today ) = ( shift, shift );
687   $today ||= time;
688 #  my $invnum = $self->invnum;
689   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
690   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
691     unless $cust_main->payname;
692
693   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
694 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
695   #my $balance_due = $self->owed + $pr_total - $cr_total;
696   my $balance_due = $self->owed + $pr_total;
697
698   #my @collect = ();
699   #my($description,$amount);
700   @buf = ();
701
702   #previous balance
703   foreach ( @pr_cust_bill ) {
704     push @buf, [
705       "Previous Balance, Invoice #". $_->invnum. 
706                  " (". time2str("%x",$_->_date). ")",
707       $money_char. sprintf("%10.2f",$_->owed)
708     ];
709   }
710   if (@pr_cust_bill) {
711     push @buf,['','-----------'];
712     push @buf,[ 'Total Previous Balance',
713                 $money_char. sprintf("%10.2f",$pr_total ) ];
714     push @buf,['',''];
715   }
716
717   #new charges
718   foreach ( $self->cust_bill_pkg ) {
719
720     if ( $_->pkgnum ) {
721
722       my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
723       my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
724       my($pkg)=$part_pkg->pkg;
725
726       if ( $_->setup != 0 ) {
727         push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
728         push @buf,
729           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
730       }
731
732       if ( $_->recur != 0 ) {
733         push @buf, [
734           "$pkg (" . time2str("%x",$_->sdate) . " - " .
735                                 time2str("%x",$_->edate) . ")",
736           $money_char. sprintf("%10.2f",$_->recur)
737         ];
738         push @buf,
739           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
740       }
741
742     } else { #pkgnum Tax
743       push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ] 
744         if $_->setup != 0;
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   push @buf,['','-----------'];
792   push @buf,['Balance Due', $money_char. 
793     sprintf("%10.2f", $balance_due ) ];
794
795   #setup template variables
796   
797   package FS::cust_bill::_template; #!
798   use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
799
800   $invnum = $self->invnum;
801   $date = $self->_date;
802   $page = 1;
803
804   $total_pages =
805     int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
806   $total_pages++
807     if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
808
809
810   #format address (variable for the template)
811   my $l = 0;
812   @address = ( '', '', '', '', '', '' );
813   package FS::cust_bill; #!
814   $FS::cust_bill::_template::address[$l++] =
815     $cust_main->payname.
816       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
817         ? " (P.O. #". $cust_main->payinfo. ")"
818         : ''
819       )
820   ;
821   $FS::cust_bill::_template::address[$l++] = $cust_main->company
822     if $cust_main->company;
823   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
824   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
825     if $cust_main->address2;
826   $FS::cust_bill::_template::address[$l++] =
827     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
828   $FS::cust_bill::_template::address[$l++] = $cust_main->country
829     unless $cust_main->country eq 'US';
830
831   #overdue? (variable for the template)
832   $FS::cust_bill::_template::overdue = ( 
833     $balance_due > 0
834     && $today > $self->_date 
835 #    && $self->printed > 1
836     && $self->printed > 0
837   );
838
839   #and subroutine for the template
840
841   sub FS::cust_bill::_template::invoice_lines {
842     my $lines = shift;
843     map { 
844       scalar(@buf) ? shift @buf : [ '', '' ];
845     }
846     ( 1 .. $lines );
847   }
848     
849   $FS::cust_bill::_template::page = 1;
850   my $lines;
851   my @collect;
852   while (@buf) {
853     push @collect, split("\n",
854       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
855     );
856     $FS::cust_bill::_template::page++;
857   }
858
859   map "$_\n", @collect;
860
861 }
862
863 =back
864
865 =head1 VERSION
866
867 $Id: cust_bill.pm,v 1.17 2002-02-06 15:50:54 ivan Exp $
868
869 =head1 BUGS
870
871 The delete method.
872
873 print_text formatting (and some logic :/) is in source, but needs to be
874 slurped in from a file.  Also number of lines ($=).
875
876 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
877 or something similar so the look can be completely customized?)
878
879 =head1 SEE ALSO
880
881 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
882 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
883 documentation.
884
885 =cut
886
887 1;
888