invoice_lines() fix
[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     'ECHECK',
631     $ach_processor,
632     $ach_login,
633     $ach_password,
634     $ach_action,
635     \@ach_options,
636     @_
637   );
638 }
639
640 =item realtime_lec
641
642 Attempts to pay this invoice with phone bill (LEC) payment via a
643 Business::OnlinePayment realtime gateway.  See
644 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
645 for supported processors.
646
647 =cut
648
649 sub realtime_lec {
650   my $self = shift;
651   $self->realtime_bop(
652     'LEC',
653     $bop_processor,
654     $bop_login,
655     $bop_password,
656     $bop_action,
657     \@bop_options,
658     @_
659   );
660 }
661
662 sub realtime_bop {
663   my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
664   my $cust_main = $self->cust_main;
665   my $amount = $self->owed;
666
667   my $address = $cust_main->address1;
668   $address .= ", ". $cust_main->address2 if $cust_main->address2;
669
670   my($payname, $payfirst, $paylast);
671   if ( $cust_main->payname && $method ne 'ECHECK' ) {
672     $payname = $cust_main->payname;
673     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
674       or do {
675               #$dbh->rollback if $oldAutoCommit;
676               return "Illegal payname $payname";
677             };
678     ($payfirst, $paylast) = ($1, $2);
679   } else {
680     $payfirst = $cust_main->getfield('first');
681     $paylast = $cust_main->getfield('last');
682     $payname =  "$payfirst $paylast";
683   }
684
685   my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
686   if ( $conf->exists('emailinvoiceauto')
687        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
688     push @invoicing_list, $cust_main->all_emails;
689   }
690   my $email = $invoicing_list[0];
691
692   my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
693
694   my $description = 'Internet Services';
695   if ( $conf->exists('business-onlinepayment-description') ) {
696     my $dtempl = $conf->config('business-onlinepayment-description');
697
698     my $agent_obj = $cust_main->agent
699       or die "can't retreive agent for $cust_main (agentnum ".
700              $cust_main->agentnum. ")";
701     my $agent = $agent_obj->agent;
702     my $pkgs = join(', ',
703       map { $_->cust_pkg->part_pkg->pkg }
704         grep { $_->pkgnum } $self->cust_bill_pkg
705     );
706     $description = eval qq("$dtempl");
707
708   }
709
710   my %content;
711   if ( $method eq 'CC' ) { 
712     $content{card_number} = $cust_main->payinfo;
713     $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
714     $content{expiration} = "$2/$1";
715   } elsif ( $method eq 'ECHECK' ) {
716     my($account_number,$routing_code) = $cust_main->payinfo;
717     ( $content{account_number}, $content{routing_code} ) =
718       split('@', $cust_main->payinfo);
719     $content{bank_name} = $cust_main->payname;
720   } elsif ( $method eq 'LEC' ) {
721     $content{phone} = $cust_main->payinfo;
722   }
723   
724   my $transaction =
725     new Business::OnlinePayment( $processor, @$options );
726   $transaction->content(
727     'type'           => $method,
728     'login'          => $login,
729     'password'       => $password,
730     'action'         => $action1,
731     'description'    => $description,
732     'amount'         => $amount,
733     'invoice_number' => $self->invnum,
734     'customer_id'    => $self->custnum,
735     'last_name'      => $paylast,
736     'first_name'     => $payfirst,
737     'name'           => $payname,
738     'address'        => $address,
739     'city'           => $cust_main->city,
740     'state'          => $cust_main->state,
741     'zip'            => $cust_main->zip,
742     'country'        => $cust_main->country,
743     'referer'        => 'http://cleanwhisker.420.am/',
744     'email'          => $email,
745     'phone'          => $cust_main->daytime || $cust_main->night,
746     %content, #after
747   );
748   $transaction->submit();
749
750   if ( $transaction->is_success() && $action2 ) {
751     my $auth = $transaction->authorization;
752     my $ordernum = $transaction->can('order_number')
753                    ? $transaction->order_number
754                    : '';
755
756     #warn "********* $auth ***********\n";
757     #warn "********* $ordernum ***********\n";
758     my $capture =
759       new Business::OnlinePayment( $processor, @$options );
760
761     my %capture = (
762       %content,
763       type           => $method,
764       action         => $action2,
765       login          => $login,
766       password       => $password,
767       order_number   => $ordernum,
768       amount         => $amount,
769       authorization  => $auth,
770       description    => $description,
771     );
772
773     foreach my $field (qw( authorization_source_code returned_ACI                                          transaction_identifier validation_code           
774                            transaction_sequence_num local_transaction_date    
775                            local_transaction_time AVS_result_code          )) {
776       $capture{$field} = $transaction->$field() if $transaction->can($field);
777     }
778
779     $capture->content( %capture );
780
781     $capture->submit();
782
783     unless ( $capture->is_success ) {
784       my $e = "Authorization sucessful but capture failed, invnum #".
785               $self->invnum. ': '.  $capture->result_code.
786               ": ". $capture->error_message;
787       warn $e;
788       return $e;
789     }
790
791   }
792
793   if ( $transaction->is_success() ) {
794
795     my %method2payby = (
796       'CC'     => 'CARD',
797       'ECHECK' => 'CHEK',
798       'LEC'    => 'LECB',
799     );
800
801     my $cust_pay = new FS::cust_pay ( {
802        'invnum'   => $self->invnum,
803        'paid'     => $amount,
804        '_date'     => '',
805        'payby'    => method2payby{$method},
806        'payinfo'  => $cust_main->payinfo,
807        'paybatch' => "$processor:". $transaction->authorization,
808     } );
809     my $error = $cust_pay->insert;
810     if ( $error ) {
811       # gah, even with transactions.
812       my $e = 'WARNING: Card/ACH debited but database not updated - '.
813               'error applying payment, invnum #' . $self->invnum.
814               " ($processor): $error";
815       warn $e;
816       return $e;
817     } else {
818       return '';
819     }
820   #} elsif ( $options{'report_badcard'} ) {
821   } else {
822
823     my $perror = "$processor error, invnum #". $self->invnum. ': '.
824                  $transaction->result_code. ": ". $transaction->error_message;
825
826     if ( $conf->exists('emaildecline')
827          && grep { $_ ne 'POST' } $cust_main->invoicing_list
828     ) {
829       my @templ = $conf->config('declinetemplate');
830       my $template = new Text::Template (
831         TYPE   => 'ARRAY',
832         SOURCE => [ map "$_\n", @templ ],
833       ) or return "($perror) can't create template: $Text::Template::ERROR";
834       $template->compile()
835         or return "($perror) can't compile template: $Text::Template::ERROR";
836
837       my $templ_hash = { error => $transaction->error_message };
838
839       #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
840       $ENV{MAILADDRESS} = $invoice_from;
841       my $header = new Mail::Header ( [
842         "From: $invoice_from",
843         "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
844         "Sender: $invoice_from",
845         "Reply-To: $invoice_from",
846         "Date: ". time2str("%a, %d %b %Y %X %z", time),
847         "Subject: Your payment could not be processed",
848       ] );
849       my $message = new Mail::Internet (
850         'Header' => $header,
851         'Body' => [ $template->fill_in(HASH => $templ_hash) ],
852       );
853       $!=0;
854       $message->smtpsend( Host => $smtpmachine )
855         or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
856           or return "($perror) (customer # ". $self->custnum.
857             ") can't send card decline email to ".
858             join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
859             " via server $smtpmachine with SMTP: $!";
860     }
861   
862     return $perror;
863   }
864
865 }
866
867 =item batch_card
868
869 Adds a payment for this invoice to the pending credit card batch (see
870 L<FS::cust_pay_batch>).
871
872 =cut
873
874 sub batch_card {
875   my $self = shift;
876   my $cust_main = $self->cust_main;
877
878   my $cust_pay_batch = new FS::cust_pay_batch ( {
879     'invnum'   => $self->getfield('invnum'),
880     'custnum'  => $cust_main->getfield('custnum'),
881     'last'     => $cust_main->getfield('last'),
882     'first'    => $cust_main->getfield('first'),
883     'address1' => $cust_main->getfield('address1'),
884     'address2' => $cust_main->getfield('address2'),
885     'city'     => $cust_main->getfield('city'),
886     'state'    => $cust_main->getfield('state'),
887     'zip'      => $cust_main->getfield('zip'),
888     'country'  => $cust_main->getfield('country'),
889     'trancode' => 77,
890     'cardnum'  => $cust_main->getfield('payinfo'),
891     'exp'      => $cust_main->getfield('paydate'),
892     'payname'  => $cust_main->getfield('payname'),
893     'amount'   => $self->owed,
894   } );
895   my $error = $cust_pay_batch->insert;
896   die $error if $error;
897
898   '';
899 }
900
901 =item print_text [TIME];
902
903 Returns an text invoice, as a list of lines.
904
905 TIME an optional value used to control the printing of overdue messages.  The
906 default is now.  It isn't the date of the invoice; that's the `_date' field.
907 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
908 L<Time::Local> and L<Date::Parse> for conversion functions.
909
910 =cut
911
912 sub print_text {
913
914   my( $self, $today, $template ) = @_;
915   $today ||= time;
916 #  my $invnum = $self->invnum;
917   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
918   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
919     unless $cust_main->payname && $cust_main->payby ne 'CHEK';
920
921   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
922 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
923   #my $balance_due = $self->owed + $pr_total - $cr_total;
924   my $balance_due = $self->owed + $pr_total;
925
926   #my @collect = ();
927   #my($description,$amount);
928   @buf = ();
929
930   #previous balance
931   foreach ( @pr_cust_bill ) {
932     push @buf, [
933       "Previous Balance, Invoice #". $_->invnum. 
934                  " (". time2str("%x",$_->_date). ")",
935       $money_char. sprintf("%10.2f",$_->owed)
936     ];
937   }
938   if (@pr_cust_bill) {
939     push @buf,['','-----------'];
940     push @buf,[ 'Total Previous Balance',
941                 $money_char. sprintf("%10.2f",$pr_total ) ];
942     push @buf,['',''];
943   }
944
945   #new charges
946   foreach ( ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
947             ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
948   ) {
949
950     if ( $_->pkgnum ) {
951
952       my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
953       my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
954       my($pkg)=$part_pkg->pkg;
955
956       if ( $_->setup != 0 ) {
957         push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
958         push @buf,
959           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
960       }
961
962       if ( $_->recur != 0 ) {
963         push @buf, [
964           "$pkg (" . time2str("%x",$_->sdate) . " - " .
965                                 time2str("%x",$_->edate) . ")",
966           $money_char. sprintf("%10.2f",$_->recur)
967         ];
968         push @buf,
969           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
970       }
971
972     } else { #pkgnum tax
973       my $itemdesc = defined $_->dbdef_table->column('itemdesc')
974                      ? ( $_->itemdesc || 'Tax' )
975                      : 'Tax';
976       push @buf,[$itemdesc, $money_char. sprintf("%10.2f",$_->setup) ] 
977         if $_->setup != 0;
978     }
979   }
980
981   push @buf,['','-----------'];
982   push @buf,['Total New Charges',
983              $money_char. sprintf("%10.2f",$self->charged) ];
984   push @buf,['',''];
985
986   push @buf,['','-----------'];
987   push @buf,['Total Charges',
988              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
989   push @buf,['',''];
990
991   #credits
992   foreach ( $self->cust_credited ) {
993
994     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
995
996     my $reason = substr($_->cust_credit->reason,0,32);
997     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
998     $reason = " ($reason) " if $reason;
999     push @buf,[
1000       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1001         $reason,
1002       $money_char. sprintf("%10.2f",$_->amount)
1003     ];
1004   }
1005   #foreach ( @cr_cust_credit ) {
1006   #  push @buf,[
1007   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1008   #    $money_char. sprintf("%10.2f",$_->credited)
1009   #  ];
1010   #}
1011
1012   #get & print payments
1013   foreach ( $self->cust_bill_pay ) {
1014
1015     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1016
1017     push @buf,[
1018       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1019       $money_char. sprintf("%10.2f",$_->amount )
1020     ];
1021   }
1022
1023   #balance due
1024   push @buf,['','-----------'];
1025   push @buf,['Balance Due', $money_char. 
1026     sprintf("%10.2f", $balance_due ) ];
1027
1028   #create the template
1029   my $templatefile = 'invoice_template';
1030   $templatefile .= "_$template" if $template;
1031   my @invoice_template = $conf->config($templatefile)
1032   or die "cannot load config file $templatefile";
1033   $invoice_lines = 0;
1034   my $wasfunc = 0;
1035   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1036     /invoice_lines\((\d*)\)/;
1037     $invoice_lines += $1 || scalar(@buf);
1038     $wasfunc=1;
1039   }
1040   die "no invoice_lines() functions in template?" unless $wasfunc;
1041   my $invoice_template = new Text::Template (
1042     TYPE   => 'ARRAY',
1043     SOURCE => [ map "$_\n", @invoice_template ],
1044   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1045   $invoice_template->compile()
1046     or die "can't compile template: $Text::Template::ERROR";
1047
1048   #setup template variables
1049   package FS::cust_bill::_template; #!
1050   use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
1051
1052   $invnum = $self->invnum;
1053   $date = $self->_date;
1054   $page = 1;
1055
1056   if ( $FS::cust_bill::invoice_lines ) {
1057     $total_pages =
1058       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1059     $total_pages++
1060       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1061   } else {
1062     $total_pages = 1;
1063   }
1064
1065   #format address (variable for the template)
1066   my $l = 0;
1067   @address = ( '', '', '', '', '', '' );
1068   package FS::cust_bill; #!
1069   $FS::cust_bill::_template::address[$l++] =
1070     $cust_main->payname.
1071       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1072         ? " (P.O. #". $cust_main->payinfo. ")"
1073         : ''
1074       )
1075   ;
1076   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1077     if $cust_main->company;
1078   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1079   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1080     if $cust_main->address2;
1081   $FS::cust_bill::_template::address[$l++] =
1082     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1083   $FS::cust_bill::_template::address[$l++] = $cust_main->country
1084     unless $cust_main->country eq 'US';
1085
1086         #  #overdue? (variable for the template)
1087         #  $FS::cust_bill::_template::overdue = ( 
1088         #    $balance_due > 0
1089         #    && $today > $self->_date 
1090         ##    && $self->printed > 1
1091         #    && $self->printed > 0
1092         #  );
1093
1094   #and subroutine for the template
1095   sub FS::cust_bill::_template::invoice_lines {
1096     my $lines = shift || scalar(@buf);
1097     map { 
1098       scalar(@buf) ? shift @buf : [ '', '' ];
1099     }
1100     ( 1 .. $lines );
1101   }
1102
1103   #and fill it in
1104   $FS::cust_bill::_template::page = 1;
1105   my $lines;
1106   my @collect;
1107   while (@buf) {
1108     push @collect, split("\n",
1109       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1110     );
1111     $FS::cust_bill::_template::page++;
1112   }
1113
1114   map "$_\n", @collect;
1115
1116 }
1117
1118 =back
1119
1120 =head1 VERSION
1121
1122 $Id: cust_bill.pm,v 1.57 2002-12-17 21:31:20 ivan Exp $
1123
1124 =head1 BUGS
1125
1126 The delete method.
1127
1128 print_text formatting (and some logic :/) is in source, but needs to be
1129 slurped in from a file.  Also number of lines ($=).
1130
1131 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1132 or something similar so the look can be completely customized?)
1133
1134 =head1 SEE ALSO
1135
1136 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1137 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
1138 documentation.
1139
1140 =cut
1141
1142 1;
1143