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 ( $conf->config('invoice_latex') ) {
427 @print_text = $self->print_ps('', $template);
430 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
432 or return "Can't open pipe to $lpr: $!";
433 print LPR @print_text;
435 or return $! ? "Error closing $lpr: $!"
436 : "Exit status $? from $lpr";
443 =item send_csv OPTIONS
445 Sends invoice as a CSV data-file to a remote host with the specified protocol.
449 protocol - currently only "ftp"
455 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
456 and YYMMDDHHMMSS is a timestamp.
458 The fields of the CSV file is as follows:
460 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
464 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
466 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
467 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
468 fields are filled in.
470 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
471 first two fields (B<record_type> and B<invnum>) and the last five fields
472 (B<pkg> through B<edate>) are filled in.
474 =item invnum - invoice number
476 =item custnum - customer number
478 =item _date - invoice date
480 =item charged - total invoice amount
482 =item first - customer first name
484 =item last - customer first name
486 =item company - company name
488 =item address1 - address line 1
490 =item address2 - address line 1
500 =item pkg - line item description
502 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
504 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
506 =item sdate - start date for recurring fee
508 =item edate - end date for recurring fee
515 my($self, %opt) = @_;
517 #part one: create file
519 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
520 mkdir $spooldir, 0700 unless -d $spooldir;
522 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
524 open(CSV, ">$file") or die "can't open $file: $!";
526 eval "use Text::CSV_XS";
529 my $csv = Text::CSV_XS->new({'always_quote'=>1});
531 my $cust_main = $self->cust_main;
537 time2str("%x", $self->_date),
538 sprintf("%.2f", $self->charged),
539 ( map { $cust_main->getfield($_) }
540 qw( first last company address1 address2 city state zip country ) ),
542 ) or die "can't create csv";
543 print CSV $csv->string. "\n";
545 #new charges (false laziness w/print_text)
546 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
548 my($pkg, $setup, $recur, $sdate, $edate);
549 if ( $cust_bill_pkg->pkgnum ) {
551 ($pkg, $setup, $recur, $sdate, $edate) = (
552 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
553 ( $cust_bill_pkg->setup != 0
554 ? sprintf("%.2f", $cust_bill_pkg->setup )
556 ( $cust_bill_pkg->recur != 0
557 ? sprintf("%.2f", $cust_bill_pkg->recur )
559 time2str("%x", $cust_bill_pkg->sdate),
560 time2str("%x", $cust_bill_pkg->edate),
564 next unless $cust_bill_pkg->setup != 0;
565 ($pkg, $setup, $recur, $sdate, $edate) =
566 ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
572 ( map { '' } (1..11) ),
573 ($pkg, $setup, $recur, $sdate, $edate)
574 ) or die "can't create csv";
575 print CSV $csv->string. "\n";
579 close CSV or die "can't close CSV: $!";
584 if ( $opt{protocol} eq 'ftp' ) {
585 eval "use Net::FTP;";
587 $net = Net::FTP->new($opt{server}) or die @$;
589 die "unknown protocol: $opt{protocol}";
592 $net->login( $opt{username}, $opt{password} )
593 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
595 $net->binary or die "can't set binary mode";
597 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
599 $net->put($file) or die "can't put $file: $!";
609 Pays this invoice with a compliemntary payment. If there is an error,
610 returns the error, otherwise returns false.
616 my $cust_pay = new FS::cust_pay ( {
617 'invnum' => $self->invnum,
618 'paid' => $self->owed,
621 'payinfo' => $self->cust_main->payinfo,
629 Attempts to pay this invoice with a credit card payment via a
630 Business::OnlinePayment realtime gateway. See
631 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
632 for supported processors.
651 Attempts to pay this invoice with an electronic check (ACH) payment via a
652 Business::OnlinePayment realtime gateway. See
653 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
654 for supported processors.
673 Attempts to pay this invoice with phone bill (LEC) payment via a
674 Business::OnlinePayment realtime gateway. See
675 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
676 for supported processors.
694 my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
696 #trim an extraneous blank line
697 pop @$options if scalar(@$options) % 2 && $options->[-1] =~ /^\s*$/;
699 my $cust_main = $self->cust_main;
700 my $amount = $self->owed;
702 my $address = $cust_main->address1;
703 $address .= ", ". $cust_main->address2 if $cust_main->address2;
705 my($payname, $payfirst, $paylast);
706 if ( $cust_main->payname && $method ne 'ECHECK' ) {
707 $payname = $cust_main->payname;
708 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
710 #$dbh->rollback if $oldAutoCommit;
711 return "Illegal payname $payname";
713 ($payfirst, $paylast) = ($1, $2);
715 $payfirst = $cust_main->getfield('first');
716 $paylast = $cust_main->getfield('last');
717 $payname = "$payfirst $paylast";
720 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
721 if ( $conf->exists('emailinvoiceauto')
722 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
723 push @invoicing_list, $cust_main->all_emails;
725 my $email = $invoicing_list[0];
727 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
729 my $description = 'Internet Services';
730 if ( $conf->exists('business-onlinepayment-description') ) {
731 my $dtempl = $conf->config('business-onlinepayment-description');
733 my $agent_obj = $cust_main->agent
734 or die "can't retreive agent for $cust_main (agentnum ".
735 $cust_main->agentnum. ")";
736 my $agent = $agent_obj->agent;
737 my $pkgs = join(', ',
738 map { $_->cust_pkg->part_pkg->pkg }
739 grep { $_->pkgnum } $self->cust_bill_pkg
741 $description = eval qq("$dtempl");
746 if ( $method eq 'CC' ) {
748 $content{card_number} = $cust_main->payinfo;
749 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
750 $content{expiration} = "$2/$1";
752 $content{cvv2} = $cust_main->paycvv
753 if defined $cust_main->dbdef_table->column('paycvv')
754 && length($cust_main->paycvv);
756 $content{recurring_billing} = 'YES'
757 if qsearch('cust_pay', { 'custnum' => $cust_main->custnum,
759 'payinfo' => $cust_main->payinfo, } );
761 } elsif ( $method eq 'ECHECK' ) {
762 my($account_number,$routing_code) = $cust_main->payinfo;
763 ( $content{account_number}, $content{routing_code} ) =
764 split('@', $cust_main->payinfo);
765 $content{bank_name} = $cust_main->payname;
766 $content{account_type} = 'CHECKING';
767 $content{account_name} = $payname;
768 $content{customer_org} = $self->company ? 'B' : 'I';
769 $content{customer_ssn} = $self->ss;
770 } elsif ( $method eq 'LEC' ) {
771 $content{phone} = $cust_main->payinfo;
775 new Business::OnlinePayment( $processor, @$options );
776 $transaction->content(
779 'password' => $password,
780 'action' => $action1,
781 'description' => $description,
783 'invoice_number' => $self->invnum,
784 'customer_id' => $self->custnum,
785 'last_name' => $paylast,
786 'first_name' => $payfirst,
788 'address' => $address,
789 'city' => $cust_main->city,
790 'state' => $cust_main->state,
791 'zip' => $cust_main->zip,
792 'country' => $cust_main->country,
793 'referer' => 'http://cleanwhisker.420.am/',
795 'phone' => $cust_main->daytime || $cust_main->night,
798 $transaction->submit();
800 if ( $transaction->is_success() && $action2 ) {
801 my $auth = $transaction->authorization;
802 my $ordernum = $transaction->can('order_number')
803 ? $transaction->order_number
806 #warn "********* $auth ***********\n";
807 #warn "********* $ordernum ***********\n";
809 new Business::OnlinePayment( $processor, @$options );
816 password => $password,
817 order_number => $ordernum,
819 authorization => $auth,
820 description => $description,
823 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
824 transaction_sequence_num local_transaction_date
825 local_transaction_time AVS_result_code )) {
826 $capture{$field} = $transaction->$field() if $transaction->can($field);
829 $capture->content( %capture );
833 unless ( $capture->is_success ) {
834 my $e = "Authorization sucessful but capture failed, invnum #".
835 $self->invnum. ': '. $capture->result_code.
836 ": ". $capture->error_message;
843 #remove paycvv after initial transaction
844 #make this disable-able via a config option if anyone insists?
845 # (though that probably violates cardholder agreements)
846 use Business::CreditCard;
847 if ( defined $cust_main->dbdef_table->column('paycvv')
848 && length($cust_main->paycvv)
849 && ! grep { $_ eq cardtype($cust_main->payinfo) } $conf->config('cvv-save')
852 my $new = new FS::cust_main { $cust_main->hash };
854 my $error = $new->replace($cust_main);
856 warn "error removing cvv: $error\n";
861 if ( $transaction->is_success() ) {
869 my $cust_pay = new FS::cust_pay ( {
870 'invnum' => $self->invnum,
873 'payby' => $method2payby{$method},
874 'payinfo' => $cust_main->payinfo,
875 'paybatch' => "$processor:". $transaction->authorization,
877 my $error = $cust_pay->insert;
879 # gah, even with transactions.
880 my $e = 'WARNING: Card/ACH debited but database not updated - '.
881 'error applying payment, invnum #' . $self->invnum.
882 " ($processor): $error";
888 #} elsif ( $options{'report_badcard'} ) {
891 my $perror = "$processor error, invnum #". $self->invnum. ': '.
892 $transaction->result_code. ": ". $transaction->error_message;
894 if ( !$realtime_bop_decline_quiet && $conf->exists('emaildecline')
895 && grep { $_ ne 'POST' } $cust_main->invoicing_list
896 && ! grep { $_ eq $transaction->error_message }
897 $conf->config('emaildecline-exclude')
899 my @templ = $conf->config('declinetemplate');
900 my $template = new Text::Template (
902 SOURCE => [ map "$_\n", @templ ],
903 ) or return "($perror) can't create template: $Text::Template::ERROR";
905 or return "($perror) can't compile template: $Text::Template::ERROR";
907 my $templ_hash = { error => $transaction->error_message };
909 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
910 $ENV{MAILADDRESS} = $invoice_from;
911 my $header = new Mail::Header ( [
912 "From: $invoice_from",
913 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
914 "Sender: $invoice_from",
915 "Reply-To: $invoice_from",
916 "Date: ". time2str("%a, %d %b %Y %X %z", time),
917 "Subject: Your payment could not be processed",
919 my $message = new Mail::Internet (
921 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
924 $message->smtpsend( Host => $smtpmachine )
925 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
926 or return "($perror) (customer # ". $self->custnum.
927 ") can't send card decline email to ".
928 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
929 " via server $smtpmachine with SMTP: $!";
937 =item realtime_card_cybercash
939 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
943 sub realtime_card_cybercash {
945 my $cust_main = $self->cust_main;
946 my $amount = $self->owed;
948 return "CyberCash CashRegister real-time card processing not enabled!"
949 unless $cybercash eq 'cybercash3.2';
951 my $address = $cust_main->address1;
952 $address .= ", ". $cust_main->address2 if $cust_main->address2;
955 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
956 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
961 my $paybatch = $self->invnum.
962 '-' . time2str("%y%m%d%H%M%S", time);
964 my $payname = $cust_main->payname ||
965 $cust_main->getfield('first').' '.$cust_main->getfield('last');
967 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
969 my @full_xaction = ( $xaction,
970 'Order-ID' => $paybatch,
971 'Amount' => "usd $amount",
972 'Card-Number' => $cust_main->getfield('payinfo'),
973 'Card-Name' => $payname,
974 'Card-Address' => $address,
975 'Card-City' => $cust_main->getfield('city'),
976 'Card-State' => $cust_main->getfield('state'),
977 'Card-Zip' => $cust_main->getfield('zip'),
978 'Card-Country' => $country,
983 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
985 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
986 my $cust_pay = new FS::cust_pay ( {
987 'invnum' => $self->invnum,
991 'payinfo' => $cust_main->payinfo,
992 'paybatch' => "$cybercash:$paybatch",
994 my $error = $cust_pay->insert;
996 # gah, even with transactions.
997 my $e = 'WARNING: Card debited but database not updated - '.
998 'error applying payment, invnum #' . $self->invnum.
999 " (CyberCash Order-ID $paybatch): $error";
1005 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1006 # || $options{'report_badcard'}
1009 return 'Cybercash error, invnum #' .
1010 $self->invnum. ':'. $result{'MErrMsg'};
1017 Adds a payment for this invoice to the pending credit card batch (see
1018 L<FS::cust_pay_batch>).
1024 my $cust_main = $self->cust_main;
1026 my $cust_pay_batch = new FS::cust_pay_batch ( {
1027 'invnum' => $self->getfield('invnum'),
1028 'custnum' => $cust_main->getfield('custnum'),
1029 'last' => $cust_main->getfield('last'),
1030 'first' => $cust_main->getfield('first'),
1031 'address1' => $cust_main->getfield('address1'),
1032 'address2' => $cust_main->getfield('address2'),
1033 'city' => $cust_main->getfield('city'),
1034 'state' => $cust_main->getfield('state'),
1035 'zip' => $cust_main->getfield('zip'),
1036 'country' => $cust_main->getfield('country'),
1037 'cardnum' => $cust_main->getfield('payinfo'),
1038 'exp' => $cust_main->getfield('paydate'),
1039 'payname' => $cust_main->getfield('payname'),
1040 'amount' => $self->owed,
1042 my $error = $cust_pay_batch->insert;
1043 die $error if $error;
1048 =item print_text [ TIME [ , TEMPLATE ] ]
1050 Returns an text invoice, as a list of lines.
1052 TIME an optional value used to control the printing of overdue messages. The
1053 default is now. It isn't the date of the invoice; that's the `_date' field.
1054 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1055 L<Time::Local> and L<Date::Parse> for conversion functions.
1061 my( $self, $today, $template ) = @_;
1063 # my $invnum = $self->invnum;
1064 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
1065 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1066 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1068 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1069 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1070 #my $balance_due = $self->owed + $pr_total - $cr_total;
1071 my $balance_due = $self->owed + $pr_total;
1074 #my($description,$amount);
1078 foreach ( @pr_cust_bill ) {
1080 "Previous Balance, Invoice #". $_->invnum.
1081 " (". time2str("%x",$_->_date). ")",
1082 $money_char. sprintf("%10.2f",$_->owed)
1085 if (@pr_cust_bill) {
1086 push @buf,['','-----------'];
1087 push @buf,[ 'Total Previous Balance',
1088 $money_char. sprintf("%10.2f",$pr_total ) ];
1093 foreach my $cust_bill_pkg (
1094 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
1095 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
1098 if ( $cust_bill_pkg->pkgnum ) {
1100 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1101 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1102 my $pkg = $part_pkg->pkg;
1104 if ( $cust_bill_pkg->setup != 0 ) {
1105 my $description = $pkg;
1106 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1107 push @buf, [ $description,
1108 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1110 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1113 if ( $cust_bill_pkg->recur != 0 ) {
1115 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1116 time2str("%x", $cust_bill_pkg->edate) . ")",
1117 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1120 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1123 } else { #pkgnum tax or one-shot line item
1124 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1125 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1127 if ( $cust_bill_pkg->setup != 0 ) {
1128 push @buf, [ $itemdesc,
1129 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1131 if ( $cust_bill_pkg->recur != 0 ) {
1132 push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1133 . time2str("%x", $cust_bill_pkg->edate). ")",
1134 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1140 push @buf,['','-----------'];
1141 push @buf,['Total New Charges',
1142 $money_char. sprintf("%10.2f",$self->charged) ];
1145 push @buf,['','-----------'];
1146 push @buf,['Total Charges',
1147 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1151 foreach ( $self->cust_credited ) {
1153 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1155 my $reason = substr($_->cust_credit->reason,0,32);
1156 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1157 $reason = " ($reason) " if $reason;
1159 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1161 $money_char. sprintf("%10.2f",$_->amount)
1164 #foreach ( @cr_cust_credit ) {
1166 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1167 # $money_char. sprintf("%10.2f",$_->credited)
1171 #get & print payments
1172 foreach ( $self->cust_bill_pay ) {
1174 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1177 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1178 $money_char. sprintf("%10.2f",$_->amount )
1183 my $balance_due_msg = $self->balance_due_msg;
1185 push @buf,['','-----------'];
1186 push @buf,[$balance_due_msg, $money_char.
1187 sprintf("%10.2f", $balance_due ) ];
1189 #create the template
1190 my $templatefile = 'invoice_template';
1191 $templatefile .= "_$template" if $template;
1192 my @invoice_template = $conf->config($templatefile)
1193 or die "cannot load config file $templatefile";
1196 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1197 /invoice_lines\((\d*)\)/;
1198 $invoice_lines += $1 || scalar(@buf);
1201 die "no invoice_lines() functions in template?" unless $wasfunc;
1202 my $invoice_template = new Text::Template (
1204 SOURCE => [ map "$_\n", @invoice_template ],
1205 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1206 $invoice_template->compile()
1207 or die "can't compile template: $Text::Template::ERROR";
1209 #setup template variables
1210 package FS::cust_bill::_template; #!
1211 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1213 $invnum = $self->invnum;
1214 $date = $self->_date;
1216 $agent = $self->cust_main->agent->agent;
1218 if ( $FS::cust_bill::invoice_lines ) {
1220 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1222 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1227 #format address (variable for the template)
1229 @address = ( '', '', '', '', '', '' );
1230 package FS::cust_bill; #!
1231 $FS::cust_bill::_template::address[$l++] =
1232 $cust_main->payname.
1233 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1234 ? " (P.O. #". $cust_main->payinfo. ")"
1238 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1239 if $cust_main->company;
1240 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1241 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1242 if $cust_main->address2;
1243 $FS::cust_bill::_template::address[$l++] =
1244 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1245 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1246 unless $cust_main->country eq 'US';
1248 # #overdue? (variable for the template)
1249 # $FS::cust_bill::_template::overdue = (
1251 # && $today > $self->_date
1252 ## && $self->printed > 1
1253 # && $self->printed > 0
1256 #and subroutine for the template
1257 sub FS::cust_bill::_template::invoice_lines {
1258 my $lines = shift || scalar(@buf);
1260 scalar(@buf) ? shift @buf : [ '', '' ];
1266 $FS::cust_bill::_template::page = 1;
1270 push @collect, split("\n",
1271 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1273 $FS::cust_bill::_template::page++;
1276 map "$_\n", @collect;
1280 =item print_ps [ TIME [ , TEMPLATE ] ]
1282 Returns an postscript invoice, as a scalar.
1284 TIME an optional value used to control the printing of overdue messages. The
1285 default is now. It isn't the date of the invoice; that's the `_date' field.
1286 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1287 L<Time::Local> and L<Date::Parse> for conversion functions.
1291 #still some false laziness w/print_text
1294 my( $self, $today, $template ) = @_;
1297 # my $invnum = $self->invnum;
1298 my $cust_main = $self->cust_main;
1299 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1300 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1302 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1303 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1304 #my $balance_due = $self->owed + $pr_total - $cr_total;
1305 my $balance_due = $self->owed + $pr_total;
1308 #my($description,$amount);
1311 #create the template
1312 my $templatefile = 'invoice_latex';
1313 $templatefile .= "_$template" if $template;
1314 my @invoice_template = $conf->config($templatefile)
1315 or die "cannot load config file $templatefile";
1317 my %invoice_data = (
1318 'invnum' => $self->invnum,
1319 'date' => time2str('%b %o, %Y', $self->_date),
1320 'agent' => $cust_main->agent->agent,
1321 'payname' => $cust_main->payname,
1322 'company' => $cust_main->company,
1323 'address1' => $cust_main->address1,
1324 'address2' => $cust_main->address2,
1325 'city' => $cust_main->city,
1326 'state' => $cust_main->state,
1327 'zip' => $cust_main->zip,
1328 'country' => $cust_main->country,
1329 'footer' => join("\n", $conf->config('invoice_latexfooter') ),
1331 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1332 'notes' => join("\n", $conf->config('invoice_latexnotes') ),
1335 $invoice_data{'footer'} =~ s/\n+$//;
1336 $invoice_data{'notes'} =~ s/\n+$//;
1338 my $countrydefault = $conf->config('countrydefault') || 'US';
1339 $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1341 $invoice_data{'po_line'} =
1342 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1343 ? "Purchase Order #". $cust_main->payinfo
1347 my @total_item = ();
1349 while ( @invoice_template ) {
1350 my $line = shift @invoice_template;
1352 if ( $line =~ /^%%Detail\s*$/ ) {
1354 while ( ( my $line_item_line = shift @invoice_template )
1355 !~ /^%%EndDetail\s*$/ ) {
1356 push @line_item, $line_item_line;
1358 foreach my $line_item ( $self->_items ) {
1359 #foreach my $line_item ( $self->_items_pkg ) {
1360 $invoice_data{'ref'} = $line_item->{'pkgnum'};
1361 $invoice_data{'description'} = $line_item->{'description'};
1362 if ( exists $line_item->{'ext_description'} ) {
1363 $invoice_data{'description'} .=
1364 "\\tabularnewline\n~~".
1365 join("\\tabularnewline\n~~", @{$line_item->{'ext_description'}} );
1367 $invoice_data{'amount'} = $line_item->{'amount'};
1368 $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1370 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1373 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1375 while ( ( my $total_item_line = shift @invoice_template )
1376 !~ /^%%EndTotalDetails\s*$/ ) {
1377 push @total_item, $total_item_line;
1380 my @total_fill = ();
1383 foreach my $tax ( $self->_items_tax ) {
1384 $invoice_data{'total_item'} = $tax->{'description'};
1385 $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1387 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1392 $invoice_data{'total_item'} = 'Sub-total';
1393 $invoice_data{'total_amount'} =
1394 '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1395 unshift @total_fill,
1396 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1400 $invoice_data{'total_item'} = '\textbf{Total}';
1401 $invoice_data{'total_amount'} =
1402 '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1404 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1407 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1410 foreach my $credit ( $self->_items_credits ) {
1411 $invoice_data{'total_item'} = $credit->{'description'};
1413 $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1415 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1420 foreach my $payment ( $self->_items_payments ) {
1421 $invoice_data{'total_item'} = $payment->{'description'};
1423 $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1425 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1429 $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1430 $invoice_data{'total_amount'} =
1431 '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1433 map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1436 push @filled_in, @total_fill;
1439 #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1440 $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1441 push @filled_in, $line;
1452 my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
1453 my $unique = int(rand(2**31)); #UGH... use File::Temp or something
1456 my $file = $self->invnum. ".$unique";
1458 open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
1459 print TEX join("\n", @filled_in ), "\n";
1463 system('pslatex', "$file.tex");
1464 system('pslatex', "$file.tex");
1465 #system('dvips', '-t', 'letter', "$file.dvi", "$file.ps");
1466 system('dvips', '-t', 'letter', "$file.dvi", '-o', "$file.ps" );
1468 open(POSTSCRIPT, "<$file.ps") or die "can't open $file.ps (probable error in LaTeX template): $!\n";
1470 #rm $file.dvi $file.log $file.aux
1471 #unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps");
1472 unlink("$file.dvi", "$file.log", "$file.aux");
1475 while (<POSTSCRIPT>) {
1485 # quick subroutine for print_ps
1487 # There are ten characters that LaTeX treats as special characters, which
1488 # means that they do not simply typeset themselves:
1489 # # $ % & ~ _ ^ \ { }
1491 # TeX ignores blanks following an escaped character; if you want a blank (as
1492 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
1496 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1500 #utility methods for print_*
1502 sub balance_due_msg {
1504 my $msg = 'Balance Due';
1505 if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1506 $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1507 } elsif ( $conf->config('invoice_default_terms') ) {
1508 $msg .= ' - '. $conf->config('invoice_default_terms');
1515 my @display = scalar(@_)
1517 : qw( _items_previous _items_pkg );
1518 #: qw( _items_pkg );
1519 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1521 foreach my $display ( @display ) {
1522 push @b, $self->$display(@_);
1527 sub _items_previous {
1529 my $cust_main = $self->cust_main;
1530 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1532 foreach ( @pr_cust_bill ) {
1534 'description' => 'Previous Balance, Invoice \#'. $_->invnum.
1535 ' ('. time2str('%x',$_->_date). ')',
1536 #'pkgpart' => 'N/A',
1538 'amount' => sprintf("%10.2f", $_->owed),
1544 # 'description' => 'Previous Balance',
1545 # #'pkgpart' => 'N/A',
1546 # 'pkgnum' => 'N/A',
1547 # 'amount' => sprintf("%10.2f", $pr_total ),
1548 # 'ext_description' => [ map {
1549 # "Invoice ". $_->invnum.
1550 # " (". time2str("%x",$_->_date). ") ".
1551 # sprintf("%10.2f", $_->owed)
1552 # } @pr_cust_bill ],
1559 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1560 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1565 my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1566 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1569 sub _items_cust_bill_pkg {
1571 my $cust_bill_pkg = shift;
1574 foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1576 if ( $cust_bill_pkg->pkgnum ) {
1578 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1579 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1580 my $pkg = $part_pkg->pkg;
1582 if ( $cust_bill_pkg->setup != 0 ) {
1583 my $description = $pkg;
1584 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1586 @d = $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1588 'description' => $description,
1589 #'pkgpart' => $part_pkg->pkgpart,
1590 'pkgnum' => $cust_pkg->pkgnum,
1591 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1592 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1593 $cust_pkg->labels ),
1599 if ( $cust_bill_pkg->recur != 0 ) {
1601 'description' => "$pkg (" .
1602 time2str('%x', $cust_bill_pkg->sdate). ' - '.
1603 time2str('%x', $cust_bill_pkg->edate). ')',
1604 #'pkgpart' => $part_pkg->pkgpart,
1605 'pkgnum' => $cust_pkg->pkgnum,
1606 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1607 'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1608 $cust_pkg->labels ),
1609 $cust_bill_pkg->details,
1614 } else { #pkgnum tax or one-shot line item (??)
1616 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1617 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1619 if ( $cust_bill_pkg->setup != 0 ) {
1621 'description' => $itemdesc,
1622 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup),
1625 if ( $cust_bill_pkg->recur != 0 ) {
1627 'description' => "$itemdesc (".
1628 time2str("%x", $cust_bill_pkg->sdate). ' - '.
1629 time2str("%x", $cust_bill_pkg->edate). ')',
1630 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur),
1642 sub _items_credits {
1647 foreach ( $self->cust_credited ) {
1649 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1651 my $reason = $_->cust_credit->reason;
1652 #my $reason = substr($_->cust_credit->reason,0,32);
1653 #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1654 $reason = " ($reason) " if $reason;
1656 #'description' => 'Credit ref\#'. $_->crednum.
1657 # " (". time2str("%x",$_->cust_credit->_date) .")".
1659 'description' => 'Credit applied'.
1660 time2str("%x",$_->cust_credit->_date). $reason,
1661 'amount' => sprintf("%10.2f",$_->amount),
1664 #foreach ( @cr_cust_credit ) {
1666 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1667 # $money_char. sprintf("%10.2f",$_->credited)
1675 sub _items_payments {
1679 #get & print payments
1680 foreach ( $self->cust_bill_pay ) {
1682 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1685 'description' => "Payment received ".
1686 time2str("%x",$_->cust_pay->_date ),
1687 'amount' => sprintf("%10.2f", $_->amount )
1701 print_text formatting (and some logic :/) is in source, but needs to be
1702 slurped in from a file. Also number of lines ($=).
1704 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1705 or something similar so the look can be completely customized?)
1709 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1710 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base