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( $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 #ask FS::UID to run this stuff for us later
30 $FS::UID::callback{'FS::cust_bill'} = sub {
34 $money_char = $conf->config('money_char') || '$';
36 $lpr = $conf->config('lpr');
37 $invoice_from = $conf->config('invoice_from');
38 $smtpmachine = $conf->config('smtpmachine');
40 if ( $conf->exists('cybercash3.2') ) {
42 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
43 require CCMckDirectLib3_2;
45 require CCMckErrno3_2;
46 #qw(MCKGetErrorMessage $E_NoErr);
47 import CCMckErrno3_2 qw($E_NoErr);
50 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
51 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
52 if ( $status != $E_NoErr ) {
53 warn "CCMckLib3_2::InitConfig error:\n";
54 foreach my $key (keys %CCMckLib3_2::Config) {
55 warn " $key => $CCMckLib3_2::Config{$key}\n"
57 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
58 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
60 $cybercash='cybercash3.2';
61 } elsif ( $conf->exists('business-onlinepayment') ) {
67 ) = $conf->config('business-onlinepayment');
68 $bop_action ||= 'normal authorization';
69 ( $ach_processor, $ach_login, $ach_password, $ach_action, @ach_options ) =
70 ( $bop_processor, $bop_login, $bop_password, $bop_action, @bop_options );
71 eval "use Business::OnlinePayment";
74 if ( $conf->exists('business-onlinepayment-ach') ) {
80 ) = $conf->config('business-onlinepayment-ach');
81 $ach_action ||= 'normal authorization';
82 eval "use Business::OnlinePayment";
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>.
386 my($self,$template) = @_;
387 my @print_text = $self->print_text('', $template);
388 my @invoicing_list = $self->cust_main->invoicing_list;
390 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
392 #better to notify this person than silence
393 @invoicing_list = ($invoice_from) unless @invoicing_list;
395 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
396 #$ENV{SMTPHOSTS} = $smtpmachine;
397 $ENV{MAILADDRESS} = $invoice_from;
398 my $header = new Mail::Header ( [
399 "From: $invoice_from",
400 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
401 "Sender: $invoice_from",
402 "Reply-To: $invoice_from",
403 "Date: ". time2str("%a, %d %b %Y %X %z", time),
406 my $message = new Mail::Internet (
408 'Body' => [ @print_text ], #( date)
411 $message->smtpsend( Host => $smtpmachine )
412 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
413 or return "(customer # ". $self->custnum. ") can't send invoice email".
414 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
415 " via server $smtpmachine with SMTP: $!";
419 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
421 or return "Can't open pipe to $lpr: $!";
422 print LPR @print_text;
424 or return $! ? "Error closing $lpr: $!"
425 : "Exit status $? from $lpr";
432 =item send_csv OPTIONS
434 Sends invoice as a CSV data-file to a remote host with the specified protocol.
438 protocol - currently only "ftp"
444 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
445 and YYMMDDHHMMSS is a timestamp.
447 The fields of the CSV file is as follows:
449 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
453 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
455 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
456 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
457 fields are filled in.
459 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
460 first two fields (B<record_type> and B<invnum>) and the last five fields
461 (B<pkg> through B<edate>) are filled in.
463 =item invnum - invoice number
465 =item custnum - customer number
467 =item _date - invoice date
469 =item charged - total invoice amount
471 =item first - customer first name
473 =item last - customer first name
475 =item company - company name
477 =item address1 - address line 1
479 =item address2 - address line 1
489 =item pkg - line item description
491 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
493 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
495 =item sdate - start date for recurring fee
497 =item edate - end date for recurring fee
504 my($self, %opt) = @_;
506 #part one: create file
508 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
509 mkdir $spooldir, 0700 unless -d $spooldir;
511 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
513 open(CSV, ">$file") or die "can't open $file: $!";
515 eval "use Text::CSV_XS";
518 my $csv = Text::CSV_XS->new({'always_quote'=>1});
520 my $cust_main = $self->cust_main;
526 time2str("%x", $self->_date),
527 sprintf("%.2f", $self->charged),
528 ( map { $cust_main->getfield($_) }
529 qw( first last company address1 address2 city state zip country ) ),
531 ) or die "can't create csv";
532 print CSV $csv->string. "\n";
534 #new charges (false laziness w/print_text)
535 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
537 my($pkg, $setup, $recur, $sdate, $edate);
538 if ( $cust_bill_pkg->pkgnum ) {
540 ($pkg, $setup, $recur, $sdate, $edate) = (
541 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
542 ( $cust_bill_pkg->setup != 0
543 ? sprintf("%.2f", $cust_bill_pkg->setup )
545 ( $cust_bill_pkg->recur != 0
546 ? sprintf("%.2f", $cust_bill_pkg->recur )
548 time2str("%x", $cust_bill_pkg->sdate),
549 time2str("%x", $cust_bill_pkg->edate),
553 next unless $cust_bill_pkg->setup != 0;
554 ($pkg, $setup, $recur, $sdate, $edate) =
555 ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
561 ( map { '' } (1..11) ),
562 ($pkg, $setup, $recur, $sdate, $edate)
563 ) or die "can't create csv";
564 print CSV $csv->string. "\n";
568 close CSV or die "can't close CSV: $!";
573 if ( $opt{protocol} eq 'ftp' ) {
574 eval "use Net::FTP;";
576 $net = Net::FTP->new($opt{server}) or die @$;
578 die "unknown protocol: $opt{protocol}";
581 $net->login( $opt{username}, $opt{password} )
582 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
584 $net->binary or die "can't set binary mode";
586 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
588 $net->put($file) or die "can't put $file: $!";
598 Pays this invoice with a compliemntary payment. If there is an error,
599 returns the error, otherwise returns false.
605 my $cust_pay = new FS::cust_pay ( {
606 'invnum' => $self->invnum,
607 'paid' => $self->owed,
610 'payinfo' => $self->cust_main->payinfo,
618 Attempts to pay this invoice with a credit card payment via a
619 Business::OnlinePayment realtime gateway. See
620 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
621 for supported processors.
640 Attempts to pay this invoice with an electronic check (ACH) payment via a
641 Business::OnlinePayment realtime gateway. See
642 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
643 for supported processors.
662 Attempts to pay this invoice with phone bill (LEC) payment via a
663 Business::OnlinePayment realtime gateway. See
664 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
665 for supported processors.
683 my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
684 my $cust_main = $self->cust_main;
685 my $amount = $self->owed;
687 my $address = $cust_main->address1;
688 $address .= ", ". $cust_main->address2 if $cust_main->address2;
690 my($payname, $payfirst, $paylast);
691 if ( $cust_main->payname && $method ne 'ECHECK' ) {
692 $payname = $cust_main->payname;
693 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
695 #$dbh->rollback if $oldAutoCommit;
696 return "Illegal payname $payname";
698 ($payfirst, $paylast) = ($1, $2);
700 $payfirst = $cust_main->getfield('first');
701 $paylast = $cust_main->getfield('last');
702 $payname = "$payfirst $paylast";
705 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
706 if ( $conf->exists('emailinvoiceauto')
707 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
708 push @invoicing_list, $cust_main->all_emails;
710 my $email = $invoicing_list[0];
712 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
714 my $description = 'Internet Services';
715 if ( $conf->exists('business-onlinepayment-description') ) {
716 my $dtempl = $conf->config('business-onlinepayment-description');
718 my $agent_obj = $cust_main->agent
719 or die "can't retreive agent for $cust_main (agentnum ".
720 $cust_main->agentnum. ")";
721 my $agent = $agent_obj->agent;
722 my $pkgs = join(', ',
723 map { $_->cust_pkg->part_pkg->pkg }
724 grep { $_->pkgnum } $self->cust_bill_pkg
726 $description = eval qq("$dtempl");
731 if ( $method eq 'CC' ) {
732 $content{card_number} = $cust_main->payinfo;
733 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
734 $content{expiration} = "$2/$1";
735 } elsif ( $method eq 'ECHECK' ) {
736 my($account_number,$routing_code) = $cust_main->payinfo;
737 ( $content{account_number}, $content{routing_code} ) =
738 split('@', $cust_main->payinfo);
739 $content{bank_name} = $cust_main->payname;
740 } elsif ( $method eq 'LEC' ) {
741 $content{phone} = $cust_main->payinfo;
745 new Business::OnlinePayment( $processor, @$options );
746 $transaction->content(
749 'password' => $password,
750 'action' => $action1,
751 'description' => $description,
753 'invoice_number' => $self->invnum,
754 'customer_id' => $self->custnum,
755 'last_name' => $paylast,
756 'first_name' => $payfirst,
758 'address' => $address,
759 'city' => $cust_main->city,
760 'state' => $cust_main->state,
761 'zip' => $cust_main->zip,
762 'country' => $cust_main->country,
763 'referer' => 'http://cleanwhisker.420.am/',
765 'phone' => $cust_main->daytime || $cust_main->night,
768 $transaction->submit();
770 if ( $transaction->is_success() && $action2 ) {
771 my $auth = $transaction->authorization;
772 my $ordernum = $transaction->can('order_number')
773 ? $transaction->order_number
776 #warn "********* $auth ***********\n";
777 #warn "********* $ordernum ***********\n";
779 new Business::OnlinePayment( $processor, @$options );
786 password => $password,
787 order_number => $ordernum,
789 authorization => $auth,
790 description => $description,
793 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
794 transaction_sequence_num local_transaction_date
795 local_transaction_time AVS_result_code )) {
796 $capture{$field} = $transaction->$field() if $transaction->can($field);
799 $capture->content( %capture );
803 unless ( $capture->is_success ) {
804 my $e = "Authorization sucessful but capture failed, invnum #".
805 $self->invnum. ': '. $capture->result_code.
806 ": ". $capture->error_message;
813 if ( $transaction->is_success() ) {
821 my $cust_pay = new FS::cust_pay ( {
822 'invnum' => $self->invnum,
825 'payby' => $method2payby{$method},
826 'payinfo' => $cust_main->payinfo,
827 'paybatch' => "$processor:". $transaction->authorization,
829 my $error = $cust_pay->insert;
831 # gah, even with transactions.
832 my $e = 'WARNING: Card/ACH debited but database not updated - '.
833 'error applying payment, invnum #' . $self->invnum.
834 " ($processor): $error";
840 #} elsif ( $options{'report_badcard'} ) {
843 my $perror = "$processor error, invnum #". $self->invnum. ': '.
844 $transaction->result_code. ": ". $transaction->error_message;
846 if ( !$quiet && $conf->exists('emaildecline')
847 && grep { $_ ne 'POST' } $cust_main->invoicing_list
849 my @templ = $conf->config('declinetemplate');
850 my $template = new Text::Template (
852 SOURCE => [ map "$_\n", @templ ],
853 ) or return "($perror) can't create template: $Text::Template::ERROR";
855 or return "($perror) can't compile template: $Text::Template::ERROR";
857 my $templ_hash = { error => $transaction->error_message };
859 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
860 $ENV{MAILADDRESS} = $invoice_from;
861 my $header = new Mail::Header ( [
862 "From: $invoice_from",
863 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
864 "Sender: $invoice_from",
865 "Reply-To: $invoice_from",
866 "Date: ". time2str("%a, %d %b %Y %X %z", time),
867 "Subject: Your payment could not be processed",
869 my $message = new Mail::Internet (
871 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
874 $message->smtpsend( Host => $smtpmachine )
875 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
876 or return "($perror) (customer # ". $self->custnum.
877 ") can't send card decline email to ".
878 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
879 " via server $smtpmachine with SMTP: $!";
887 =item realtime_card_cybercash
889 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
893 sub realtime_card_cybercash {
895 my $cust_main = $self->cust_main;
896 my $amount = $self->owed;
898 return "CyberCash CashRegister real-time card processing not enabled!"
899 unless $cybercash eq 'cybercash3.2';
901 my $address = $cust_main->address1;
902 $address .= ", ". $cust_main->address2 if $cust_main->address2;
905 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
906 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
911 my $paybatch = $self->invnum.
912 '-' . time2str("%y%m%d%H%M%S", time);
914 my $payname = $cust_main->payname ||
915 $cust_main->getfield('first').' '.$cust_main->getfield('last');
917 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
919 my @full_xaction = ( $xaction,
920 'Order-ID' => $paybatch,
921 'Amount' => "usd $amount",
922 'Card-Number' => $cust_main->getfield('payinfo'),
923 'Card-Name' => $payname,
924 'Card-Address' => $address,
925 'Card-City' => $cust_main->getfield('city'),
926 'Card-State' => $cust_main->getfield('state'),
927 'Card-Zip' => $cust_main->getfield('zip'),
928 'Card-Country' => $country,
933 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
935 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
936 my $cust_pay = new FS::cust_pay ( {
937 'invnum' => $self->invnum,
941 'payinfo' => $cust_main->payinfo,
942 'paybatch' => "$cybercash:$paybatch",
944 my $error = $cust_pay->insert;
946 # gah, even with transactions.
947 my $e = 'WARNING: Card debited but database not updated - '.
948 'error applying payment, invnum #' . $self->invnum.
949 " (CyberCash Order-ID $paybatch): $error";
955 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
956 # || $options{'report_badcard'}
959 return 'Cybercash error, invnum #' .
960 $self->invnum. ':'. $result{'MErrMsg'};
967 Adds a payment for this invoice to the pending credit card batch (see
968 L<FS::cust_pay_batch>).
974 my $cust_main = $self->cust_main;
976 my $cust_pay_batch = new FS::cust_pay_batch ( {
977 'invnum' => $self->getfield('invnum'),
978 'custnum' => $cust_main->getfield('custnum'),
979 'last' => $cust_main->getfield('last'),
980 'first' => $cust_main->getfield('first'),
981 'address1' => $cust_main->getfield('address1'),
982 'address2' => $cust_main->getfield('address2'),
983 'city' => $cust_main->getfield('city'),
984 'state' => $cust_main->getfield('state'),
985 'zip' => $cust_main->getfield('zip'),
986 'country' => $cust_main->getfield('country'),
988 'cardnum' => $cust_main->getfield('payinfo'),
989 'exp' => $cust_main->getfield('paydate'),
990 'payname' => $cust_main->getfield('payname'),
991 'amount' => $self->owed,
993 my $error = $cust_pay_batch->insert;
994 die $error if $error;
999 =item print_text [TIME];
1001 Returns an text invoice, as a list of lines.
1003 TIME an optional value used to control the printing of overdue messages. The
1004 default is now. It isn't the date of the invoice; that's the `_date' field.
1005 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1006 L<Time::Local> and L<Date::Parse> for conversion functions.
1012 my( $self, $today, $template ) = @_;
1014 # my $invnum = $self->invnum;
1015 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
1016 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1017 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1019 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1020 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1021 #my $balance_due = $self->owed + $pr_total - $cr_total;
1022 my $balance_due = $self->owed + $pr_total;
1025 #my($description,$amount);
1029 foreach ( @pr_cust_bill ) {
1031 "Previous Balance, Invoice #". $_->invnum.
1032 " (". time2str("%x",$_->_date). ")",
1033 $money_char. sprintf("%10.2f",$_->owed)
1036 if (@pr_cust_bill) {
1037 push @buf,['','-----------'];
1038 push @buf,[ 'Total Previous Balance',
1039 $money_char. sprintf("%10.2f",$pr_total ) ];
1044 foreach ( $self->cust_bill_pkg ) {
1048 my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
1049 my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
1050 my($pkg)=$part_pkg->pkg;
1052 if ( $_->setup != 0 ) {
1053 push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
1055 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1058 if ( $_->recur != 0 ) {
1060 "$pkg (" . time2str("%x",$_->sdate) . " - " .
1061 time2str("%x",$_->edate) . ")",
1062 $money_char. sprintf("%10.2f",$_->recur)
1065 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1068 } else { #pkgnum Tax
1069 push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ]
1074 push @buf,['','-----------'];
1075 push @buf,['Total New Charges',
1076 $money_char. sprintf("%10.2f",$self->charged) ];
1079 push @buf,['','-----------'];
1080 push @buf,['Total Charges',
1081 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1085 foreach ( $self->cust_credited ) {
1087 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1089 my $reason = substr($_->cust_credit->reason,0,32);
1090 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1091 $reason = " ($reason) " if $reason;
1093 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1095 $money_char. sprintf("%10.2f",$_->amount)
1098 #foreach ( @cr_cust_credit ) {
1100 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1101 # $money_char. sprintf("%10.2f",$_->credited)
1105 #get & print payments
1106 foreach ( $self->cust_bill_pay ) {
1108 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1111 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1112 $money_char. sprintf("%10.2f",$_->amount )
1117 push @buf,['','-----------'];
1118 push @buf,['Balance Due', $money_char.
1119 sprintf("%10.2f", $balance_due ) ];
1121 #create the template
1122 my $templatefile = 'invoice_template';
1123 $templatefile .= "_$template" if $template;
1124 my @invoice_template = $conf->config($templatefile)
1125 or die "cannot load config file $templatefile";
1128 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1129 /invoice_lines\((\d*)\)/;
1130 $invoice_lines += $1 || scalar(@buf);
1133 die "no invoice_lines() functions in template?" unless $wasfunc;
1134 my $invoice_template = new Text::Template (
1136 SOURCE => [ map "$_\n", @invoice_template ],
1137 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1138 $invoice_template->compile()
1139 or die "can't compile template: $Text::Template::ERROR";
1141 #setup template variables
1142 package FS::cust_bill::_template; #!
1143 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1145 $invnum = $self->invnum;
1146 $date = $self->_date;
1148 $agent = $self->cust_main->agent->agent;
1150 if ( $FS::cust_bill::invoice_lines ) {
1152 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1154 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1159 #format address (variable for the template)
1161 @address = ( '', '', '', '', '', '' );
1162 package FS::cust_bill; #!
1163 $FS::cust_bill::_template::address[$l++] =
1164 $cust_main->payname.
1165 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1166 ? " (P.O. #". $cust_main->payinfo. ")"
1170 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1171 if $cust_main->company;
1172 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1173 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1174 if $cust_main->address2;
1175 $FS::cust_bill::_template::address[$l++] =
1176 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1177 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1178 unless $cust_main->country eq 'US';
1180 # #overdue? (variable for the template)
1181 # $FS::cust_bill::_template::overdue = (
1183 # && $today > $self->_date
1184 ## && $self->printed > 1
1185 # && $self->printed > 0
1188 #and subroutine for the template
1189 sub FS::cust_bill::_template::invoice_lines {
1190 my $lines = shift || scalar(@buf);
1192 scalar(@buf) ? shift @buf : [ '', '' ];
1198 $FS::cust_bill::_template::page = 1;
1202 push @collect, split("\n",
1203 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1205 $FS::cust_bill::_template::page++;
1208 map "$_\n", @collect;
1216 $Id: cust_bill.pm,v 1.41.2.18 2002-12-23 15:22:46 ivan Exp $
1222 print_text formatting (and some logic :/) is in source, but needs to be
1223 slurped in from a file. Also number of lines ($=).
1225 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1226 or something similar so the look can be completely customized?)
1230 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1231 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base