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