all taxes now have names. closes: Bug#15
[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 ) { #email invoice
356     #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
357     #$ENV{SMTPHOSTS} = $smtpmachine;
358     $ENV{MAILADDRESS} = $invoice_from;
359     my $header = new Mail::Header ( [
360       "From: $invoice_from",
361       "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
362       "Sender: $invoice_from",
363       "Reply-To: $invoice_from",
364       "Date: ". time2str("%a, %d %b %Y %X %z", time),
365       "Subject: Invoice",
366     ] );
367     my $message = new Mail::Internet (
368       'Header' => $header,
369       'Body' => [ @print_text ], #( date)
370     );
371     $!=0;
372     $message->smtpsend( Host => $smtpmachine )
373       or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
374         or return "(customer # ". $self->custnum. ") can't send invoice email".
375                   " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
376                   " via server $smtpmachine with SMTP: $!";
377
378   }
379
380   if ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) { #postal
381     open(LPR, "|$lpr")
382       or return "Can't open pipe to $lpr: $!";
383     print LPR @print_text;
384     close LPR
385       or return $! ? "Error closing $lpr: $!"
386                    : "Exit status $? from $lpr";
387   }
388
389   '';
390
391 }
392
393 =item send_csv OPTIONS
394
395 Sends invoice as a CSV data-file to a remote host with the specified protocol.
396
397 Options are:
398
399 protocol - currently only "ftp"
400 server
401 username
402 password
403 dir
404
405 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
406 and YYMMDDHHMMSS is a timestamp.
407
408 The fields of the CSV file is as follows:
409
410 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
411
412 =over 4
413
414 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
415
416 If B<record_type> is C<cust_bill>, this is a primary invoice record.  The
417 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
418 fields are filled in.
419
420 If B<record_type> is C<cust_bill_pkg>, this is a line item record.  Only the
421 first two fields (B<record_type> and B<invnum>) and the last five fields
422 (B<pkg> through B<edate>) are filled in.
423
424 =item invnum - invoice number
425
426 =item custnum - customer number
427
428 =item _date - invoice date
429
430 =item charged - total invoice amount
431
432 =item first - customer first name
433
434 =item last - customer first name
435
436 =item company - company name
437
438 =item address1 - address line 1
439
440 =item address2 - address line 1
441
442 =item city
443
444 =item state
445
446 =item zip
447
448 =item country
449
450 =item pkg - line item description
451
452 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
453
454 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
455
456 =item sdate - start date for recurring fee
457
458 =item edate - end date for recurring fee
459
460 =back
461
462 =cut
463
464 sub send_csv {
465   my($self, %opt) = @_;
466
467   #part one: create file
468
469   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
470   mkdir $spooldir, 0700 unless -d $spooldir;
471
472   my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
473
474   open(CSV, ">$file") or die "can't open $file: $!";
475
476   eval "use Text::CSV_XS";
477   die $@ if $@;
478
479   my $csv = Text::CSV_XS->new({'always_quote'=>1});
480
481   my $cust_main = $self->cust_main;
482
483   $csv->combine(
484     'cust_bill',
485     $self->invnum,
486     $self->custnum,
487     time2str("%x", $self->_date),
488     sprintf("%.2f", $self->charged),
489     ( map { $cust_main->getfield($_) }
490         qw( first last company address1 address2 city state zip country ) ),
491     map { '' } (1..5),
492   ) or die "can't create csv";
493   print CSV $csv->string. "\n";
494
495   #new charges (false laziness w/print_text)
496   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
497
498     my($pkg, $setup, $recur, $sdate, $edate);
499     if ( $cust_bill_pkg->pkgnum ) {
500     
501       ($pkg, $setup, $recur, $sdate, $edate) = (
502         $cust_bill_pkg->cust_pkg->part_pkg->pkg,
503         ( $cust_bill_pkg->setup != 0
504           ? sprintf("%.2f", $cust_bill_pkg->setup )
505           : '' ),
506         ( $cust_bill_pkg->recur != 0
507           ? sprintf("%.2f", $cust_bill_pkg->recur )
508           : '' ),
509         time2str("%x", $cust_bill_pkg->sdate),
510         time2str("%x", $cust_bill_pkg->edate),
511       );
512
513     } else { #pkgnum tax
514       next unless $cust_bill_pkg->setup != 0;
515       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
516                        ? ( $cust_bill_pkg->itemdesc || 'Tax' )
517                        : 'Tax';
518       ($pkg, $setup, $recur, $sdate, $edate) =
519         ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
520     }
521
522     $csv->combine(
523       'cust_bill_pkg',
524       $self->invnum,
525       ( map { '' } (1..11) ),
526       ($pkg, $setup, $recur, $sdate, $edate)
527     ) or die "can't create csv";
528     print CSV $csv->string. "\n";
529
530   }
531
532   close CSV or die "can't close CSV: $!";
533
534   #part two: upload it
535
536   my $net;
537   if ( $opt{protocol} eq 'ftp' ) {
538     eval "use Net::FTP;";
539     die $@ if $@;
540     $net = Net::FTP->new($opt{server}) or die @$;
541   } else {
542     die "unknown protocol: $opt{protocol}";
543   }
544
545   $net->login( $opt{username}, $opt{password} )
546     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
547
548   $net->binary or die "can't set binary mode";
549
550   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
551
552   $net->put($file) or die "can't put $file: $!";
553
554   $net->quit;
555
556   unlink $file;
557
558 }
559
560 =item comp
561
562 Pays this invoice with a compliemntary payment.  If there is an error,
563 returns the error, otherwise returns false.
564
565 =cut
566
567 sub comp {
568   my $self = shift;
569   my $cust_pay = new FS::cust_pay ( {
570     'invnum'   => $self->invnum,
571     'paid'     => $self->owed,
572     '_date'    => '',
573     'payby'    => 'COMP',
574     'payinfo'  => $self->cust_main->payinfo,
575     'paybatch' => '',
576   } );
577   $cust_pay->insert;
578 }
579
580 =item realtime_card
581
582 Attempts to pay this invoice with a Business::OnlinePayment realtime gateway.
583 See http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
584 for supproted processors.
585
586 =cut
587
588 sub realtime_card {
589   my $self = shift;
590   my $cust_main = $self->cust_main;
591   my $amount = $self->owed;
592
593   unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
594     return "Real-time card processing not enabled (processor $processor)";
595   }
596   my $bop_processor = $1; #hmm?
597
598   my $address = $cust_main->address1;
599   $address .= ", ". $cust_main->address2 if $cust_main->address2;
600
601   #fix exp. date
602   #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
603   $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
604   my $exp = "$2/$1";
605
606   my($payname, $payfirst, $paylast);
607   if ( $cust_main->payname ) {
608     $payname = $cust_main->payname;
609     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
610       or do {
611               #$dbh->rollback if $oldAutoCommit;
612               return "Illegal payname $payname";
613             };
614     ($payfirst, $paylast) = ($1, $2);
615   } else {
616     $payfirst = $cust_main->getfield('first');
617     $paylast = $cust_main->getfield('last');
618     $payname =  "$payfirst $paylast";
619   }
620
621   my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
622   if ( $conf->exists('emailinvoiceauto')
623        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
624     push @invoicing_list, $cust_main->all_emails;
625   }
626   my $email = $invoicing_list[0];
627
628   my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
629
630   my $description = 'Internet Services';
631   if ( $conf->exists('business-onlinepayment-description') ) {
632     my $dtempl = $conf->config('business-onlinepayment-description');
633
634     my $agent_obj = $cust_main->agent
635       or die "can't retreive agent for $cust_main (agentnum ".
636              $cust_main->agentnum. ")";
637     my $agent = $agent_obj->agent;
638     my $pkgs = join(', ',
639       map { $_->cust_pkg->part_pkg->pkg }
640         grep { $_->pkgnum } $self->cust_bill_pkg
641     );
642     $description = eval qq("$dtempl");
643
644   }
645   
646   my $transaction =
647     new Business::OnlinePayment( $bop_processor, @bop_options );
648   $transaction->content(
649     'type'           => 'CC',
650     'login'          => $bop_login,
651     'password'       => $bop_password,
652     'action'         => $action1,
653     'description'    => $description,
654     'amount'         => $amount,
655     'invoice_number' => $self->invnum,
656     'customer_id'    => $self->custnum,
657     'last_name'      => $paylast,
658     'first_name'     => $payfirst,
659     'name'           => $payname,
660     'address'        => $address,
661     'city'           => $cust_main->city,
662     'state'          => $cust_main->state,
663     'zip'            => $cust_main->zip,
664     'country'        => $cust_main->country,
665     'card_number'    => $cust_main->payinfo,
666     'expiration'     => $exp,
667     'referer'        => 'http://cleanwhisker.420.am/',
668     'email'          => $email,
669     'phone'          => $cust_main->daytime || $cust_main->night,
670   );
671   $transaction->submit();
672
673   if ( $transaction->is_success() && $action2 ) {
674     my $auth = $transaction->authorization;
675     my $ordernum = $transaction->can('order_number')
676                    ? $transaction->order_number
677                    : '';
678
679     #warn "********* $auth ***********\n";
680     #warn "********* $ordernum ***********\n";
681     my $capture =
682       new Business::OnlinePayment( $bop_processor, @bop_options );
683
684     my %capture = (
685       type           => 'CC',
686       action         => $action2,
687       login          => $bop_login,
688       password       => $bop_password,
689       order_number   => $ordernum,
690       amount         => $amount,
691       authorization  => $auth,
692       description    => $description,
693       card_number    => $cust_main->payinfo,
694       expiration     => $exp,
695     );
696
697     foreach my $field (qw( authorization_source_code returned_ACI                                          transaction_identifier validation_code           
698                            transaction_sequence_num local_transaction_date    
699                            local_transaction_time AVS_result_code          )) {
700       $capture{$field} = $transaction->$field() if $transaction->can($field);
701     }
702
703     $capture->content( %capture );
704
705     $capture->submit();
706
707     unless ( $capture->is_success ) {
708       my $e = "Authorization sucessful but capture failed, invnum #".
709               $self->invnum. ': '.  $capture->result_code.
710               ": ". $capture->error_message;
711       warn $e;
712       return $e;
713     }
714
715   }
716
717   if ( $transaction->is_success() ) {
718
719     my $cust_pay = new FS::cust_pay ( {
720        'invnum'   => $self->invnum,
721        'paid'     => $amount,
722        '_date'     => '',
723        'payby'    => 'CARD',
724        'payinfo'  => $cust_main->payinfo,
725        'paybatch' => "$processor:". $transaction->authorization,
726     } );
727     my $error = $cust_pay->insert;
728     if ( $error ) {
729       # gah, even with transactions.
730       my $e = 'WARNING: Card debited but database not updated - '.
731               'error applying payment, invnum #' . $self->invnum.
732               " ($processor): $error";
733       warn $e;
734       return $e;
735     } else {
736       return '';
737     }
738   #} elsif ( $options{'report_badcard'} ) {
739   } else {
740
741     my $perror = "$processor error, invnum #". $self->invnum. ': '.
742                  $transaction->result_code. ": ". $transaction->error_message;
743
744     if ( $conf->exists('emaildecline')
745          && grep { $_ ne 'POST' } $cust_main->invoicing_list
746     ) {
747       my @templ = $conf->config('declinetemplate');
748       my $template = new Text::Template (
749         TYPE   => 'ARRAY',
750         SOURCE => [ map "$_\n", @templ ],
751       ) or return "($perror) can't create template: $Text::Template::ERROR";
752       $template->compile()
753         or return "($perror) can't compile template: $Text::Template::ERROR";
754
755       my $templ_hash = { error => $transaction->error_message };
756
757       #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
758       $ENV{MAILADDRESS} = $invoice_from;
759       my $header = new Mail::Header ( [
760         "From: $invoice_from",
761         "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
762         "Sender: $invoice_from",
763         "Reply-To: $invoice_from",
764         "Date: ". time2str("%a, %d %b %Y %X %z", time),
765         "Subject: Your credit card could not be processed",
766       ] );
767       my $message = new Mail::Internet (
768         'Header' => $header,
769         'Body' => [ $template->fill_in(HASH => $templ_hash) ],
770       );
771       $!=0;
772       $message->smtpsend( Host => $smtpmachine )
773         or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
774           or return "($perror) (customer # ". $self->custnum.
775             ") can't send card decline email to ".
776             join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
777             " via server $smtpmachine with SMTP: $!";
778     }
779   
780     return $perror;
781   }
782
783 }
784
785 =item batch_card
786
787 Adds a payment for this invoice to the pending credit card batch (see
788 L<FS::cust_pay_batch>).
789
790 =cut
791
792 sub batch_card {
793   my $self = shift;
794   my $cust_main = $self->cust_main;
795
796   my $cust_pay_batch = new FS::cust_pay_batch ( {
797     'invnum'   => $self->getfield('invnum'),
798     'custnum'  => $cust_main->getfield('custnum'),
799     'last'     => $cust_main->getfield('last'),
800     'first'    => $cust_main->getfield('first'),
801     'address1' => $cust_main->getfield('address1'),
802     'address2' => $cust_main->getfield('address2'),
803     'city'     => $cust_main->getfield('city'),
804     'state'    => $cust_main->getfield('state'),
805     'zip'      => $cust_main->getfield('zip'),
806     'country'  => $cust_main->getfield('country'),
807     'trancode' => 77,
808     'cardnum'  => $cust_main->getfield('payinfo'),
809     'exp'      => $cust_main->getfield('paydate'),
810     'payname'  => $cust_main->getfield('payname'),
811     'amount'   => $self->owed,
812   } );
813   my $error = $cust_pay_batch->insert;
814   die $error if $error;
815
816   '';
817 }
818
819 =item print_text [TIME];
820
821 Returns an text invoice, as a list of lines.
822
823 TIME an optional value used to control the printing of overdue messages.  The
824 default is now.  It isn't the date of the invoice; that's the `_date' field.
825 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
826 L<Time::Local> and L<Date::Parse> for conversion functions.
827
828 =cut
829
830 sub print_text {
831
832   my( $self, $today, $template ) = @_;
833   $today ||= time;
834 #  my $invnum = $self->invnum;
835   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
836   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
837     unless $cust_main->payname;
838
839   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
840 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
841   #my $balance_due = $self->owed + $pr_total - $cr_total;
842   my $balance_due = $self->owed + $pr_total;
843
844   #my @collect = ();
845   #my($description,$amount);
846   @buf = ();
847
848   #previous balance
849   foreach ( @pr_cust_bill ) {
850     push @buf, [
851       "Previous Balance, Invoice #". $_->invnum. 
852                  " (". time2str("%x",$_->_date). ")",
853       $money_char. sprintf("%10.2f",$_->owed)
854     ];
855   }
856   if (@pr_cust_bill) {
857     push @buf,['','-----------'];
858     push @buf,[ 'Total Previous Balance',
859                 $money_char. sprintf("%10.2f",$pr_total ) ];
860     push @buf,['',''];
861   }
862
863   #new charges
864   foreach ( ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
865             ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
866   ) {
867
868     if ( $_->pkgnum ) {
869
870       my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
871       my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
872       my($pkg)=$part_pkg->pkg;
873
874       if ( $_->setup != 0 ) {
875         push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
876         push @buf,
877           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
878       }
879
880       if ( $_->recur != 0 ) {
881         push @buf, [
882           "$pkg (" . time2str("%x",$_->sdate) . " - " .
883                                 time2str("%x",$_->edate) . ")",
884           $money_char. sprintf("%10.2f",$_->recur)
885         ];
886         push @buf,
887           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
888       }
889
890     } else { #pkgnum tax
891       my $itemdesc = defined $_->dbdef_table->column('itemdesc')
892                      ? ( $_->itemdesc || 'Tax' )
893                      : 'Tax';
894       push @buf,[$itemdesc, $money_char. sprintf("%10.2f",$_->setup) ] 
895         if $_->setup != 0;
896     }
897   }
898
899   push @buf,['','-----------'];
900   push @buf,['Total New Charges',
901              $money_char. sprintf("%10.2f",$self->charged) ];
902   push @buf,['',''];
903
904   push @buf,['','-----------'];
905   push @buf,['Total Charges',
906              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
907   push @buf,['',''];
908
909   #credits
910   foreach ( $self->cust_credited ) {
911
912     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
913
914     my $reason = substr($_->cust_credit->reason,0,32);
915     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
916     $reason = " ($reason) " if $reason;
917     push @buf,[
918       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
919         $reason,
920       $money_char. sprintf("%10.2f",$_->amount)
921     ];
922   }
923   #foreach ( @cr_cust_credit ) {
924   #  push @buf,[
925   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
926   #    $money_char. sprintf("%10.2f",$_->credited)
927   #  ];
928   #}
929
930   #get & print payments
931   foreach ( $self->cust_bill_pay ) {
932
933     #something more elaborate if $_->amount ne ->cust_pay->paid ?
934
935     push @buf,[
936       "Payment received ". time2str("%x",$_->cust_pay->_date ),
937       $money_char. sprintf("%10.2f",$_->amount )
938     ];
939   }
940
941   #balance due
942   push @buf,['','-----------'];
943   push @buf,['Balance Due', $money_char. 
944     sprintf("%10.2f", $balance_due ) ];
945
946   #create the template
947   my $templatefile = 'invoice_template';
948   $templatefile .= "_$template" if $template;
949   my @invoice_template = $conf->config($templatefile)
950   or die "cannot load config file $templatefile";
951   $invoice_lines = 0;
952   my $wasfunc = 0;
953   foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
954     /invoice_lines\((\d+)\)/;
955     $invoice_lines += $1;
956     $wasfunc=1;
957   }
958   die "no invoice_lines() functions in template?" unless $wasfunc;
959   my $invoice_template = new Text::Template (
960     TYPE   => 'ARRAY',
961     SOURCE => [ map "$_\n", @invoice_template ],
962   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
963   $invoice_template->compile()
964     or die "can't compile template: $Text::Template::ERROR";
965
966   #setup template variables
967   package FS::cust_bill::_template; #!
968   use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
969
970   $invnum = $self->invnum;
971   $date = $self->_date;
972   $page = 1;
973
974   if ( $FS::cust_bill::invoice_lines ) {
975     $total_pages =
976       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
977     $total_pages++
978       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
979   } else {
980     $total_pages = 1;
981   }
982
983   #format address (variable for the template)
984   my $l = 0;
985   @address = ( '', '', '', '', '', '' );
986   package FS::cust_bill; #!
987   $FS::cust_bill::_template::address[$l++] =
988     $cust_main->payname.
989       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
990         ? " (P.O. #". $cust_main->payinfo. ")"
991         : ''
992       )
993   ;
994   $FS::cust_bill::_template::address[$l++] = $cust_main->company
995     if $cust_main->company;
996   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
997   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
998     if $cust_main->address2;
999   $FS::cust_bill::_template::address[$l++] =
1000     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1001   $FS::cust_bill::_template::address[$l++] = $cust_main->country
1002     unless $cust_main->country eq 'US';
1003
1004         #  #overdue? (variable for the template)
1005         #  $FS::cust_bill::_template::overdue = ( 
1006         #    $balance_due > 0
1007         #    && $today > $self->_date 
1008         ##    && $self->printed > 1
1009         #    && $self->printed > 0
1010         #  );
1011
1012   #and subroutine for the template
1013
1014   sub FS::cust_bill::_template::invoice_lines {
1015     my $lines = shift or return @buf;
1016     map { 
1017       scalar(@buf) ? shift @buf : [ '', '' ];
1018     }
1019     ( 1 .. $lines );
1020   }
1021
1022
1023   #and fill it in
1024   $FS::cust_bill::_template::page = 1;
1025   my $lines;
1026   my @collect;
1027   while (@buf) {
1028     push @collect, split("\n",
1029       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1030     );
1031     $FS::cust_bill::_template::page++;
1032   }
1033
1034   map "$_\n", @collect;
1035
1036 }
1037
1038 =back
1039
1040 =head1 VERSION
1041
1042 $Id: cust_bill.pm,v 1.46 2002-09-21 11:17:39 ivan Exp $
1043
1044 =head1 BUGS
1045
1046 The delete method.
1047
1048 print_text formatting (and some logic :/) is in source, but needs to be
1049 slurped in from a file.  Also number of lines ($=).
1050
1051 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1052 or something similar so the look can be completely customized?)
1053
1054 =head1 SEE ALSO
1055
1056 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1057 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
1058 documentation.
1059
1060 =cut
1061
1062 1;
1063