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