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 ( $bop_processor,$bop_login, $bop_password, $bop_action ) = ( '', '', '', '');
42 ( $ach_processor,$ach_login, $ach_password, $ach_action ) = ( '', '', '', '');
45 if ( $conf->exists('cybercash3.2') ) {
47 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
48 require CCMckDirectLib3_2;
50 require CCMckErrno3_2;
51 #qw(MCKGetErrorMessage $E_NoErr);
52 import CCMckErrno3_2 qw($E_NoErr);
55 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
56 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
57 if ( $status != $E_NoErr ) {
58 warn "CCMckLib3_2::InitConfig error:\n";
59 foreach my $key (keys %CCMckLib3_2::Config) {
60 warn " $key => $CCMckLib3_2::Config{$key}\n"
62 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
63 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
65 $cybercash='cybercash3.2';
66 } elsif ( $conf->exists('business-onlinepayment') ) {
72 ) = $conf->config('business-onlinepayment');
73 $bop_action ||= 'normal authorization';
74 ( $ach_processor, $ach_login, $ach_password, $ach_action, @ach_options ) =
75 ( $bop_processor, $bop_login, $bop_password, $bop_action, @bop_options );
76 eval "use Business::OnlinePayment";
79 if ( $conf->exists('business-onlinepayment-ach') ) {
85 ) = $conf->config('business-onlinepayment-ach');
86 $ach_action ||= 'normal authorization';
87 eval "use Business::OnlinePayment";
94 FS::cust_bill - Object methods for cust_bill records
100 $record = new FS::cust_bill \%hash;
101 $record = new FS::cust_bill { 'column' => 'value' };
103 $error = $record->insert;
105 $error = $new_record->replace($old_record);
107 $error = $record->delete;
109 $error = $record->check;
111 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
113 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
115 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
117 @cust_pay_objects = $cust_bill->cust_pay;
119 $tax_amount = $record->tax;
121 @lines = $cust_bill->print_text;
122 @lines = $cust_bill->print_text $time;
126 An FS::cust_bill object represents an invoice; a declaration that a customer
127 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
128 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
129 following fields are currently supported:
133 =item invnum - primary key (assigned automatically for new invoices)
135 =item custnum - customer (see L<FS::cust_main>)
137 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
138 L<Time::Local> and L<Date::Parse> for conversion functions.
140 =item charged - amount of this invoice
142 =item printed - deprecated
144 =item closed - books closed flag, empty or `Y'
154 Creates a new invoice. To add the invoice to the database, see L<"insert">.
155 Invoices are normally created by calling the bill method of a customer object
156 (see L<FS::cust_main>).
160 sub table { 'cust_bill'; }
164 Adds this invoice to the database ("Posts" the invoice). If there is an error,
165 returns the error, otherwise returns false.
169 Currently unimplemented. I don't remove invoices because there would then be
170 no record you ever posted this invoice (which is bad, no?)
176 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
177 $self->SUPER::delete(@_);
180 =item replace OLD_RECORD
182 Replaces the OLD_RECORD with this one in the database. If there is an error,
183 returns the error, otherwise returns false.
185 Only printed may be changed. printed is normally updated by calling the
186 collect method of a customer object (see L<FS::cust_main>).
191 my( $new, $old ) = ( shift, shift );
192 return "Can't change custnum!" unless $old->custnum == $new->custnum;
193 #return "Can't change _date!" unless $old->_date eq $new->_date;
194 return "Can't change _date!" unless $old->_date == $new->_date;
195 return "Can't change charged!" unless $old->charged == $new->charged;
197 $new->SUPER::replace($old);
202 Checks all fields to make sure this is a valid invoice. If there is an error,
203 returns the error, otherwise returns false. Called by the insert and replace
212 $self->ut_numbern('invnum')
213 || $self->ut_number('custnum')
214 || $self->ut_numbern('_date')
215 || $self->ut_money('charged')
216 || $self->ut_numbern('printed')
217 || $self->ut_enum('closed', [ '', 'Y' ])
219 return $error if $error;
221 return "Unknown customer"
222 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
224 $self->_date(time) unless $self->_date;
226 $self->printed(0) if $self->printed eq '';
233 Returns a list consisting of the total previous balance for this customer,
234 followed by the previous outstanding invoices (as FS::cust_bill objects also).
241 my @cust_bill = sort { $a->_date <=> $b->_date }
242 grep { $_->owed != 0 && $_->_date < $self->_date }
243 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
245 foreach ( @cust_bill ) { $total += $_->owed; }
251 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
257 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
260 =item cust_bill_event
262 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
267 sub cust_bill_event {
269 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
275 Returns the customer (see L<FS::cust_main>) for this invoice.
281 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
286 Depreciated. See the cust_credited method.
288 #Returns a list consisting of the total previous credited (see
289 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
290 #outstanding credits (FS::cust_credit objects).
296 croak "FS::cust_bill->cust_credit depreciated; see ".
297 "FS::cust_bill->cust_credit_bill";
300 #my @cust_credit = sort { $a->_date <=> $b->_date }
301 # grep { $_->credited != 0 && $_->_date < $self->_date }
302 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
304 #foreach (@cust_credit) { $total += $_->credited; }
305 #$total, @cust_credit;
310 Depreciated. See the cust_bill_pay method.
312 #Returns all payments (see L<FS::cust_pay>) for this invoice.
318 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
320 #sort { $a->_date <=> $b->_date }
321 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
327 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
333 sort { $a->_date <=> $b->_date }
334 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
339 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
345 sort { $a->_date <=> $b->_date }
346 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
352 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
359 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
361 foreach (@taxlines) { $total += $_->setup; }
367 Returns the amount owed (still outstanding) on this invoice, which is charged
368 minus all payment applications (see L<FS::cust_bill_pay>) and credit
369 applications (see L<FS::cust_credit_bill>).
375 my $balance = $self->charged;
376 $balance -= $_->amount foreach ( $self->cust_bill_pay );
377 $balance -= $_->amount foreach ( $self->cust_credited );
378 $balance = sprintf( "%.2f", $balance);
379 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
385 Sends this invoice to the destinations configured for this customer: send
386 emails or print. See L<FS::cust_main_invoice>.
391 my($self,$template) = @_;
392 my @print_text = $self->print_text('', $template);
393 my @invoicing_list = $self->cust_main->invoicing_list;
395 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
397 #better to notify this person than silence
398 @invoicing_list = ($invoice_from) unless @invoicing_list;
400 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
401 #$ENV{SMTPHOSTS} = $smtpmachine;
402 $ENV{MAILADDRESS} = $invoice_from;
403 my $header = new Mail::Header ( [
404 "From: $invoice_from",
405 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
406 "Sender: $invoice_from",
407 "Reply-To: $invoice_from",
408 "Date: ". time2str("%a, %d %b %Y %X %z", time),
411 my $message = new Mail::Internet (
413 'Body' => [ @print_text ], #( date)
416 $message->smtpsend( Host => $smtpmachine )
417 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
418 or return "(customer # ". $self->custnum. ") can't send invoice email".
419 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
420 " via server $smtpmachine with SMTP: $!";
424 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
426 or return "Can't open pipe to $lpr: $!";
427 print LPR @print_text;
429 or return $! ? "Error closing $lpr: $!"
430 : "Exit status $? from $lpr";
437 =item send_csv OPTIONS
439 Sends invoice as a CSV data-file to a remote host with the specified protocol.
443 protocol - currently only "ftp"
449 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
450 and YYMMDDHHMMSS is a timestamp.
452 The fields of the CSV file is as follows:
454 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
458 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
460 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
461 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
462 fields are filled in.
464 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
465 first two fields (B<record_type> and B<invnum>) and the last five fields
466 (B<pkg> through B<edate>) are filled in.
468 =item invnum - invoice number
470 =item custnum - customer number
472 =item _date - invoice date
474 =item charged - total invoice amount
476 =item first - customer first name
478 =item last - customer first name
480 =item company - company name
482 =item address1 - address line 1
484 =item address2 - address line 1
494 =item pkg - line item description
496 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
498 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
500 =item sdate - start date for recurring fee
502 =item edate - end date for recurring fee
509 my($self, %opt) = @_;
511 #part one: create file
513 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
514 mkdir $spooldir, 0700 unless -d $spooldir;
516 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
518 open(CSV, ">$file") or die "can't open $file: $!";
520 eval "use Text::CSV_XS";
523 my $csv = Text::CSV_XS->new({'always_quote'=>1});
525 my $cust_main = $self->cust_main;
531 time2str("%x", $self->_date),
532 sprintf("%.2f", $self->charged),
533 ( map { $cust_main->getfield($_) }
534 qw( first last company address1 address2 city state zip country ) ),
536 ) or die "can't create csv";
537 print CSV $csv->string. "\n";
539 #new charges (false laziness w/print_text)
540 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
542 my($pkg, $setup, $recur, $sdate, $edate);
543 if ( $cust_bill_pkg->pkgnum ) {
545 ($pkg, $setup, $recur, $sdate, $edate) = (
546 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
547 ( $cust_bill_pkg->setup != 0
548 ? sprintf("%.2f", $cust_bill_pkg->setup )
550 ( $cust_bill_pkg->recur != 0
551 ? sprintf("%.2f", $cust_bill_pkg->recur )
553 time2str("%x", $cust_bill_pkg->sdate),
554 time2str("%x", $cust_bill_pkg->edate),
558 next unless $cust_bill_pkg->setup != 0;
559 ($pkg, $setup, $recur, $sdate, $edate) =
560 ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
566 ( map { '' } (1..11) ),
567 ($pkg, $setup, $recur, $sdate, $edate)
568 ) or die "can't create csv";
569 print CSV $csv->string. "\n";
573 close CSV or die "can't close CSV: $!";
578 if ( $opt{protocol} eq 'ftp' ) {
579 eval "use Net::FTP;";
581 $net = Net::FTP->new($opt{server}) or die @$;
583 die "unknown protocol: $opt{protocol}";
586 $net->login( $opt{username}, $opt{password} )
587 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
589 $net->binary or die "can't set binary mode";
591 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
593 $net->put($file) or die "can't put $file: $!";
603 Pays this invoice with a compliemntary payment. If there is an error,
604 returns the error, otherwise returns false.
610 my $cust_pay = new FS::cust_pay ( {
611 'invnum' => $self->invnum,
612 'paid' => $self->owed,
615 'payinfo' => $self->cust_main->payinfo,
623 Attempts to pay this invoice with a credit card payment via a
624 Business::OnlinePayment realtime gateway. See
625 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
626 for supported processors.
645 Attempts to pay this invoice with an electronic check (ACH) payment via a
646 Business::OnlinePayment realtime gateway. See
647 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
648 for supported processors.
667 Attempts to pay this invoice with phone bill (LEC) payment via a
668 Business::OnlinePayment realtime gateway. See
669 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
670 for supported processors.
688 my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
690 #trim an extraneous blank line
691 pop @$options if scalar(@$options) % 2 && $options->[-1] =~ /^\s*$/;
693 my $cust_main = $self->cust_main;
694 my $amount = $self->owed;
696 my $address = $cust_main->address1;
697 $address .= ", ". $cust_main->address2 if $cust_main->address2;
699 my($payname, $payfirst, $paylast);
700 if ( $cust_main->payname && $method ne 'ECHECK' ) {
701 $payname = $cust_main->payname;
702 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
704 #$dbh->rollback if $oldAutoCommit;
705 return "Illegal payname $payname";
707 ($payfirst, $paylast) = ($1, $2);
709 $payfirst = $cust_main->getfield('first');
710 $paylast = $cust_main->getfield('last');
711 $payname = "$payfirst $paylast";
714 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
715 if ( $conf->exists('emailinvoiceauto')
716 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
717 push @invoicing_list, $cust_main->all_emails;
719 my $email = $invoicing_list[0];
721 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
723 my $description = 'Internet Services';
724 if ( $conf->exists('business-onlinepayment-description') ) {
725 my $dtempl = $conf->config('business-onlinepayment-description');
727 my $agent_obj = $cust_main->agent
728 or die "can't retreive agent for $cust_main (agentnum ".
729 $cust_main->agentnum. ")";
730 my $agent = $agent_obj->agent;
731 my $pkgs = join(', ',
732 map { $_->cust_pkg->part_pkg->pkg }
733 grep { $_->pkgnum } $self->cust_bill_pkg
735 $description = eval qq("$dtempl");
740 if ( $method eq 'CC' ) {
741 $content{card_number} = $cust_main->payinfo;
742 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
743 $content{expiration} = "$2/$1";
744 } elsif ( $method eq 'ECHECK' ) {
745 my($account_number,$routing_code) = $cust_main->payinfo;
746 ( $content{account_number}, $content{routing_code} ) =
747 split('@', $cust_main->payinfo);
748 $content{bank_name} = $cust_main->payname;
749 $content{account_type} = 'CHECKING';
750 $content{account_name} = $payname;
751 $content{customer_org} = $self->company ? 'B' : 'I';
752 $content{customer_ssn} = $self->ss;
753 } elsif ( $method eq 'LEC' ) {
754 $content{phone} = $cust_main->payinfo;
758 new Business::OnlinePayment( $processor, @$options );
759 $transaction->content(
762 'password' => $password,
763 'action' => $action1,
764 'description' => $description,
766 'invoice_number' => $self->invnum,
767 'customer_id' => $self->custnum,
768 'last_name' => $paylast,
769 'first_name' => $payfirst,
771 'address' => $address,
772 'city' => $cust_main->city,
773 'state' => $cust_main->state,
774 'zip' => $cust_main->zip,
775 'country' => $cust_main->country,
776 'referer' => 'http://cleanwhisker.420.am/',
778 'phone' => $cust_main->daytime || $cust_main->night,
781 $transaction->submit();
783 if ( $transaction->is_success() && $action2 ) {
784 my $auth = $transaction->authorization;
785 my $ordernum = $transaction->can('order_number')
786 ? $transaction->order_number
789 #warn "********* $auth ***********\n";
790 #warn "********* $ordernum ***********\n";
792 new Business::OnlinePayment( $processor, @$options );
799 password => $password,
800 order_number => $ordernum,
802 authorization => $auth,
803 description => $description,
806 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
807 transaction_sequence_num local_transaction_date
808 local_transaction_time AVS_result_code )) {
809 $capture{$field} = $transaction->$field() if $transaction->can($field);
812 $capture->content( %capture );
816 unless ( $capture->is_success ) {
817 my $e = "Authorization sucessful but capture failed, invnum #".
818 $self->invnum. ': '. $capture->result_code.
819 ": ". $capture->error_message;
826 if ( $transaction->is_success() ) {
834 my $cust_pay = new FS::cust_pay ( {
835 'invnum' => $self->invnum,
838 'payby' => $method2payby{$method},
839 'payinfo' => $cust_main->payinfo,
840 'paybatch' => "$processor:". $transaction->authorization,
842 my $error = $cust_pay->insert;
844 # gah, even with transactions.
845 my $e = 'WARNING: Card/ACH debited but database not updated - '.
846 'error applying payment, invnum #' . $self->invnum.
847 " ($processor): $error";
853 #} elsif ( $options{'report_badcard'} ) {
856 my $perror = "$processor error, invnum #". $self->invnum. ': '.
857 $transaction->result_code. ": ". $transaction->error_message;
859 if ( !$quiet && $conf->exists('emaildecline')
860 && grep { $_ ne 'POST' } $cust_main->invoicing_list
862 my @templ = $conf->config('declinetemplate');
863 my $template = new Text::Template (
865 SOURCE => [ map "$_\n", @templ ],
866 ) or return "($perror) can't create template: $Text::Template::ERROR";
868 or return "($perror) can't compile template: $Text::Template::ERROR";
870 my $templ_hash = { error => $transaction->error_message };
872 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
873 $ENV{MAILADDRESS} = $invoice_from;
874 my $header = new Mail::Header ( [
875 "From: $invoice_from",
876 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
877 "Sender: $invoice_from",
878 "Reply-To: $invoice_from",
879 "Date: ". time2str("%a, %d %b %Y %X %z", time),
880 "Subject: Your payment could not be processed",
882 my $message = new Mail::Internet (
884 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
887 $message->smtpsend( Host => $smtpmachine )
888 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
889 or return "($perror) (customer # ". $self->custnum.
890 ") can't send card decline email to ".
891 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
892 " via server $smtpmachine with SMTP: $!";
900 =item realtime_card_cybercash
902 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
906 sub realtime_card_cybercash {
908 my $cust_main = $self->cust_main;
909 my $amount = $self->owed;
911 return "CyberCash CashRegister real-time card processing not enabled!"
912 unless $cybercash eq 'cybercash3.2';
914 my $address = $cust_main->address1;
915 $address .= ", ". $cust_main->address2 if $cust_main->address2;
918 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
919 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
924 my $paybatch = $self->invnum.
925 '-' . time2str("%y%m%d%H%M%S", time);
927 my $payname = $cust_main->payname ||
928 $cust_main->getfield('first').' '.$cust_main->getfield('last');
930 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
932 my @full_xaction = ( $xaction,
933 'Order-ID' => $paybatch,
934 'Amount' => "usd $amount",
935 'Card-Number' => $cust_main->getfield('payinfo'),
936 'Card-Name' => $payname,
937 'Card-Address' => $address,
938 'Card-City' => $cust_main->getfield('city'),
939 'Card-State' => $cust_main->getfield('state'),
940 'Card-Zip' => $cust_main->getfield('zip'),
941 'Card-Country' => $country,
946 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
948 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
949 my $cust_pay = new FS::cust_pay ( {
950 'invnum' => $self->invnum,
954 'payinfo' => $cust_main->payinfo,
955 'paybatch' => "$cybercash:$paybatch",
957 my $error = $cust_pay->insert;
959 # gah, even with transactions.
960 my $e = 'WARNING: Card debited but database not updated - '.
961 'error applying payment, invnum #' . $self->invnum.
962 " (CyberCash Order-ID $paybatch): $error";
968 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
969 # || $options{'report_badcard'}
972 return 'Cybercash error, invnum #' .
973 $self->invnum. ':'. $result{'MErrMsg'};
980 Adds a payment for this invoice to the pending credit card batch (see
981 L<FS::cust_pay_batch>).
987 my $cust_main = $self->cust_main;
989 my $cust_pay_batch = new FS::cust_pay_batch ( {
990 'invnum' => $self->getfield('invnum'),
991 'custnum' => $cust_main->getfield('custnum'),
992 'last' => $cust_main->getfield('last'),
993 'first' => $cust_main->getfield('first'),
994 'address1' => $cust_main->getfield('address1'),
995 'address2' => $cust_main->getfield('address2'),
996 'city' => $cust_main->getfield('city'),
997 'state' => $cust_main->getfield('state'),
998 'zip' => $cust_main->getfield('zip'),
999 'country' => $cust_main->getfield('country'),
1001 'cardnum' => $cust_main->getfield('payinfo'),
1002 'exp' => $cust_main->getfield('paydate'),
1003 'payname' => $cust_main->getfield('payname'),
1004 'amount' => $self->owed,
1006 my $error = $cust_pay_batch->insert;
1007 die $error if $error;
1012 =item print_text [TIME];
1014 Returns an text invoice, as a list of lines.
1016 TIME an optional value used to control the printing of overdue messages. The
1017 default is now. It isn't the date of the invoice; that's the `_date' field.
1018 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1019 L<Time::Local> and L<Date::Parse> for conversion functions.
1025 my( $self, $today, $template ) = @_;
1027 # my $invnum = $self->invnum;
1028 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
1029 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1030 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1032 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1033 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1034 #my $balance_due = $self->owed + $pr_total - $cr_total;
1035 my $balance_due = $self->owed + $pr_total;
1038 #my($description,$amount);
1042 foreach ( @pr_cust_bill ) {
1044 "Previous Balance, Invoice #". $_->invnum.
1045 " (". time2str("%x",$_->_date). ")",
1046 $money_char. sprintf("%10.2f",$_->owed)
1049 if (@pr_cust_bill) {
1050 push @buf,['','-----------'];
1051 push @buf,[ 'Total Previous Balance',
1052 $money_char. sprintf("%10.2f",$pr_total ) ];
1057 foreach ( $self->cust_bill_pkg ) {
1061 my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
1062 my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
1063 my($pkg)=$part_pkg->pkg;
1065 if ( $_->setup != 0 ) {
1066 push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
1068 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1071 if ( $_->recur != 0 ) {
1073 "$pkg (" . time2str("%x",$_->sdate) . " - " .
1074 time2str("%x",$_->edate) . ")",
1075 $money_char. sprintf("%10.2f",$_->recur)
1078 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1081 } else { #pkgnum Tax
1082 push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ]
1087 push @buf,['','-----------'];
1088 push @buf,['Total New Charges',
1089 $money_char. sprintf("%10.2f",$self->charged) ];
1092 push @buf,['','-----------'];
1093 push @buf,['Total Charges',
1094 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1098 foreach ( $self->cust_credited ) {
1100 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1102 my $reason = substr($_->cust_credit->reason,0,32);
1103 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1104 $reason = " ($reason) " if $reason;
1106 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1108 $money_char. sprintf("%10.2f",$_->amount)
1111 #foreach ( @cr_cust_credit ) {
1113 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1114 # $money_char. sprintf("%10.2f",$_->credited)
1118 #get & print payments
1119 foreach ( $self->cust_bill_pay ) {
1121 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1124 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1125 $money_char. sprintf("%10.2f",$_->amount )
1130 push @buf,['','-----------'];
1131 push @buf,['Balance Due', $money_char.
1132 sprintf("%10.2f", $balance_due ) ];
1134 #create the template
1135 my $templatefile = 'invoice_template';
1136 $templatefile .= "_$template" if $template;
1137 my @invoice_template = $conf->config($templatefile)
1138 or die "cannot load config file $templatefile";
1141 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1142 /invoice_lines\((\d*)\)/;
1143 $invoice_lines += $1 || scalar(@buf);
1146 die "no invoice_lines() functions in template?" unless $wasfunc;
1147 my $invoice_template = new Text::Template (
1149 SOURCE => [ map "$_\n", @invoice_template ],
1150 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1151 $invoice_template->compile()
1152 or die "can't compile template: $Text::Template::ERROR";
1154 #setup template variables
1155 package FS::cust_bill::_template; #!
1156 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1158 $invnum = $self->invnum;
1159 $date = $self->_date;
1161 $agent = $self->cust_main->agent->agent;
1163 if ( $FS::cust_bill::invoice_lines ) {
1165 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1167 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1172 #format address (variable for the template)
1174 @address = ( '', '', '', '', '', '' );
1175 package FS::cust_bill; #!
1176 $FS::cust_bill::_template::address[$l++] =
1177 $cust_main->payname.
1178 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1179 ? " (P.O. #". $cust_main->payinfo. ")"
1183 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1184 if $cust_main->company;
1185 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1186 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1187 if $cust_main->address2;
1188 $FS::cust_bill::_template::address[$l++] =
1189 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1190 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1191 unless $cust_main->country eq 'US';
1193 # #overdue? (variable for the template)
1194 # $FS::cust_bill::_template::overdue = (
1196 # && $today > $self->_date
1197 ## && $self->printed > 1
1198 # && $self->printed > 0
1201 #and subroutine for the template
1202 sub FS::cust_bill::_template::invoice_lines {
1203 my $lines = shift || scalar(@buf);
1205 scalar(@buf) ? shift @buf : [ '', '' ];
1211 $FS::cust_bill::_template::page = 1;
1215 push @collect, split("\n",
1216 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1218 $FS::cust_bill::_template::page++;
1221 map "$_\n", @collect;
1229 $Id: cust_bill.pm,v 1.41.2.21 2003-06-30 18:56:02 ivan Exp $
1235 print_text formatting (and some logic :/) is in source, but needs to be
1236 slurped in from a file. Also number of lines ($=).
1238 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1239 or something similar so the look can be completely customized?)
1243 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1244 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base