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