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