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