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