4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $lpr $invoice_from $smtpmachine );
6 use vars qw( $cybercash );
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( $ach_processor $ach_login $ach_password $ach_action @ach_options );
10 use vars qw( $invoice_lines @buf ); #yuck
11 use vars qw( $realtime_bop_decline_quiet );
13 use Mail::Internet 1.44;
16 use FS::UID qw( datasrc );
17 use FS::Record qw( qsearch qsearchs );
19 use FS::cust_bill_pkg;
23 use FS::cust_credit_bill;
24 use FS::cust_pay_batch;
25 use FS::cust_bill_event;
27 @ISA = qw( FS::Record );
29 $realtime_bop_decline_quiet = 0;
31 #ask FS::UID to run this stuff for us later
32 $FS::UID::callback{'FS::cust_bill'} = sub {
36 $money_char = $conf->config('money_char') || '$';
38 $lpr = $conf->config('lpr');
39 $invoice_from = $conf->config('invoice_from');
40 $smtpmachine = $conf->config('smtpmachine');
42 ( $bop_processor,$bop_login, $bop_password, $bop_action ) = ( '', '', '', '');
44 ( $ach_processor,$ach_login, $ach_password, $ach_action ) = ( '', '', '', '');
47 if ( $conf->exists('cybercash3.2') ) {
49 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
50 require CCMckDirectLib3_2;
52 require CCMckErrno3_2;
53 #qw(MCKGetErrorMessage $E_NoErr);
54 import CCMckErrno3_2 qw($E_NoErr);
57 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
58 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
59 if ( $status != $E_NoErr ) {
60 warn "CCMckLib3_2::InitConfig error:\n";
61 foreach my $key (keys %CCMckLib3_2::Config) {
62 warn " $key => $CCMckLib3_2::Config{$key}\n"
64 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
65 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
67 $cybercash='cybercash3.2';
68 } elsif ( $conf->exists('business-onlinepayment') ) {
74 ) = $conf->config('business-onlinepayment');
75 $bop_action ||= 'normal authorization';
76 ( $ach_processor, $ach_login, $ach_password, $ach_action, @ach_options ) =
77 ( $bop_processor, $bop_login, $bop_password, $bop_action, @bop_options );
78 eval "use Business::OnlinePayment";
81 if ( $conf->exists('business-onlinepayment-ach') ) {
87 ) = $conf->config('business-onlinepayment-ach');
88 $ach_action ||= 'normal authorization';
89 eval "use Business::OnlinePayment";
96 FS::cust_bill - Object methods for cust_bill records
102 $record = new FS::cust_bill \%hash;
103 $record = new FS::cust_bill { 'column' => 'value' };
105 $error = $record->insert;
107 $error = $new_record->replace($old_record);
109 $error = $record->delete;
111 $error = $record->check;
113 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
115 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
117 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
119 @cust_pay_objects = $cust_bill->cust_pay;
121 $tax_amount = $record->tax;
123 @lines = $cust_bill->print_text;
124 @lines = $cust_bill->print_text $time;
128 An FS::cust_bill object represents an invoice; a declaration that a customer
129 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
130 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
131 following fields are currently supported:
135 =item invnum - primary key (assigned automatically for new invoices)
137 =item custnum - customer (see L<FS::cust_main>)
139 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
140 L<Time::Local> and L<Date::Parse> for conversion functions.
142 =item charged - amount of this invoice
144 =item printed - deprecated
146 =item closed - books closed flag, empty or `Y'
156 Creates a new invoice. To add the invoice to the database, see L<"insert">.
157 Invoices are normally created by calling the bill method of a customer object
158 (see L<FS::cust_main>).
162 sub table { 'cust_bill'; }
166 Adds this invoice to the database ("Posts" the invoice). If there is an error,
167 returns the error, otherwise returns false.
171 Currently unimplemented. I don't remove invoices because there would then be
172 no record you ever posted this invoice (which is bad, no?)
178 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
179 $self->SUPER::delete(@_);
182 =item replace OLD_RECORD
184 Replaces the OLD_RECORD with this one in the database. If there is an error,
185 returns the error, otherwise returns false.
187 Only printed may be changed. printed is normally updated by calling the
188 collect method of a customer object (see L<FS::cust_main>).
193 my( $new, $old ) = ( shift, shift );
194 return "Can't change custnum!" unless $old->custnum == $new->custnum;
195 #return "Can't change _date!" unless $old->_date eq $new->_date;
196 return "Can't change _date!" unless $old->_date == $new->_date;
197 return "Can't change charged!" unless $old->charged == $new->charged;
199 $new->SUPER::replace($old);
204 Checks all fields to make sure this is a valid invoice. If there is an error,
205 returns the error, otherwise returns false. Called by the insert and replace
214 $self->ut_numbern('invnum')
215 || $self->ut_number('custnum')
216 || $self->ut_numbern('_date')
217 || $self->ut_money('charged')
218 || $self->ut_numbern('printed')
219 || $self->ut_enum('closed', [ '', 'Y' ])
221 return $error if $error;
223 return "Unknown customer"
224 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
226 $self->_date(time) unless $self->_date;
228 $self->printed(0) if $self->printed eq '';
235 Returns a list consisting of the total previous balance for this customer,
236 followed by the previous outstanding invoices (as FS::cust_bill objects also).
243 my @cust_bill = sort { $a->_date <=> $b->_date }
244 grep { $_->owed != 0 && $_->_date < $self->_date }
245 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
247 foreach ( @cust_bill ) { $total += $_->owed; }
253 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
259 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
262 =item cust_bill_event
264 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
269 sub cust_bill_event {
271 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
277 Returns the customer (see L<FS::cust_main>) for this invoice.
283 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
288 Depreciated. See the cust_credited method.
290 #Returns a list consisting of the total previous credited (see
291 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
292 #outstanding credits (FS::cust_credit objects).
298 croak "FS::cust_bill->cust_credit depreciated; see ".
299 "FS::cust_bill->cust_credit_bill";
302 #my @cust_credit = sort { $a->_date <=> $b->_date }
303 # grep { $_->credited != 0 && $_->_date < $self->_date }
304 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
306 #foreach (@cust_credit) { $total += $_->credited; }
307 #$total, @cust_credit;
312 Depreciated. See the cust_bill_pay method.
314 #Returns all payments (see L<FS::cust_pay>) for this invoice.
320 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
322 #sort { $a->_date <=> $b->_date }
323 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
329 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
335 sort { $a->_date <=> $b->_date }
336 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
341 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
347 sort { $a->_date <=> $b->_date }
348 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
354 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
361 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
363 foreach (@taxlines) { $total += $_->setup; }
369 Returns the amount owed (still outstanding) on this invoice, which is charged
370 minus all payment applications (see L<FS::cust_bill_pay>) and credit
371 applications (see L<FS::cust_credit_bill>).
377 my $balance = $self->charged;
378 $balance -= $_->amount foreach ( $self->cust_bill_pay );
379 $balance -= $_->amount foreach ( $self->cust_credited );
380 $balance = sprintf( "%.2f", $balance);
381 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
387 Sends this invoice to the destinations configured for this customer: send
388 emails or print. See L<FS::cust_main_invoice>.
393 my($self,$template) = @_;
394 my @print_text = $self->print_text('', $template);
395 my @invoicing_list = $self->cust_main->invoicing_list;
397 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
399 #better to notify this person than silence
400 @invoicing_list = ($invoice_from) unless @invoicing_list;
402 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
403 #$ENV{SMTPHOSTS} = $smtpmachine;
404 $ENV{MAILADDRESS} = $invoice_from;
405 my $header = new Mail::Header ( [
406 "From: $invoice_from",
407 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
408 "Sender: $invoice_from",
409 "Reply-To: $invoice_from",
410 "Date: ". time2str("%a, %d %b %Y %X %z", time),
413 my $message = new Mail::Internet (
415 'Body' => [ @print_text ], #( date)
418 $message->smtpsend( Host => $smtpmachine )
419 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
420 or return "(customer # ". $self->custnum. ") can't send invoice email".
421 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
422 " via server $smtpmachine with SMTP: $!";
426 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
428 or return "Can't open pipe to $lpr: $!";
429 print LPR @print_text;
431 or return $! ? "Error closing $lpr: $!"
432 : "Exit status $? from $lpr";
439 =item send_csv OPTIONS
441 Sends invoice as a CSV data-file to a remote host with the specified protocol.
445 protocol - currently only "ftp"
451 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
452 and YYMMDDHHMMSS is a timestamp.
454 The fields of the CSV file is as follows:
456 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
460 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
462 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
463 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
464 fields are filled in.
466 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
467 first two fields (B<record_type> and B<invnum>) and the last five fields
468 (B<pkg> through B<edate>) are filled in.
470 =item invnum - invoice number
472 =item custnum - customer number
474 =item _date - invoice date
476 =item charged - total invoice amount
478 =item first - customer first name
480 =item last - customer first name
482 =item company - company name
484 =item address1 - address line 1
486 =item address2 - address line 1
496 =item pkg - line item description
498 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
500 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
502 =item sdate - start date for recurring fee
504 =item edate - end date for recurring fee
511 my($self, %opt) = @_;
513 #part one: create file
515 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
516 mkdir $spooldir, 0700 unless -d $spooldir;
518 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
520 open(CSV, ">$file") or die "can't open $file: $!";
522 eval "use Text::CSV_XS";
525 my $csv = Text::CSV_XS->new({'always_quote'=>1});
527 my $cust_main = $self->cust_main;
533 time2str("%x", $self->_date),
534 sprintf("%.2f", $self->charged),
535 ( map { $cust_main->getfield($_) }
536 qw( first last company address1 address2 city state zip country ) ),
538 ) or die "can't create csv";
539 print CSV $csv->string. "\n";
541 #new charges (false laziness w/print_text)
542 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
544 my($pkg, $setup, $recur, $sdate, $edate);
545 if ( $cust_bill_pkg->pkgnum ) {
547 ($pkg, $setup, $recur, $sdate, $edate) = (
548 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
549 ( $cust_bill_pkg->setup != 0
550 ? sprintf("%.2f", $cust_bill_pkg->setup )
552 ( $cust_bill_pkg->recur != 0
553 ? sprintf("%.2f", $cust_bill_pkg->recur )
555 time2str("%x", $cust_bill_pkg->sdate),
556 time2str("%x", $cust_bill_pkg->edate),
560 next unless $cust_bill_pkg->setup != 0;
561 ($pkg, $setup, $recur, $sdate, $edate) =
562 ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
568 ( map { '' } (1..11) ),
569 ($pkg, $setup, $recur, $sdate, $edate)
570 ) or die "can't create csv";
571 print CSV $csv->string. "\n";
575 close CSV or die "can't close CSV: $!";
580 if ( $opt{protocol} eq 'ftp' ) {
581 eval "use Net::FTP;";
583 $net = Net::FTP->new($opt{server}) or die @$;
585 die "unknown protocol: $opt{protocol}";
588 $net->login( $opt{username}, $opt{password} )
589 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
591 $net->binary or die "can't set binary mode";
593 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
595 $net->put($file) or die "can't put $file: $!";
605 Pays this invoice with a compliemntary payment. If there is an error,
606 returns the error, otherwise returns false.
612 my $cust_pay = new FS::cust_pay ( {
613 'invnum' => $self->invnum,
614 'paid' => $self->owed,
617 'payinfo' => $self->cust_main->payinfo,
625 Attempts to pay this invoice with a credit card payment via a
626 Business::OnlinePayment realtime gateway. See
627 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
628 for supported processors.
647 Attempts to pay this invoice with an electronic check (ACH) payment via a
648 Business::OnlinePayment realtime gateway. See
649 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
650 for supported processors.
669 Attempts to pay this invoice with phone bill (LEC) payment via a
670 Business::OnlinePayment realtime gateway. See
671 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
672 for supported processors.
690 my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
692 #trim an extraneous blank line
693 pop @$options if scalar(@$options) % 2 && $options->[-1] =~ /^\s*$/;
695 my $cust_main = $self->cust_main;
696 my $amount = $self->owed;
698 my $address = $cust_main->address1;
699 $address .= ", ". $cust_main->address2 if $cust_main->address2;
701 my($payname, $payfirst, $paylast);
702 if ( $cust_main->payname && $method ne 'ECHECK' ) {
703 $payname = $cust_main->payname;
704 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
706 #$dbh->rollback if $oldAutoCommit;
707 return "Illegal payname $payname";
709 ($payfirst, $paylast) = ($1, $2);
711 $payfirst = $cust_main->getfield('first');
712 $paylast = $cust_main->getfield('last');
713 $payname = "$payfirst $paylast";
716 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
717 if ( $conf->exists('emailinvoiceauto')
718 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
719 push @invoicing_list, $cust_main->all_emails;
721 my $email = $invoicing_list[0];
723 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
725 my $description = 'Internet Services';
726 if ( $conf->exists('business-onlinepayment-description') ) {
727 my $dtempl = $conf->config('business-onlinepayment-description');
729 my $agent_obj = $cust_main->agent
730 or die "can't retreive agent for $cust_main (agentnum ".
731 $cust_main->agentnum. ")";
732 my $agent = $agent_obj->agent;
733 my $pkgs = join(', ',
734 map { $_->cust_pkg->part_pkg->pkg }
735 grep { $_->pkgnum } $self->cust_bill_pkg
737 $description = eval qq("$dtempl");
742 if ( $method eq 'CC' ) {
744 $content{card_number} = $cust_main->payinfo;
745 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
746 $content{expiration} = "$2/$1";
748 $content{cvv2} = $cust_main->paycvv
749 if defined $cust_main->dbdef_table->column('paycvv')
750 && length($cust_main->paycvv);
752 $content{recurring_billing} = 'YES'
753 if qsearch('cust_pay', { 'custnum' => $cust_main->custnum,
755 'payinfo' => $cust_main->payinfo, } );
757 } elsif ( $method eq 'ECHECK' ) {
758 my($account_number,$routing_code) = $cust_main->payinfo;
759 ( $content{account_number}, $content{routing_code} ) =
760 split('@', $cust_main->payinfo);
761 $content{bank_name} = $cust_main->payname;
762 $content{account_type} = 'CHECKING';
763 $content{account_name} = $payname;
764 $content{customer_org} = $self->company ? 'B' : 'I';
765 $content{customer_ssn} = $self->ss;
766 } elsif ( $method eq 'LEC' ) {
767 $content{phone} = $cust_main->payinfo;
771 new Business::OnlinePayment( $processor, @$options );
772 $transaction->content(
775 'password' => $password,
776 'action' => $action1,
777 'description' => $description,
779 'invoice_number' => $self->invnum,
780 'customer_id' => $self->custnum,
781 'last_name' => $paylast,
782 'first_name' => $payfirst,
784 'address' => $address,
785 'city' => $cust_main->city,
786 'state' => $cust_main->state,
787 'zip' => $cust_main->zip,
788 'country' => $cust_main->country,
789 'referer' => 'http://cleanwhisker.420.am/',
791 'phone' => $cust_main->daytime || $cust_main->night,
794 $transaction->submit();
796 if ( $transaction->is_success() && $action2 ) {
797 my $auth = $transaction->authorization;
798 my $ordernum = $transaction->can('order_number')
799 ? $transaction->order_number
802 #warn "********* $auth ***********\n";
803 #warn "********* $ordernum ***********\n";
805 new Business::OnlinePayment( $processor, @$options );
812 password => $password,
813 order_number => $ordernum,
815 authorization => $auth,
816 description => $description,
819 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
820 transaction_sequence_num local_transaction_date
821 local_transaction_time AVS_result_code )) {
822 $capture{$field} = $transaction->$field() if $transaction->can($field);
825 $capture->content( %capture );
829 unless ( $capture->is_success ) {
830 my $e = "Authorization sucessful but capture failed, invnum #".
831 $self->invnum. ': '. $capture->result_code.
832 ": ". $capture->error_message;
839 #remove paycvv after initial transaction
840 #make this disable-able via a config option if anyone insists?
841 # (though that probably violates cardholder agreements)
842 use Business::CreditCard;
843 if ( defined $cust_main->dbdef_table->column('paycvv')
844 && length($cust_main->paycvv)
845 && ! grep { $_ eq cardtype($cust_main->payinfo) } $conf->config('cvv-save')
848 my $new = new FS::cust_main { $cust_main->hash };
850 my $error = $new->replace($cust_main);
852 warn "error removing cvv: $error\n";
857 if ( $transaction->is_success() ) {
865 my $cust_pay = new FS::cust_pay ( {
866 'invnum' => $self->invnum,
869 'payby' => $method2payby{$method},
870 'payinfo' => $cust_main->payinfo,
871 'paybatch' => "$processor:". $transaction->authorization,
873 my $error = $cust_pay->insert;
875 # gah, even with transactions.
876 my $e = 'WARNING: Card/ACH debited but database not updated - '.
877 'error applying payment, invnum #' . $self->invnum.
878 " ($processor): $error";
884 #} elsif ( $options{'report_badcard'} ) {
887 my $perror = "$processor error, invnum #". $self->invnum. ': '.
888 $transaction->result_code. ": ". $transaction->error_message;
890 if ( !$realtime_bop_decline_quiet && $conf->exists('emaildecline')
891 && grep { $_ ne 'POST' } $cust_main->invoicing_list
892 && ! grep { $_ eq $transaction->error_message }
893 $conf->config('emaildecline-exclude')
895 my @templ = $conf->config('declinetemplate');
896 my $template = new Text::Template (
898 SOURCE => [ map "$_\n", @templ ],
899 ) or return "($perror) can't create template: $Text::Template::ERROR";
901 or return "($perror) can't compile template: $Text::Template::ERROR";
903 my $templ_hash = { error => $transaction->error_message };
905 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
906 $ENV{MAILADDRESS} = $invoice_from;
907 my $header = new Mail::Header ( [
908 "From: $invoice_from",
909 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
910 "Sender: $invoice_from",
911 "Reply-To: $invoice_from",
912 "Date: ". time2str("%a, %d %b %Y %X %z", time),
913 "Subject: Your payment could not be processed",
915 my $message = new Mail::Internet (
917 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
920 $message->smtpsend( Host => $smtpmachine )
921 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
922 or return "($perror) (customer # ". $self->custnum.
923 ") can't send card decline email to ".
924 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
925 " via server $smtpmachine with SMTP: $!";
933 =item realtime_card_cybercash
935 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
939 sub realtime_card_cybercash {
941 my $cust_main = $self->cust_main;
942 my $amount = $self->owed;
944 return "CyberCash CashRegister real-time card processing not enabled!"
945 unless $cybercash eq 'cybercash3.2';
947 my $address = $cust_main->address1;
948 $address .= ", ". $cust_main->address2 if $cust_main->address2;
951 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
952 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
957 my $paybatch = $self->invnum.
958 '-' . time2str("%y%m%d%H%M%S", time);
960 my $payname = $cust_main->payname ||
961 $cust_main->getfield('first').' '.$cust_main->getfield('last');
963 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
965 my @full_xaction = ( $xaction,
966 'Order-ID' => $paybatch,
967 'Amount' => "usd $amount",
968 'Card-Number' => $cust_main->getfield('payinfo'),
969 'Card-Name' => $payname,
970 'Card-Address' => $address,
971 'Card-City' => $cust_main->getfield('city'),
972 'Card-State' => $cust_main->getfield('state'),
973 'Card-Zip' => $cust_main->getfield('zip'),
974 'Card-Country' => $country,
979 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
981 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
982 my $cust_pay = new FS::cust_pay ( {
983 'invnum' => $self->invnum,
987 'payinfo' => $cust_main->payinfo,
988 'paybatch' => "$cybercash:$paybatch",
990 my $error = $cust_pay->insert;
992 # gah, even with transactions.
993 my $e = 'WARNING: Card debited but database not updated - '.
994 'error applying payment, invnum #' . $self->invnum.
995 " (CyberCash Order-ID $paybatch): $error";
1001 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1002 # || $options{'report_badcard'}
1005 return 'Cybercash error, invnum #' .
1006 $self->invnum. ':'. $result{'MErrMsg'};
1013 Adds a payment for this invoice to the pending credit card batch (see
1014 L<FS::cust_pay_batch>).
1020 my $cust_main = $self->cust_main;
1022 my $cust_pay_batch = new FS::cust_pay_batch ( {
1023 'invnum' => $self->getfield('invnum'),
1024 'custnum' => $cust_main->getfield('custnum'),
1025 'last' => $cust_main->getfield('last'),
1026 'first' => $cust_main->getfield('first'),
1027 'address1' => $cust_main->getfield('address1'),
1028 'address2' => $cust_main->getfield('address2'),
1029 'city' => $cust_main->getfield('city'),
1030 'state' => $cust_main->getfield('state'),
1031 'zip' => $cust_main->getfield('zip'),
1032 'country' => $cust_main->getfield('country'),
1033 'cardnum' => $cust_main->getfield('payinfo'),
1034 'exp' => $cust_main->getfield('paydate'),
1035 'payname' => $cust_main->getfield('payname'),
1036 'amount' => $self->owed,
1038 my $error = $cust_pay_batch->insert;
1039 die $error if $error;
1044 =item print_text [ TIME [ , TEMPLATE ] ]
1046 Returns an text invoice, as a list of lines.
1048 TIME an optional value used to control the printing of overdue messages. The
1049 default is now. It isn't the date of the invoice; that's the `_date' field.
1050 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1051 L<Time::Local> and L<Date::Parse> for conversion functions.
1057 my( $self, $today, $template ) = @_;
1059 # my $invnum = $self->invnum;
1060 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
1061 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1062 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1064 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1065 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1066 #my $balance_due = $self->owed + $pr_total - $cr_total;
1067 my $balance_due = $self->owed + $pr_total;
1070 #my($description,$amount);
1074 foreach ( @pr_cust_bill ) {
1076 "Previous Balance, Invoice #". $_->invnum.
1077 " (". time2str("%x",$_->_date). ")",
1078 $money_char. sprintf("%10.2f",$_->owed)
1081 if (@pr_cust_bill) {
1082 push @buf,['','-----------'];
1083 push @buf,[ 'Total Previous Balance',
1084 $money_char. sprintf("%10.2f",$pr_total ) ];
1089 foreach my $cust_bill_pkg (
1090 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1091 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1094 if ( $cust_bill_pkg->pkgnum ) {
1096 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1097 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1098 my $pkg = $part_pkg->pkg;
1100 if ( $cust_bill_pkg->setup != 0 ) {
1101 my $description = $pkg;
1102 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1103 push @buf, [ $description,
1104 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1106 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1109 if ( $cust_bill_pkg->recur != 0 ) {
1111 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1112 time2str("%x", $cust_bill_pkg->edate) . ")",
1113 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1116 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1119 } else { #pkgnum tax or one-shot line item
1120 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1121 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1123 if ( $cust_bill_pkg->setup != 0 ) {
1124 push @buf, [ $itemdesc,
1125 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1127 if ( $cust_bill_pkg->recur != 0 ) {
1128 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1129 . time2str("%x", $cust_bill_pkg->edate). ")",
1130 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1136 push @buf,['','-----------'];
1137 push @buf,['Total New Charges',
1138 $money_char. sprintf("%10.2f",$self->charged) ];
1141 push @buf,['','-----------'];
1142 push @buf,['Total Charges',
1143 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1147 foreach ( $self->cust_credited ) {
1149 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1151 my $reason = substr($_->cust_credit->reason,0,32);
1152 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1153 $reason = " ($reason) " if $reason;
1155 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1157 $money_char. sprintf("%10.2f",$_->amount)
1160 #foreach ( @cr_cust_credit ) {
1162 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1163 # $money_char. sprintf("%10.2f",$_->credited)
1167 #get & print payments
1168 foreach ( $self->cust_bill_pay ) {
1170 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1173 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1174 $money_char. sprintf("%10.2f",$_->amount )
1179 my $balance_due_msg = $self->balance_due_msg;
1181 push @buf,['','-----------'];
1182 push @buf,[$balance_due_msg, $money_char.
1183 sprintf("%10.2f", $balance_due ) ];
1185 #create the template
1186 my $templatefile = 'invoice_template';
1187 $templatefile .= "_$template" if $template;
1188 my @invoice_template = $conf->config($templatefile)
1189 or die "cannot load config file $templatefile";
1192 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1193 /invoice_lines\((\d*)\)/;
1194 $invoice_lines += $1 || scalar(@buf);
1197 die "no invoice_lines() functions in template?" unless $wasfunc;
1198 my $invoice_template = new Text::Template (
1200 SOURCE => [ map "$_\n", @invoice_template ],
1201 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1202 $invoice_template->compile()
1203 or die "can't compile template: $Text::Template::ERROR";
1205 #setup template variables
1206 package FS::cust_bill::_template; #!
1207 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1209 $invnum = $self->invnum;
1210 $date = $self->_date;
1212 $agent = $self->cust_main->agent->agent;
1214 if ( $FS::cust_bill::invoice_lines ) {
1216 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1218 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1223 #format address (variable for the template)
1225 @address = ( '', '', '', '', '', '' );
1226 package FS::cust_bill; #!
1227 $FS::cust_bill::_template::address[$l++] =
1228 $cust_main->payname.
1229 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1230 ? " (P.O. #". $cust_main->payinfo. ")"
1234 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1235 if $cust_main->company;
1236 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1237 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1238 if $cust_main->address2;
1239 $FS::cust_bill::_template::address[$l++] =
1240 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1241 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1242 unless $cust_main->country eq 'US';
1244 # #overdue? (variable for the template)
1245 # $FS::cust_bill::_template::overdue = (
1247 # && $today > $self->_date
1248 ## && $self->printed > 1
1249 # && $self->printed > 0
1252 #and subroutine for the template
1253 sub FS::cust_bill::_template::invoice_lines {
1254 my $lines = shift || scalar(@buf);
1256 scalar(@buf) ? shift @buf : [ '', '' ];
1262 $FS::cust_bill::_template::page = 1;
1266 push @collect, split("\n",
1267 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1269 $FS::cust_bill::_template::page++;
1272 map "$_\n", @collect;
1276 =item print_ps [ TIME [ , TEMPLATE ] ]
1278 Returns an postscript invoice, as a scalar.
1280 TIME an optional value used to control the printing of overdue messages. The
1281 default is now. It isn't the date of the invoice; that's the `_date' field.
1282 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1283 L<Time::Local> and L<Date::Parse> for conversion functions.
1287 #still some false laziness w/print_text
1290 my( $self, $today, $template ) = @_;
1293 # my $invnum = $self->invnum;
1294 my $cust_main = $self->cust_main;
1295 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1296 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1298 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1299 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1300 #my $balance_due = $self->owed + $pr_total - $cr_total;
1301 my $balance_due = $self->owed + $pr_total;
1304 #my($description,$amount);
1307 #create the template
1308 my $templatefile = 'invoice_latex';
1309 $templatefile .= "_$template" if $template;
1310 my @invoice_template = $conf->config($templatefile)
1311 or die "cannot load config file $templatefile";
1313 my %invoice_data = (
1314 'invnum' => $self->invnum,
1315 'date' => time2str('%b %o, %Y', $self->_date),
1316 'agent' => $cust_main->agent->agent,
1317 'payname' => $cust_main->payname,
1318 'company' => $cust_main->company,
1319 'address1' => $cust_main->address1,
1320 'address2' => $cust_main->address2,
1321 'city' => $cust_main->city,
1322 'state' => $cust_main->state,
1323 'zip' => $cust_main->zip,
1324 'country' => $cust_main->country,
1325 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1327 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1328 'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1331 $invoice_data{'footer'} =~ s/\n+$//;
1332 $invoice_data{'notes'} =~ s/\n+$//;
1334 my $countrydefault = $conf->config('countrydefault') || 'US';
1335 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1337 $invoice_data{'po_line'} =
1338 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1339 ? "Purchase Order #". $cust_main->payinfo
1343 my @total_item = ();
1345 while ( @invoice_template ) {
1346 my $line = shift @invoice_template;
1348 if ( $line =~ /^%%Detail\s*$/ ) {
1350 while ( ( my $line_item_line = shift @invoice_template )
1351 !~ /^%%EndDetail\s*$/ ) {
1352 push @line_item, $line_item_line;
1354 foreach my $line_item ( $self->_items ) {
1355 #foreach my $line_item ( $self->_items_pkg ) {
1356 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1357 $invoice_data{'description'} = $line_item->{'description'};
1358 if ( exists $line_item->{'ext_description'} ) {
1359 $invoice_data{'description'} .=
1360 "\\tabularnewline\n~~".
1361 join("\\tabularnewline\n~~", @{$line_item->{'ext_description'}} );
1363 $invoice_data{'amount'} = $line_item->{'amount'};
1364 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1366 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1369 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1371 while ( ( my $total_item_line = shift @invoice_template )
1372 !~ /^%%EndTotalDetails\s*$/ ) {
1373 push @total_item, $total_item_line;
1376 my @total_fill = ();
1379 foreach my $tax ( $self->_items_tax ) {
1380 $invoice_data{'total_item'} = $tax->{'description'};
1381 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1383 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1388 $invoice_data{'total_item'} = 'Sub-total';
1389 $invoice_data{'total_amount'} =
1390 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1391 unshift @total_fill,
1392 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1396 $invoice_data{'total_item'} = '\textbf{Total}';
1397 $invoice_data{'total_amount'} =
1398 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1400 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1403 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1406 foreach my $credit ( $self->_items_credits ) {
1407 $invoice_data{'total_item'} = $credit->{'description'};
1409 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1411 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1416 foreach my $payment ( $self->_items_payments ) {
1417 $invoice_data{'total_item'} = $payment->{'description'};
1419 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1421 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1425 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1426 $invoice_data{'total_amount'} =
1427 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1429 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1432 push @filled_in, @total_fill;
1435 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1436 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1437 push @filled_in, $line;
1448 my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
1449 my $unique = int(rand(2**31)); #UGH... use File::Temp or something
1452 my $file = $self->invnum. ".$unique";
1454 open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
1455 print TEX join("\n", @filled_in ), "\n";
1459 system('pslatex', "$file.tex");
1460 system('pslatex', "$file.tex");
1461 #system('dvips', '-t', 'letter', "$file.dvi", "$file.ps");
1462 system('dvips', '-t', 'letter', "$file.dvi" );
1464 open(POSTSCRIPT, "<$file.ps") or die "can't open $file.ps (probable error in LaTeX template): $!\n";
1466 #rm $file.dvi $file.log $file.aux
1467 #unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps");
1468 unlink("$file.dvi", "$file.log", "$file.aux");
1471 while (<POSTSCRIPT>) {
1481 #utility methods for print_*
1483 sub balance_due_msg {
1485 my $msg = 'Balance Due';
1486 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1487 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1488 } elsif ( $conf->config('invoice_default_terms') ) {
1489 $msg .= ' - '. $conf->config('invoice_default_terms');
1496 my @display = scalar(@_)
1498 : qw( _items_previous _items_pkg );
1499 #: qw( _items_pkg );
1500 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1502 foreach my $display ( @display ) {
1503 push @b, $self->$display(@_);
1508 sub _items_previous {
1510 my $cust_main = $self->cust_main;
1511 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1513 foreach ( @pr_cust_bill ) {
1515 'description' => 'Previous Balance, Invoice \#'. $_->invnum.
1516 ' ('. time2str('%x',$_->_date). ')',
1517 #'pkgpart' => 'N/A',
1519 'amount' => sprintf("%10.2f", $_->owed),
1525 # 'description' => 'Previous Balance',
1526 # #'pkgpart' => 'N/A',
1527 # 'pkgnum' => 'N/A',
1528 # 'amount' => sprintf("%10.2f", $pr_total ),
1529 # 'ext_description' => [ map {
1530 # "Invoice ". $_->invnum.
1531 # " (". time2str("%x",$_->_date). ") ".
1532 # sprintf("%10.2f", $_->owed)
1533 # } @pr_cust_bill ],
1540 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1541 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1546 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1547 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1550 sub _items_cust_bill_pkg {
1552 my $cust_bill_pkg = shift;
1555 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1557 if ( $cust_bill_pkg->pkgnum ) {
1559 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1560 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1561 my $pkg = $part_pkg->pkg;
1563 if ( $cust_bill_pkg->setup != 0 ) {
1564 my $description = $pkg;
1565 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1567 @d = $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1569 'description' => $description,
1570 #'pkgpart' => $part_pkg->pkgpart,
1571 'pkgnum' => $cust_pkg->pkgnum,
1572 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1573 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1574 $cust_pkg->labels ),
1580 if ( $cust_bill_pkg->recur != 0 ) {
1582 'description' => "$pkg (" .
1583 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1584 time2str('%x', $cust_bill_pkg->edate). ')',
1585 #'pkgpart' => $part_pkg->pkgpart,
1586 'pkgnum' => $cust_pkg->pkgnum,
1587 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1588 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1589 $cust_pkg->labels ),
1590 $cust_bill_pkg->details,
1595 } else { #pkgnum tax or one-shot line item (??)
1597 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1598 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1600 if ( $cust_bill_pkg->setup != 0 ) {
1602 'description' => $itemdesc,
1603 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1606 if ( $cust_bill_pkg->recur != 0 ) {
1608 'description' => "$itemdesc (".
1609 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1610 time2str("%x", $cust_bill_pkg->edate). ')',
1611 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1623 sub _items_credits {
1628 foreach ( $self->cust_credited ) {
1630 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1632 my $reason = $_->cust_credit->reason;
1633 #my $reason = substr($_->cust_credit->reason,0,32);
1634 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1635 $reason = " ($reason) " if $reason;
1637 #'description' => 'Credit ref\#'. $_->crednum.
1638 # " (". time2str("%x",$_->cust_credit->_date) .")".
1640 'description' => 'Credit applied'.
1641 time2str("%x",$_->cust_credit->_date). $reason,
1642 'amount' => sprintf("%10.2f",$_->amount),
1645 #foreach ( @cr_cust_credit ) {
1647 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1648 # $money_char. sprintf("%10.2f",$_->credited)
1656 sub _items_payments {
1660 #get & print payments
1661 foreach ( $self->cust_bill_pay ) {
1663 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1666 'description' => "Payment received ".
1667 time2str("%x",$_->cust_pay->_date ),
1668 'amount' => sprintf("%10.2f", $_->amount )
1682 print_text formatting (and some logic :/) is in source, but needs to be
1683 slurped in from a file. Also number of lines ($=).
1685 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1686 or something similar so the look can be completely customized?)
1690 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1691 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base