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