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