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