c3524b98429a854b2e1318e54943760f6ffcec47
[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     $!=0;
406     $message->smtpsend( Host => $smtpmachine )
407       or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
408         or return "(customer # ". $self->custnum. ") can't send invoice email".
409                   " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
410                   " via server $smtpmachine with SMTP: $!";
411
412   #} elsif ( grep { $_ eq 'POST' } @invoicing_list ) {
413   } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
414     open(LPR, "|$lpr")
415       or return "Can't open pipe to $lpr: $!";
416     print LPR $self->print_text; #( date )
417     close LPR
418       or return $! ? "Error closing $lpr: $!"
419                    : "Exit status $? from $lpr";
420   }
421
422   '';
423
424 }
425
426 =item comp
427
428 Pays this invoice with a compliemntary payment.  If there is an error,
429 returns the error, otherwise returns false.
430
431 =cut
432
433 sub comp {
434   my $self = shift;
435   my $cust_pay = new FS::cust_pay ( {
436     'invnum'   => $self->invnum,
437     'paid'     => $self->owed,
438     '_date'    => '',
439     'payby'    => 'COMP',
440     'payinfo'  => $self->cust_main->payinfo,
441     'paybatch' => '',
442   } );
443   $cust_pay->insert;
444 }
445
446 =item realtime_card
447
448 Attempts to pay this invoice with a Business::OnlinePayment realtime gateway.
449 See http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
450 for supproted processors.
451
452 =cut
453
454 sub realtime_card {
455   my $self = shift;
456   my $cust_main = $self->cust_main;
457   my $amount = $self->owed;
458
459   unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
460     return "Real-time card processing not enabled (processor $processor)";
461   }
462   my $bop_processor = $1; #hmm?
463
464   my $address = $cust_main->address1;
465   $address .= ", ". $cust_main->address2 if $cust_main->address2;
466
467   #fix exp. date
468   #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
469   $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
470   my $exp = "$2/$1";
471
472   my($payname, $payfirst, $paylast);
473   if ( $cust_main->payname ) {
474     $payname = $cust_main->payname;
475     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)$/
476       or do {
477               #$dbh->rollback if $oldAutoCommit;
478               return "Illegal payname $payname";
479             };
480     ($payfirst, $paylast) = ($1, $2);
481   } else {
482     $payfirst = $cust_main->getfield('first');
483     $paylast = $cust_main->getfield('first');
484     $payname =  "$payfirst $paylast";
485   }
486
487   my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
488   if ( $conf->exists('emailinvoiceauto')
489        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
490     push @invoicing_list, $cust_main->default_invoicing_list;
491   }
492   my $email = $invoicing_list[0];
493
494   my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
495   
496   my $transaction =
497     new Business::OnlinePayment( $bop_processor, @bop_options );
498   $transaction->content(
499     'type'           => 'CC',
500     'login'          => $bop_login,
501     'password'       => $bop_password,
502     'action'         => $action1,
503     'description'    => 'Internet Services',
504     'amount'         => $amount,
505     'invoice_number' => $self->invnum,
506     'customer_id'    => $self->custnum,
507     'last_name'      => $paylast,
508     'first_name'     => $payfirst,
509     'name'           => $payname,
510     'address'        => $address,
511     'city'           => $cust_main->city,
512     'state'          => $cust_main->state,
513     'zip'            => $cust_main->zip,
514     'country'        => $cust_main->country,
515     'card_number'    => $cust_main->payinfo,
516     'expiration'     => $exp,
517     'referer'        => 'http://cleanwhisker.420.am/',
518     'email'          => $email,
519   );
520   $transaction->submit();
521
522   if ( $transaction->is_success() && $action2 ) {
523     my $auth = $transaction->authorization;
524     my $ordernum = $transaction->order_number;
525     #warn "********* $auth ***********\n";
526     #warn "********* $ordernum ***********\n";
527     my $capture =
528       new Business::OnlinePayment( $bop_processor, @bop_options );
529
530     $capture->content(
531       action         => $action2,
532       login          => $bop_login,
533       password       => $bop_password,
534       order_number   => $ordernum,
535       amount         => $amount,
536       authorization  => $auth,
537       description    => 'Internet Services',
538     );
539
540     $capture->submit();
541
542     unless ( $capture->is_success ) {
543       my $e = "Authorization sucessful but capture failed, invnum #".
544               $self->invnum. ': '.  $capture->result_code.
545               ": ". $capture->error_message;
546       warn $e;
547       return $e;
548     }
549
550   }
551
552   if ( $transaction->is_success() ) {
553
554     my $cust_pay = new FS::cust_pay ( {
555        'invnum'   => $self->invnum,
556        'paid'     => $amount,
557        '_date'     => '',
558        'payby'    => 'CARD',
559        'payinfo'  => $cust_main->payinfo,
560        'paybatch' => "$processor:". $transaction->authorization,
561     } );
562     my $error = $cust_pay->insert;
563     if ( $error ) {
564       # gah, even with transactions.
565       my $e = 'WARNING: Card debited but database not updated - '.
566               'error applying payment, invnum #' . $self->invnum.
567               " ($processor): $error";
568       warn $e;
569       return $e;
570     } else {
571       return '';
572     }
573   #} elsif ( $options{'report_badcard'} ) {
574   } else {
575     return "$processor error, invnum #". $self->invnum. ': '.
576            $transaction->result_code. ": ". $transaction->error_message;
577   }
578
579 }
580
581 =item realtime_card_cybercash
582
583 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
584
585 =cut
586
587 sub realtime_card_cybercash {
588   my $self = shift;
589   my $cust_main = $self->cust_main;
590   my $amount = $self->owed;
591
592   return "CyberCash CashRegister real-time card processing not enabled!"
593     unless $processor eq 'cybercash3.2';
594
595   my $address = $cust_main->address1;
596   $address .= ", ". $cust_main->address2 if $cust_main->address2;
597
598   #fix exp. date
599   #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
600   $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
601   my $exp = "$2/$1";
602
603   #
604
605   my $paybatch = $self->invnum. 
606                   '-' . time2str("%y%m%d%H%M%S", time);
607
608   my $payname = $cust_main->payname ||
609                 $cust_main->getfield('first').' '.$cust_main->getfield('last');
610
611   my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
612
613   my @full_xaction = ( $xaction,
614     'Order-ID'     => $paybatch,
615     'Amount'       => "usd $amount",
616     'Card-Number'  => $cust_main->getfield('payinfo'),
617     'Card-Name'    => $payname,
618     'Card-Address' => $address,
619     'Card-City'    => $cust_main->getfield('city'),
620     'Card-State'   => $cust_main->getfield('state'),
621     'Card-Zip'     => $cust_main->getfield('zip'),
622     'Card-Country' => $country,
623     'Card-Exp'     => $exp,
624   );
625
626   my %result;
627   %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
628   
629   if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
630     my $cust_pay = new FS::cust_pay ( {
631        'invnum'   => $self->invnum,
632        'paid'     => $amount,
633        '_date'     => '',
634        'payby'    => 'CARD',
635        'payinfo'  => $cust_main->payinfo,
636        'paybatch' => "$processor:$paybatch",
637     } );
638     my $error = $cust_pay->insert;
639     if ( $error ) {
640       # gah, even with transactions.
641       my $e = 'WARNING: Card debited but database not updated - '.
642               'error applying payment, invnum #' . $self->invnum.
643               " (CyberCash Order-ID $paybatch): $error";
644       warn $e;
645       return $e;
646     } else {
647       return '';
648     }
649 #  } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
650 #            || $options{'report_badcard'}
651 #          ) {
652   } else {
653      return 'Cybercash error, invnum #' . 
654        $self->invnum. ':'. $result{'MErrMsg'};
655   }
656
657 }
658
659 =item batch_card
660
661 Adds a payment for this invoice to the pending credit card batch (see
662 L<FS::cust_pay_batch>).
663
664 =cut
665
666 sub batch_card {
667   my $self = shift;
668   my $cust_main = $self->cust_main;
669
670   my $cust_pay_batch = new FS::cust_pay_batch ( {
671     'invnum'   => $self->getfield('invnum'),
672     'custnum'  => $cust_main->getfield('custnum'),
673     'last'     => $cust_main->getfield('last'),
674     'first'    => $cust_main->getfield('first'),
675     'address1' => $cust_main->getfield('address1'),
676     'address2' => $cust_main->getfield('address2'),
677     'city'     => $cust_main->getfield('city'),
678     'state'    => $cust_main->getfield('state'),
679     'zip'      => $cust_main->getfield('zip'),
680     'country'  => $cust_main->getfield('country'),
681     'trancode' => 77,
682     'cardnum'  => $cust_main->getfield('payinfo'),
683     'exp'      => $cust_main->getfield('paydate'),
684     'payname'  => $cust_main->getfield('payname'),
685     'amount'   => $self->owed,
686   } );
687   $cust_pay_batch->insert;
688
689 }
690
691 =item print_text [TIME];
692
693 Returns an text invoice, as a list of lines.
694
695 TIME an optional value used to control the printing of overdue messages.  The
696 default is now.  It isn't the date of the invoice; that's the `_date' field.
697 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
698 L<Time::Local> and L<Date::Parse> for conversion functions.
699
700 =cut
701
702 sub print_text {
703
704   my( $self, $today ) = ( shift, shift );
705   $today ||= time;
706 #  my $invnum = $self->invnum;
707   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
708   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
709     unless $cust_main->payname;
710
711   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
712 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
713   #my $balance_due = $self->owed + $pr_total - $cr_total;
714   my $balance_due = $self->owed + $pr_total;
715
716   #my @collect = ();
717   #my($description,$amount);
718   @buf = ();
719
720   #previous balance
721   foreach ( @pr_cust_bill ) {
722     push @buf, [
723       "Previous Balance, Invoice #". $_->invnum. 
724                  " (". time2str("%x",$_->_date). ")",
725       $money_char. sprintf("%10.2f",$_->owed)
726     ];
727   }
728   if (@pr_cust_bill) {
729     push @buf,['','-----------'];
730     push @buf,[ 'Total Previous Balance',
731                 $money_char. sprintf("%10.2f",$pr_total ) ];
732     push @buf,['',''];
733   }
734
735   #new charges
736   foreach ( $self->cust_bill_pkg ) {
737
738     if ( $_->pkgnum ) {
739
740       my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
741       my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
742       my($pkg)=$part_pkg->pkg;
743
744       if ( $_->setup != 0 ) {
745         push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
746         push @buf,
747           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
748       }
749
750       if ( $_->recur != 0 ) {
751         push @buf, [
752           "$pkg (" . time2str("%x",$_->sdate) . " - " .
753                                 time2str("%x",$_->edate) . ")",
754           $money_char. sprintf("%10.2f",$_->recur)
755         ];
756         push @buf,
757           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
758       }
759
760     } else { #pkgnum Tax
761       push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ] 
762         if $_->setup != 0;
763     }
764   }
765
766   push @buf,['','-----------'];
767   push @buf,['Total New Charges',
768              $money_char. sprintf("%10.2f",$self->charged) ];
769   push @buf,['',''];
770
771   push @buf,['','-----------'];
772   push @buf,['Total Charges',
773              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
774   push @buf,['',''];
775
776   #credits
777   foreach ( $self->cust_credited ) {
778
779     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
780
781     my $reason = substr($_->cust_credit->reason,0,32);
782     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
783     $reason = " ($reason) " if $reason;
784     push @buf,[
785       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
786         $reason,
787       $money_char. sprintf("%10.2f",$_->amount)
788     ];
789   }
790   #foreach ( @cr_cust_credit ) {
791   #  push @buf,[
792   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
793   #    $money_char. sprintf("%10.2f",$_->credited)
794   #  ];
795   #}
796
797   #get & print payments
798   foreach ( $self->cust_bill_pay ) {
799
800     #something more elaborate if $_->amount ne ->cust_pay->paid ?
801
802     push @buf,[
803       "Payment received ". time2str("%x",$_->cust_pay->_date ),
804       $money_char. sprintf("%10.2f",$_->amount )
805     ];
806   }
807
808   #balance due
809   push @buf,['','-----------'];
810   push @buf,['Balance Due', $money_char. 
811     sprintf("%10.2f", $balance_due ) ];
812
813   #setup template variables
814   
815   package FS::cust_bill::_template; #!
816   use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
817
818   $invnum = $self->invnum;
819   $date = $self->_date;
820   $page = 1;
821
822   $total_pages =
823     int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
824   $total_pages++
825     if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
826
827
828   #format address (variable for the template)
829   my $l = 0;
830   @address = ( '', '', '', '', '', '' );
831   package FS::cust_bill; #!
832   $FS::cust_bill::_template::address[$l++] =
833     $cust_main->payname.
834       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
835         ? " (P.O. #". $cust_main->payinfo. ")"
836         : ''
837       )
838   ;
839   $FS::cust_bill::_template::address[$l++] = $cust_main->company
840     if $cust_main->company;
841   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
842   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
843     if $cust_main->address2;
844   $FS::cust_bill::_template::address[$l++] =
845     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
846   $FS::cust_bill::_template::address[$l++] = $cust_main->country
847     unless $cust_main->country eq 'US';
848
849   #overdue? (variable for the template)
850   $FS::cust_bill::_template::overdue = ( 
851     $balance_due > 0
852     && $today > $self->_date 
853 #    && $self->printed > 1
854     && $self->printed > 0
855   );
856
857   #and subroutine for the template
858
859   sub FS::cust_bill::_template::invoice_lines {
860     my $lines = shift;
861     map { 
862       scalar(@buf) ? shift @buf : [ '', '' ];
863     }
864     ( 1 .. $lines );
865   }
866     
867   $FS::cust_bill::_template::page = 1;
868   my $lines;
869   my @collect;
870   while (@buf) {
871     push @collect, split("\n",
872       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
873     );
874     $FS::cust_bill::_template::page++;
875   }
876
877   map "$_\n", @collect;
878
879 }
880
881 =back
882
883 =head1 VERSION
884
885 $Id: cust_bill.pm,v 1.22 2002-03-07 14:13:21 ivan Exp $
886
887 =head1 BUGS
888
889 The delete method.
890
891 print_text formatting (and some logic :/) is in source, but needs to be
892 slurped in from a file.  Also number of lines ($=).
893
894 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
895 or something similar so the look can be completely customized?)
896
897 =head1 SEE ALSO
898
899 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
900 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
901 documentation.
902
903 =cut
904
905 1;
906