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