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;
23 @ISA = qw( FS::Record );
25 #ask FS::UID to run this stuff for us later
26 $FS::UID::callback{'FS::cust_bill'} = sub {
30 $money_char = $conf->config('money_char') || '$';
32 my @invoice_template = $conf->config('invoice_template')
33 or die "cannot load config file invoice_template";
35 foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
36 /invoice_lines\((\d+)\)/;
39 die "no invoice_lines() functions in template?" unless $invoice_lines;
40 $invoice_template = new Text::Template (
42 SOURCE => [ map "$_\n", @invoice_template ],
43 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
44 $invoice_template->compile()
45 or die "can't compile template: $Text::Template::ERROR";
47 $lpr = $conf->config('lpr');
48 $invoice_from = $conf->config('invoice_from');
49 $smtpmachine = $conf->config('smtpmachine');
51 if ( $conf->exists('cybercash3.2') ) {
53 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
54 require CCMckDirectLib3_2;
56 require CCMckErrno3_2;
57 #qw(MCKGetErrorMessage $E_NoErr);
58 import CCMckErrno3_2 qw($E_NoErr);
61 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
62 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
63 if ( $status != $E_NoErr ) {
64 warn "CCMckLib3_2::InitConfig error:\n";
65 foreach my $key (keys %CCMckLib3_2::Config) {
66 warn " $key => $CCMckLib3_2::Config{$key}\n"
68 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
69 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
71 $processor='cybercash3.2';
72 } elsif ( $conf->exists('business-onlinepayment') ) {
78 ) = $conf->config('business-onlinepayment');
79 $bop_action ||= 'normal authorization';
80 eval "use Business::OnlinePayment";
81 $processor="Business::OnlinePayment::$bop_processor";
88 FS::cust_bill - Object methods for cust_bill records
94 $record = new FS::cust_bill \%hash;
95 $record = new FS::cust_bill { 'column' => 'value' };
97 $error = $record->insert;
99 $error = $new_record->replace($old_record);
101 $error = $record->delete;
103 $error = $record->check;
105 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
107 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
109 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
111 @cust_pay_objects = $cust_bill->cust_pay;
113 $tax_amount = $record->tax;
115 @lines = $cust_bill->print_text;
116 @lines = $cust_bill->print_text $time;
120 An FS::cust_bill object represents an invoice; a declaration that a customer
121 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
122 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
123 following fields are currently supported:
127 =item invnum - primary key (assigned automatically for new invoices)
129 =item custnum - customer (see L<FS::cust_main>)
131 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
132 L<Time::Local> and L<Date::Parse> for conversion functions.
134 =item charged - amount of this invoice
136 =item printed - deprecated
138 =item closed - books closed flag, empty or `Y'
148 Creates a new invoice. To add the invoice to the database, see L<"insert">.
149 Invoices are normally created by calling the bill method of a customer object
150 (see L<FS::cust_main>).
154 sub table { 'cust_bill'; }
158 Adds this invoice to the database ("Posts" the invoice). If there is an error,
159 returns the error, otherwise returns false.
163 Currently unimplemented. I don't remove invoices because there would then be
164 no record you ever posted this invoice (which is bad, no?)
170 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
171 $self->SUPER::delete(@_);
174 =item replace OLD_RECORD
176 Replaces the OLD_RECORD with this one in the database. If there is an error,
177 returns the error, otherwise returns false.
179 Only printed may be changed. printed is normally updated by calling the
180 collect method of a customer object (see L<FS::cust_main>).
185 my( $new, $old ) = ( shift, shift );
186 return "Can't change custnum!" unless $old->custnum == $new->custnum;
187 #return "Can't change _date!" unless $old->_date eq $new->_date;
188 return "Can't change _date!" unless $old->_date == $new->_date;
189 return "Can't change charged!" unless $old->charged == $new->charged;
191 $new->SUPER::replace($old);
196 Checks all fields to make sure this is a valid invoice. If there is an error,
197 returns the error, otherwise returns false. Called by the insert and replace
206 $self->ut_numbern('invnum')
207 || $self->ut_number('custnum')
208 || $self->ut_numbern('_date')
209 || $self->ut_money('charged')
210 || $self->ut_numbern('printed')
211 || $self->ut_enum('closed', [ '', 'Y' ])
213 return $error if $error;
215 return "Unknown customer"
216 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
218 $self->_date(time) unless $self->_date;
220 $self->printed(0) if $self->printed eq '';
227 Returns a list consisting of the total previous balance for this customer,
228 followed by the previous outstanding invoices (as FS::cust_bill objects also).
235 my @cust_bill = sort { $a->_date <=> $b->_date }
236 grep { $_->owed != 0 && $_->_date < $self->_date }
237 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
239 foreach ( @cust_bill ) { $total += $_->owed; }
245 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
251 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
256 Returns the customer (see L<FS::cust_main>) for this invoice.
262 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
267 Depreciated. See the cust_credited method.
269 #Returns a list consisting of the total previous credited (see
270 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
271 #outstanding credits (FS::cust_credit objects).
277 croak "FS::cust_bill->cust_credit depreciated; see ".
278 "FS::cust_bill->cust_credit_bill";
281 #my @cust_credit = sort { $a->_date <=> $b->_date }
282 # grep { $_->credited != 0 && $_->_date < $self->_date }
283 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
285 #foreach (@cust_credit) { $total += $_->credited; }
286 #$total, @cust_credit;
291 Depreciated. See the cust_bill_pay method.
293 #Returns all payments (see L<FS::cust_pay>) for this invoice.
299 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
301 #sort { $a->_date <=> $b->_date }
302 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
308 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
314 sort { $a->_date <=> $b->_date }
315 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
320 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
326 sort { $a->_date <=> $b->_date }
327 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
333 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
340 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
342 foreach (@taxlines) { $total += $_->setup; }
348 Returns the amount owed (still outstanding) on this invoice, which is charged
349 minus all payment applications (see L<FS::cust_bill_pay>) and credit
350 applications (see L<FS::cust_credit_bill>).
356 my $balance = $self->charged;
357 $balance -= $_->amount foreach ( $self->cust_bill_pay );
358 $balance -= $_->amount foreach ( $self->cust_credited );
359 $balance = sprintf( "%.2f", $balance);
360 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
366 Sends this invoice to the destinations configured for this customer: send
367 emails or print. See L<FS::cust_main_invoice>.
374 #my @print_text = $cust_bill->print_text; #( date )
375 my @invoicing_list = $self->cust_main->invoicing_list;
376 if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
377 $ENV{SMTPHOSTS} = $smtpmachine;
378 $ENV{MAILADDRESS} = $invoice_from;
379 my $header = new Mail::Header ( [
380 "From: $invoice_from",
381 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
382 "Sender: $invoice_from",
383 "Reply-To: $invoice_from",
384 "Date: ". time2str("%a, %d %b %Y %X %z", time),
387 my $message = new Mail::Internet (
389 'Body' => [ $self->print_text ], #( date)
392 or return "Can't send invoice email to server $smtpmachine!";
394 #} elsif ( grep { $_ eq 'POST' } @invoicing_list ) {
395 } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
397 or return "Can't open pipe to $lpr: $!";
398 print LPR $self->print_text; #( date )
400 or return $! ? "Error closing $lpr: $!"
401 : "Exit status $? from $lpr";
410 Pays this invoice with a compliemntary payment. If there is an error,
411 returns the error, otherwise returns false.
417 my $cust_pay = new FS::cust_pay ( {
418 'invnum' => $self->invnum,
419 'paid' => $self->owed,
422 'payinfo' => $self->cust_main->payinfo,
430 Attempts to pay this invoice with a Business::OnlinePayment realtime gateway.
431 See http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
432 for supproted processors.
438 my $cust_main = $self->cust_main;
439 my $amount = $self->owed;
441 unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
442 return "Real-time card processing not enabled (processor $processor)";
444 my $bop_processor = $1; #hmm?
446 my $address = $cust_main->address1;
447 $address .= ", ". $cust_main->address2 if $cust_main->address2;
450 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
451 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
454 my($payname, $payfirst, $paylast);
455 if ( $cust_main->payname ) {
456 $payname = $cust_main->payname;
457 $payname =~ /^\s*([\w \,\.\-\']*\w)?\s+([\w\,\.\-\']+)$/
459 #$dbh->rollback if $oldAutoCommit;
460 return "Illegal payname $payname";
462 ($payfirst, $paylast) = ($1, $2);
464 $payfirst = $cust_main->getfield('first');
465 $paylast = $cust_main->getfield('first');
466 $payname = "$payfirst $paylast";
469 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
470 if ( $conf->exists('emailinvoiceauto')
471 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
472 push @invoicing_list, $cust_main->default_invoicing_list;
474 my $email = $invoicing_list[0];
476 my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
479 new Business::OnlinePayment( $bop_processor, @bop_options );
480 $transaction->content(
482 'login' => $bop_login,
483 'password' => $bop_password,
484 'action' => $action1,
485 'description' => 'Internet Services',
487 'invoice_number' => $self->invnum,
488 'customer_id' => $self->custnum,
489 'last_name' => $paylast,
490 'first_name' => $payfirst,
492 'address' => $address,
493 'city' => $cust_main->city,
494 'state' => $cust_main->state,
495 'zip' => $cust_main->zip,
496 'country' => $cust_main->country,
497 'card_number' => $cust_main->payinfo,
498 'expiration' => $exp,
499 'referer' => 'http://cleanwhisker.420.am/',
502 $transaction->submit();
504 if ( $transaction->is_success() && $action2 ) {
505 my $auth = $transaction->authorization;
506 my $ordernum = $transaction->order_number;
507 #warn "********* $auth ***********\n";
508 #warn "********* $ordernum ***********\n";
510 new Business::OnlinePayment( $bop_processor, @bop_options );
515 password => $bop_password,
516 order_number => $ordernum,
518 authorization => $auth,
519 description => 'Internet Services',
524 unless ( $capture->is_success ) {
525 my $e = "Authorization sucessful but capture failed, invnum #".
526 $self->invnum. ': '. $capture->result_code.
527 ": ". $capture->error_message;
534 if ( $transaction->is_success() ) {
536 my $cust_pay = new FS::cust_pay ( {
537 'invnum' => $self->invnum,
541 'payinfo' => $cust_main->payinfo,
542 'paybatch' => "$processor:". $transaction->authorization,
544 my $error = $cust_pay->insert;
546 # gah, even with transactions.
547 my $e = 'WARNING: Card debited but database not updated - '.
548 'error applying payment, invnum #' . $self->invnum.
549 " ($processor): $error";
555 #} elsif ( $options{'report_badcard'} ) {
557 return "$processor error, invnum #". $self->invnum. ': '.
558 $transaction->result_code. ": ". $transaction->error_message;
563 =item realtime_card_cybercash
565 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
569 sub realtime_card_cybercash {
571 my $cust_main = $self->cust_main;
572 my $amount = $self->owed;
574 return "CyberCash CashRegister real-time card processing not enabled!"
575 unless $processor eq 'cybercash3.2';
577 my $address = $cust_main->address1;
578 $address .= ", ". $cust_main->address2 if $cust_main->address2;
581 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
582 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
587 my $paybatch = $self->invnum.
588 '-' . time2str("%y%m%d%H%M%S", time);
590 my $payname = $cust_main->payname ||
591 $cust_main->getfield('first').' '.$cust_main->getfield('last');
593 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
595 my @full_xaction = ( $xaction,
596 'Order-ID' => $paybatch,
597 'Amount' => "usd $amount",
598 'Card-Number' => $cust_main->getfield('payinfo'),
599 'Card-Name' => $payname,
600 'Card-Address' => $address,
601 'Card-City' => $cust_main->getfield('city'),
602 'Card-State' => $cust_main->getfield('state'),
603 'Card-Zip' => $cust_main->getfield('zip'),
604 'Card-Country' => $country,
609 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
611 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
612 my $cust_pay = new FS::cust_pay ( {
613 'invnum' => $self->invnum,
617 'payinfo' => $cust_main->payinfo,
618 'paybatch' => "$processor:$paybatch",
620 my $error = $cust_pay->insert;
622 # gah, even with transactions.
623 my $e = 'WARNING: Card debited but database not updated - '.
624 'error applying payment, invnum #' . $self->invnum.
625 " (CyberCash Order-ID $paybatch): $error";
631 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
632 # || $options{'report_badcard'}
635 return 'Cybercash error, invnum #' .
636 $self->invnum. ':'. $result{'MErrMsg'};
643 Adds a payment for this invoice to the pending credit card batch (see
644 L<FS::cust_pay_batch>).
650 my $cust_main = $self->cust_main;
652 my $cust_pay_batch = new FS::cust_pay_batch ( {
653 'invnum' => $self->getfield('invnum'),
654 'custnum' => $cust_main->getfield('custnum'),
655 'last' => $cust_main->getfield('last'),
656 'first' => $cust_main->getfield('first'),
657 'address1' => $cust_main->getfield('address1'),
658 'address2' => $cust_main->getfield('address2'),
659 'city' => $cust_main->getfield('city'),
660 'state' => $cust_main->getfield('state'),
661 'zip' => $cust_main->getfield('zip'),
662 'country' => $cust_main->getfield('country'),
664 'cardnum' => $cust_main->getfield('payinfo'),
665 'exp' => $cust_main->getfield('paydate'),
666 'payname' => $cust_main->getfield('payname'),
667 'amount' => $self->owed,
669 $cust_pay_batch->insert;
673 =item print_text [TIME];
675 Returns an text invoice, as a list of lines.
677 TIME an optional value used to control the printing of overdue messages. The
678 default is now. It isn't the date of the invoice; that's the `_date' field.
679 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
680 L<Time::Local> and L<Date::Parse> for conversion functions.
686 my( $self, $today ) = ( shift, shift );
688 # my $invnum = $self->invnum;
689 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
690 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
691 unless $cust_main->payname;
693 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
694 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
695 #my $balance_due = $self->owed + $pr_total - $cr_total;
696 my $balance_due = $self->owed + $pr_total;
699 #my($description,$amount);
703 foreach ( @pr_cust_bill ) {
705 "Previous Balance, Invoice #". $_->invnum.
706 " (". time2str("%x",$_->_date). ")",
707 $money_char. sprintf("%10.2f",$_->owed)
711 push @buf,['','-----------'];
712 push @buf,[ 'Total Previous Balance',
713 $money_char. sprintf("%10.2f",$pr_total ) ];
718 foreach ( $self->cust_bill_pkg ) {
722 my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
723 my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
724 my($pkg)=$part_pkg->pkg;
726 if ( $_->setup != 0 ) {
727 push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
729 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
732 if ( $_->recur != 0 ) {
734 "$pkg (" . time2str("%x",$_->sdate) . " - " .
735 time2str("%x",$_->edate) . ")",
736 $money_char. sprintf("%10.2f",$_->recur)
739 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
743 push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ]
748 push @buf,['','-----------'];
749 push @buf,['Total New Charges',
750 $money_char. sprintf("%10.2f",$self->charged) ];
753 push @buf,['','-----------'];
754 push @buf,['Total Charges',
755 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
759 foreach ( $self->cust_credited ) {
761 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
763 my $reason = substr($_->cust_credit->reason,0,32);
764 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
765 $reason = " ($reason) " if $reason;
767 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
769 $money_char. sprintf("%10.2f",$_->amount)
772 #foreach ( @cr_cust_credit ) {
774 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
775 # $money_char. sprintf("%10.2f",$_->credited)
779 #get & print payments
780 foreach ( $self->cust_bill_pay ) {
782 #something more elaborate if $_->amount ne ->cust_pay->paid ?
785 "Payment received ". time2str("%x",$_->cust_pay->_date ),
786 $money_char. sprintf("%10.2f",$_->amount )
791 push @buf,['','-----------'];
792 push @buf,['Balance Due', $money_char.
793 sprintf("%10.2f", $balance_due ) ];
795 #setup template variables
797 package FS::cust_bill::_template; #!
798 use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
800 $invnum = $self->invnum;
801 $date = $self->_date;
805 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
807 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
810 #format address (variable for the template)
812 @address = ( '', '', '', '', '', '' );
813 package FS::cust_bill; #!
814 $FS::cust_bill::_template::address[$l++] =
816 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
817 ? " (P.O. #". $cust_main->payinfo. ")"
821 $FS::cust_bill::_template::address[$l++] = $cust_main->company
822 if $cust_main->company;
823 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
824 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
825 if $cust_main->address2;
826 $FS::cust_bill::_template::address[$l++] =
827 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
828 $FS::cust_bill::_template::address[$l++] = $cust_main->country
829 unless $cust_main->country eq 'US';
831 #overdue? (variable for the template)
832 $FS::cust_bill::_template::overdue = (
834 && $today > $self->_date
835 # && $self->printed > 1
836 && $self->printed > 0
839 #and subroutine for the template
841 sub FS::cust_bill::_template::invoice_lines {
844 scalar(@buf) ? shift @buf : [ '', '' ];
849 $FS::cust_bill::_template::page = 1;
853 push @collect, split("\n",
854 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
856 $FS::cust_bill::_template::page++;
859 map "$_\n", @collect;
867 $Id: cust_bill.pm,v 1.17 2002-02-06 15:50:54 ivan Exp $
873 print_text formatting (and some logic :/) is in source, but needs to be
874 slurped in from a file. Also number of lines ($=).
876 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
877 or something similar so the look can be completely customized?)
881 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
882 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base