4 use vars qw( @ISA $conf $invoice_template $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
14 use FS::Record qw( qsearch qsearchs );
16 use FS::cust_bill_pkg;
20 use FS::cust_credit_bill;
21 use FS::cust_pay_batch;
22 use FS::cust_bill_event;
24 @ISA = qw( FS::Record );
26 #ask FS::UID to run this stuff for us later
27 $FS::UID::callback{'FS::cust_bill'} = sub {
31 $money_char = $conf->config('money_char') || '$';
33 my @invoice_template = $conf->config('invoice_template')
34 or die "cannot load config file invoice_template";
36 foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
37 /invoice_lines\((\d+)\)/;
40 die "no invoice_lines() functions in template?" unless $invoice_lines;
41 $invoice_template = new Text::Template (
43 SOURCE => [ map "$_\n", @invoice_template ],
44 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
45 $invoice_template->compile()
46 or die "can't compile template: $Text::Template::ERROR";
48 $lpr = $conf->config('lpr');
49 $invoice_from = $conf->config('invoice_from');
50 $smtpmachine = $conf->config('smtpmachine');
52 if ( $conf->exists('cybercash3.2') ) {
54 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
55 require CCMckDirectLib3_2;
57 require CCMckErrno3_2;
58 #qw(MCKGetErrorMessage $E_NoErr);
59 import CCMckErrno3_2 qw($E_NoErr);
62 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
63 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
64 if ( $status != $E_NoErr ) {
65 warn "CCMckLib3_2::InitConfig error:\n";
66 foreach my $key (keys %CCMckLib3_2::Config) {
67 warn " $key => $CCMckLib3_2::Config{$key}\n"
69 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
70 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
72 $processor='cybercash3.2';
73 } elsif ( $conf->exists('business-onlinepayment') ) {
79 ) = $conf->config('business-onlinepayment');
80 $bop_action ||= 'normal authorization';
81 eval "use Business::OnlinePayment";
82 $processor="Business::OnlinePayment::$bop_processor";
89 FS::cust_bill - Object methods for cust_bill records
95 $record = new FS::cust_bill \%hash;
96 $record = new FS::cust_bill { 'column' => 'value' };
98 $error = $record->insert;
100 $error = $new_record->replace($old_record);
102 $error = $record->delete;
104 $error = $record->check;
106 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
108 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
110 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
112 @cust_pay_objects = $cust_bill->cust_pay;
114 $tax_amount = $record->tax;
116 @lines = $cust_bill->print_text;
117 @lines = $cust_bill->print_text $time;
121 An FS::cust_bill object represents an invoice; a declaration that a customer
122 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
123 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
124 following fields are currently supported:
128 =item invnum - primary key (assigned automatically for new invoices)
130 =item custnum - customer (see L<FS::cust_main>)
132 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
133 L<Time::Local> and L<Date::Parse> for conversion functions.
135 =item charged - amount of this invoice
137 =item printed - deprecated
139 =item closed - books closed flag, empty or `Y'
149 Creates a new invoice. To add the invoice to the database, see L<"insert">.
150 Invoices are normally created by calling the bill method of a customer object
151 (see L<FS::cust_main>).
155 sub table { 'cust_bill'; }
159 Adds this invoice to the database ("Posts" the invoice). If there is an error,
160 returns the error, otherwise returns false.
164 Currently unimplemented. I don't remove invoices because there would then be
165 no record you ever posted this invoice (which is bad, no?)
171 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
172 $self->SUPER::delete(@_);
175 =item replace OLD_RECORD
177 Replaces the OLD_RECORD with this one in the database. If there is an error,
178 returns the error, otherwise returns false.
180 Only printed may be changed. printed is normally updated by calling the
181 collect method of a customer object (see L<FS::cust_main>).
186 my( $new, $old ) = ( shift, shift );
187 return "Can't change custnum!" unless $old->custnum == $new->custnum;
188 #return "Can't change _date!" unless $old->_date eq $new->_date;
189 return "Can't change _date!" unless $old->_date == $new->_date;
190 return "Can't change charged!" unless $old->charged == $new->charged;
192 $new->SUPER::replace($old);
197 Checks all fields to make sure this is a valid invoice. If there is an error,
198 returns the error, otherwise returns false. Called by the insert and replace
207 $self->ut_numbern('invnum')
208 || $self->ut_number('custnum')
209 || $self->ut_numbern('_date')
210 || $self->ut_money('charged')
211 || $self->ut_numbern('printed')
212 || $self->ut_enum('closed', [ '', 'Y' ])
214 return $error if $error;
216 return "Unknown customer"
217 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
219 $self->_date(time) unless $self->_date;
221 $self->printed(0) if $self->printed eq '';
228 Returns a list consisting of the total previous balance for this customer,
229 followed by the previous outstanding invoices (as FS::cust_bill objects also).
236 my @cust_bill = sort { $a->_date <=> $b->_date }
237 grep { $_->owed != 0 && $_->_date < $self->_date }
238 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
240 foreach ( @cust_bill ) { $total += $_->owed; }
246 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
252 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
255 =item cust_bill_event
257 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
262 sub cust_bill_event {
264 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
270 Returns the customer (see L<FS::cust_main>) for this invoice.
276 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
281 Depreciated. See the cust_credited method.
283 #Returns a list consisting of the total previous credited (see
284 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
285 #outstanding credits (FS::cust_credit objects).
291 croak "FS::cust_bill->cust_credit depreciated; see ".
292 "FS::cust_bill->cust_credit_bill";
295 #my @cust_credit = sort { $a->_date <=> $b->_date }
296 # grep { $_->credited != 0 && $_->_date < $self->_date }
297 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
299 #foreach (@cust_credit) { $total += $_->credited; }
300 #$total, @cust_credit;
305 Depreciated. See the cust_bill_pay method.
307 #Returns all payments (see L<FS::cust_pay>) for this invoice.
313 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
315 #sort { $a->_date <=> $b->_date }
316 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
322 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
328 sort { $a->_date <=> $b->_date }
329 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
334 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
340 sort { $a->_date <=> $b->_date }
341 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
347 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
354 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
356 foreach (@taxlines) { $total += $_->setup; }
362 Returns the amount owed (still outstanding) on this invoice, which is charged
363 minus all payment applications (see L<FS::cust_bill_pay>) and credit
364 applications (see L<FS::cust_credit_bill>).
370 my $balance = $self->charged;
371 $balance -= $_->amount foreach ( $self->cust_bill_pay );
372 $balance -= $_->amount foreach ( $self->cust_credited );
373 $balance = sprintf( "%.2f", $balance);
374 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
380 Sends this invoice to the destinations configured for this customer: send
381 emails or print. See L<FS::cust_main_invoice>.
388 #my @print_text = $cust_bill->print_text; #( date )
389 my @invoicing_list = $self->cust_main->invoicing_list;
390 if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
391 #$ENV{SMTPHOSTS} = $smtpmachine;
392 $ENV{MAILADDRESS} = $invoice_from;
393 my $header = new Mail::Header ( [
394 "From: $invoice_from",
395 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
396 "Sender: $invoice_from",
397 "Reply-To: $invoice_from",
398 "Date: ". time2str("%a, %d %b %Y %X %z", time),
401 my $message = new Mail::Internet (
403 'Body' => [ $self->print_text ], #( date)
407 $message->smtpsend( Host => $smtpmachine )
408 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
409 or return "(customer # ". $self->custnum. ") can't send invoice email".
410 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
411 " via server $smtpmachine with SMTP: $!";
413 #} elsif ( grep { $_ eq 'POST' } @invoicing_list ) {
414 } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
416 or return "Can't open pipe to $lpr: $!";
417 print LPR $self->print_text; #( date )
419 or return $! ? "Error closing $lpr: $!"
420 : "Exit status $? from $lpr";
429 Pays this invoice with a compliemntary payment. If there is an error,
430 returns the error, otherwise returns false.
436 my $cust_pay = new FS::cust_pay ( {
437 'invnum' => $self->invnum,
438 'paid' => $self->owed,
441 'payinfo' => $self->cust_main->payinfo,
449 Attempts to pay this invoice with a Business::OnlinePayment realtime gateway.
450 See http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
451 for supproted processors.
457 my $cust_main = $self->cust_main;
458 my $amount = $self->owed;
460 unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
461 return "Real-time card processing not enabled (processor $processor)";
463 my $bop_processor = $1; #hmm?
465 my $address = $cust_main->address1;
466 $address .= ", ". $cust_main->address2 if $cust_main->address2;
469 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
470 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
473 my($payname, $payfirst, $paylast);
474 if ( $cust_main->payname ) {
475 $payname = $cust_main->payname;
476 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)$/
478 #$dbh->rollback if $oldAutoCommit;
479 return "Illegal payname $payname";
481 ($payfirst, $paylast) = ($1, $2);
483 $payfirst = $cust_main->getfield('first');
484 $paylast = $cust_main->getfield('first');
485 $payname = "$payfirst $paylast";
488 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
489 if ( $conf->exists('emailinvoiceauto')
490 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
491 push @invoicing_list, $cust_main->default_invoicing_list;
493 my $email = $invoicing_list[0];
495 my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
498 new Business::OnlinePayment( $bop_processor, @bop_options );
499 $transaction->content(
501 'login' => $bop_login,
502 'password' => $bop_password,
503 'action' => $action1,
504 'description' => 'Internet Services',
506 'invoice_number' => $self->invnum,
507 'customer_id' => $self->custnum,
508 'last_name' => $paylast,
509 'first_name' => $payfirst,
511 'address' => $address,
512 'city' => $cust_main->city,
513 'state' => $cust_main->state,
514 'zip' => $cust_main->zip,
515 'country' => $cust_main->country,
516 'card_number' => $cust_main->payinfo,
517 'expiration' => $exp,
518 'referer' => 'http://cleanwhisker.420.am/',
521 $transaction->submit();
523 if ( $transaction->is_success() && $action2 ) {
524 my $auth = $transaction->authorization;
525 my $ordernum = $transaction->order_number;
526 #warn "********* $auth ***********\n";
527 #warn "********* $ordernum ***********\n";
529 new Business::OnlinePayment( $bop_processor, @bop_options );
534 password => $bop_password,
535 order_number => $ordernum,
537 authorization => $auth,
538 description => 'Internet Services',
543 unless ( $capture->is_success ) {
544 my $e = "Authorization sucessful but capture failed, invnum #".
545 $self->invnum. ': '. $capture->result_code.
546 ": ". $capture->error_message;
553 if ( $transaction->is_success() ) {
555 my $cust_pay = new FS::cust_pay ( {
556 'invnum' => $self->invnum,
560 'payinfo' => $cust_main->payinfo,
561 'paybatch' => "$processor:". $transaction->authorization,
563 my $error = $cust_pay->insert;
565 # gah, even with transactions.
566 my $e = 'WARNING: Card debited but database not updated - '.
567 'error applying payment, invnum #' . $self->invnum.
568 " ($processor): $error";
574 #} elsif ( $options{'report_badcard'} ) {
576 return "$processor error, invnum #". $self->invnum. ': '.
577 $transaction->result_code. ": ". $transaction->error_message;
582 =item realtime_card_cybercash
584 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
588 sub realtime_card_cybercash {
590 my $cust_main = $self->cust_main;
591 my $amount = $self->owed;
593 return "CyberCash CashRegister real-time card processing not enabled!"
594 unless $processor eq 'cybercash3.2';
596 my $address = $cust_main->address1;
597 $address .= ", ". $cust_main->address2 if $cust_main->address2;
600 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
601 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
606 my $paybatch = $self->invnum.
607 '-' . time2str("%y%m%d%H%M%S", time);
609 my $payname = $cust_main->payname ||
610 $cust_main->getfield('first').' '.$cust_main->getfield('last');
612 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
614 my @full_xaction = ( $xaction,
615 'Order-ID' => $paybatch,
616 'Amount' => "usd $amount",
617 'Card-Number' => $cust_main->getfield('payinfo'),
618 'Card-Name' => $payname,
619 'Card-Address' => $address,
620 'Card-City' => $cust_main->getfield('city'),
621 'Card-State' => $cust_main->getfield('state'),
622 'Card-Zip' => $cust_main->getfield('zip'),
623 'Card-Country' => $country,
628 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
630 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
631 my $cust_pay = new FS::cust_pay ( {
632 'invnum' => $self->invnum,
636 'payinfo' => $cust_main->payinfo,
637 'paybatch' => "$processor:$paybatch",
639 my $error = $cust_pay->insert;
641 # gah, even with transactions.
642 my $e = 'WARNING: Card debited but database not updated - '.
643 'error applying payment, invnum #' . $self->invnum.
644 " (CyberCash Order-ID $paybatch): $error";
650 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
651 # || $options{'report_badcard'}
654 return 'Cybercash error, invnum #' .
655 $self->invnum. ':'. $result{'MErrMsg'};
662 Adds a payment for this invoice to the pending credit card batch (see
663 L<FS::cust_pay_batch>).
669 my $cust_main = $self->cust_main;
671 my $cust_pay_batch = new FS::cust_pay_batch ( {
672 'invnum' => $self->getfield('invnum'),
673 'custnum' => $cust_main->getfield('custnum'),
674 'last' => $cust_main->getfield('last'),
675 'first' => $cust_main->getfield('first'),
676 'address1' => $cust_main->getfield('address1'),
677 'address2' => $cust_main->getfield('address2'),
678 'city' => $cust_main->getfield('city'),
679 'state' => $cust_main->getfield('state'),
680 'zip' => $cust_main->getfield('zip'),
681 'country' => $cust_main->getfield('country'),
683 'cardnum' => $cust_main->getfield('payinfo'),
684 'exp' => $cust_main->getfield('paydate'),
685 'payname' => $cust_main->getfield('payname'),
686 'amount' => $self->owed,
688 $cust_pay_batch->insert;
692 =item print_text [TIME];
694 Returns an text invoice, as a list of lines.
696 TIME an optional value used to control the printing of overdue messages. The
697 default is now. It isn't the date of the invoice; that's the `_date' field.
698 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
699 L<Time::Local> and L<Date::Parse> for conversion functions.
705 my( $self, $today ) = ( shift, shift );
707 # my $invnum = $self->invnum;
708 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
709 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
710 unless $cust_main->payname;
712 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
713 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
714 #my $balance_due = $self->owed + $pr_total - $cr_total;
715 my $balance_due = $self->owed + $pr_total;
718 #my($description,$amount);
722 foreach ( @pr_cust_bill ) {
724 "Previous Balance, Invoice #". $_->invnum.
725 " (". time2str("%x",$_->_date). ")",
726 $money_char. sprintf("%10.2f",$_->owed)
730 push @buf,['','-----------'];
731 push @buf,[ 'Total Previous Balance',
732 $money_char. sprintf("%10.2f",$pr_total ) ];
737 foreach ( $self->cust_bill_pkg ) {
741 my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
742 my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
743 my($pkg)=$part_pkg->pkg;
745 if ( $_->setup != 0 ) {
746 push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
748 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
751 if ( $_->recur != 0 ) {
753 "$pkg (" . time2str("%x",$_->sdate) . " - " .
754 time2str("%x",$_->edate) . ")",
755 $money_char. sprintf("%10.2f",$_->recur)
758 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
762 push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ]
767 push @buf,['','-----------'];
768 push @buf,['Total New Charges',
769 $money_char. sprintf("%10.2f",$self->charged) ];
772 push @buf,['','-----------'];
773 push @buf,['Total Charges',
774 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
778 foreach ( $self->cust_credited ) {
780 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
782 my $reason = substr($_->cust_credit->reason,0,32);
783 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
784 $reason = " ($reason) " if $reason;
786 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
788 $money_char. sprintf("%10.2f",$_->amount)
791 #foreach ( @cr_cust_credit ) {
793 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
794 # $money_char. sprintf("%10.2f",$_->credited)
798 #get & print payments
799 foreach ( $self->cust_bill_pay ) {
801 #something more elaborate if $_->amount ne ->cust_pay->paid ?
804 "Payment received ". time2str("%x",$_->cust_pay->_date ),
805 $money_char. sprintf("%10.2f",$_->amount )
810 push @buf,['','-----------'];
811 push @buf,['Balance Due', $money_char.
812 sprintf("%10.2f", $balance_due ) ];
814 #setup template variables
816 package FS::cust_bill::_template; #!
817 use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
819 $invnum = $self->invnum;
820 $date = $self->_date;
824 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
826 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
829 #format address (variable for the template)
831 @address = ( '', '', '', '', '', '' );
832 package FS::cust_bill; #!
833 $FS::cust_bill::_template::address[$l++] =
835 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
836 ? " (P.O. #". $cust_main->payinfo. ")"
840 $FS::cust_bill::_template::address[$l++] = $cust_main->company
841 if $cust_main->company;
842 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
843 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
844 if $cust_main->address2;
845 $FS::cust_bill::_template::address[$l++] =
846 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
847 $FS::cust_bill::_template::address[$l++] = $cust_main->country
848 unless $cust_main->country eq 'US';
850 #overdue? (variable for the template)
851 $FS::cust_bill::_template::overdue = (
853 && $today > $self->_date
854 # && $self->printed > 1
855 && $self->printed > 0
858 #and subroutine for the template
860 sub FS::cust_bill::_template::invoice_lines {
863 scalar(@buf) ? shift @buf : [ '', '' ];
868 $FS::cust_bill::_template::page = 1;
872 push @collect, split("\n",
873 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
875 $FS::cust_bill::_template::page++;
878 map "$_\n", @collect;
886 $Id: cust_bill.pm,v 1.21 2002-03-07 14:10:10 ivan Exp $
892 print_text formatting (and some logic :/) is in source, but needs to be
893 slurped in from a file. Also number of lines ($=).
895 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
896 or something similar so the look can be completely customized?)
900 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
901 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base