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