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
12 use Mail::Internet 1.44;
15 use FS::UID qw( datasrc );
16 use FS::Record qw( qsearch qsearchs );
18 use FS::cust_bill_pkg;
22 use FS::cust_credit_bill;
23 use FS::cust_pay_batch;
24 use FS::cust_bill_event;
26 @ISA = qw( FS::Record );
28 #ask FS::UID to run this stuff for us later
29 $FS::UID::callback{'FS::cust_bill'} = sub {
33 $money_char = $conf->config('money_char') || '$';
35 $lpr = $conf->config('lpr');
36 $invoice_from = $conf->config('invoice_from');
37 $smtpmachine = $conf->config('smtpmachine');
39 if ( $conf->exists('cybercash3.2') ) {
41 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
42 require CCMckDirectLib3_2;
44 require CCMckErrno3_2;
45 #qw(MCKGetErrorMessage $E_NoErr);
46 import CCMckErrno3_2 qw($E_NoErr);
49 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
50 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
51 if ( $status != $E_NoErr ) {
52 warn "CCMckLib3_2::InitConfig error:\n";
53 foreach my $key (keys %CCMckLib3_2::Config) {
54 warn " $key => $CCMckLib3_2::Config{$key}\n"
56 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
57 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
59 $cybercash='cybercash3.2';
60 } elsif ( $conf->exists('business-onlinepayment') ) {
66 ) = $conf->config('business-onlinepayment');
67 $bop_action ||= 'normal authorization';
68 ( $ach_processor, $ach_login, $ach_password, $ach_action, @ach_options ) =
69 ( $bop_processor, $bop_login, $bop_password, $bop_action, @bop_options );
70 eval "use Business::OnlinePayment";
73 if ( $conf->exists('business-onlinepayment-ach') ) {
79 ) = $conf->config('business-onlinepayment-ach');
80 $ach_action ||= 'normal authorization';
81 eval "use Business::OnlinePayment";
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 } );
254 =item cust_bill_event
256 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
261 sub cust_bill_event {
263 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
269 Returns the customer (see L<FS::cust_main>) for this invoice.
275 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
280 Depreciated. See the cust_credited method.
282 #Returns a list consisting of the total previous credited (see
283 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
284 #outstanding credits (FS::cust_credit objects).
290 croak "FS::cust_bill->cust_credit depreciated; see ".
291 "FS::cust_bill->cust_credit_bill";
294 #my @cust_credit = sort { $a->_date <=> $b->_date }
295 # grep { $_->credited != 0 && $_->_date < $self->_date }
296 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
298 #foreach (@cust_credit) { $total += $_->credited; }
299 #$total, @cust_credit;
304 Depreciated. See the cust_bill_pay method.
306 #Returns all payments (see L<FS::cust_pay>) for this invoice.
312 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
314 #sort { $a->_date <=> $b->_date }
315 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
321 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
327 sort { $a->_date <=> $b->_date }
328 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
333 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
339 sort { $a->_date <=> $b->_date }
340 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
346 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
353 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
355 foreach (@taxlines) { $total += $_->setup; }
361 Returns the amount owed (still outstanding) on this invoice, which is charged
362 minus all payment applications (see L<FS::cust_bill_pay>) and credit
363 applications (see L<FS::cust_credit_bill>).
369 my $balance = $self->charged;
370 $balance -= $_->amount foreach ( $self->cust_bill_pay );
371 $balance -= $_->amount foreach ( $self->cust_credited );
372 $balance = sprintf( "%.2f", $balance);
373 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
379 Sends this invoice to the destinations configured for this customer: send
380 emails or print. See L<FS::cust_main_invoice>.
385 my($self,$template) = @_;
386 my @print_text = $self->print_text('', $template);
387 my @invoicing_list = $self->cust_main->invoicing_list;
389 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
391 #better to notify this person than silence
392 @invoicing_list = ($invoice_from) unless @invoicing_list;
394 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
395 #$ENV{SMTPHOSTS} = $smtpmachine;
396 $ENV{MAILADDRESS} = $invoice_from;
397 my $header = new Mail::Header ( [
398 "From: $invoice_from",
399 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
400 "Sender: $invoice_from",
401 "Reply-To: $invoice_from",
402 "Date: ". time2str("%a, %d %b %Y %X %z", time),
405 my $message = new Mail::Internet (
407 'Body' => [ @print_text ], #( date)
410 $message->smtpsend( Host => $smtpmachine )
411 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
412 or return "(customer # ". $self->custnum. ") can't send invoice email".
413 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
414 " via server $smtpmachine with SMTP: $!";
418 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
420 or return "Can't open pipe to $lpr: $!";
421 print LPR @print_text;
423 or return $! ? "Error closing $lpr: $!"
424 : "Exit status $? from $lpr";
431 =item send_csv OPTIONS
433 Sends invoice as a CSV data-file to a remote host with the specified protocol.
437 protocol - currently only "ftp"
443 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
444 and YYMMDDHHMMSS is a timestamp.
446 The fields of the CSV file is as follows:
448 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
452 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
454 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
455 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
456 fields are filled in.
458 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
459 first two fields (B<record_type> and B<invnum>) and the last five fields
460 (B<pkg> through B<edate>) are filled in.
462 =item invnum - invoice number
464 =item custnum - customer number
466 =item _date - invoice date
468 =item charged - total invoice amount
470 =item first - customer first name
472 =item last - customer first name
474 =item company - company name
476 =item address1 - address line 1
478 =item address2 - address line 1
488 =item pkg - line item description
490 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
492 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
494 =item sdate - start date for recurring fee
496 =item edate - end date for recurring fee
503 my($self, %opt) = @_;
505 #part one: create file
507 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
508 mkdir $spooldir, 0700 unless -d $spooldir;
510 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
512 open(CSV, ">$file") or die "can't open $file: $!";
514 eval "use Text::CSV_XS";
517 my $csv = Text::CSV_XS->new({'always_quote'=>1});
519 my $cust_main = $self->cust_main;
525 time2str("%x", $self->_date),
526 sprintf("%.2f", $self->charged),
527 ( map { $cust_main->getfield($_) }
528 qw( first last company address1 address2 city state zip country ) ),
530 ) or die "can't create csv";
531 print CSV $csv->string. "\n";
533 #new charges (false laziness w/print_text)
534 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
536 my($pkg, $setup, $recur, $sdate, $edate);
537 if ( $cust_bill_pkg->pkgnum ) {
539 ($pkg, $setup, $recur, $sdate, $edate) = (
540 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
541 ( $cust_bill_pkg->setup != 0
542 ? sprintf("%.2f", $cust_bill_pkg->setup )
544 ( $cust_bill_pkg->recur != 0
545 ? sprintf("%.2f", $cust_bill_pkg->recur )
547 time2str("%x", $cust_bill_pkg->sdate),
548 time2str("%x", $cust_bill_pkg->edate),
552 next unless $cust_bill_pkg->setup != 0;
553 ($pkg, $setup, $recur, $sdate, $edate) =
554 ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
560 ( map { '' } (1..11) ),
561 ($pkg, $setup, $recur, $sdate, $edate)
562 ) or die "can't create csv";
563 print CSV $csv->string. "\n";
567 close CSV or die "can't close CSV: $!";
572 if ( $opt{protocol} eq 'ftp' ) {
573 eval "use Net::FTP;";
575 $net = Net::FTP->new($opt{server}) or die @$;
577 die "unknown protocol: $opt{protocol}";
580 $net->login( $opt{username}, $opt{password} )
581 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
583 $net->binary or die "can't set binary mode";
585 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
587 $net->put($file) or die "can't put $file: $!";
597 Pays this invoice with a compliemntary payment. If there is an error,
598 returns the error, otherwise returns false.
604 my $cust_pay = new FS::cust_pay ( {
605 'invnum' => $self->invnum,
606 'paid' => $self->owed,
609 'payinfo' => $self->cust_main->payinfo,
617 Attempts to pay this invoice with a credit card payment via a
618 Business::OnlinePayment realtime gateway. See
619 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
620 for supported processors.
639 Attempts to pay this invoice with an electronic check (ACH) payment via a
640 Business::OnlinePayment realtime gateway. See
641 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
642 for supported processors.
661 Attempts to pay this invoice with phone bill (LEC) payment via a
662 Business::OnlinePayment realtime gateway. See
663 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
664 for supported processors.
682 my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
683 my $cust_main = $self->cust_main;
684 my $amount = $self->owed;
686 my $address = $cust_main->address1;
687 $address .= ", ". $cust_main->address2 if $cust_main->address2;
689 my($payname, $payfirst, $paylast);
690 if ( $cust_main->payname && $method ne 'CHECK' ) {
691 $payname = $cust_main->payname;
692 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
694 #$dbh->rollback if $oldAutoCommit;
695 return "Illegal payname $payname";
697 ($payfirst, $paylast) = ($1, $2);
699 $payfirst = $cust_main->getfield('first');
700 $paylast = $cust_main->getfield('last');
701 $payname = "$payfirst $paylast";
704 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
705 if ( $conf->exists('emailinvoiceauto')
706 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
707 push @invoicing_list, $cust_main->all_emails;
709 my $email = $invoicing_list[0];
711 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
713 my $description = 'Internet Services';
714 if ( $conf->exists('business-onlinepayment-description') ) {
715 my $dtempl = $conf->config('business-onlinepayment-description');
717 my $agent_obj = $cust_main->agent
718 or die "can't retreive agent for $cust_main (agentnum ".
719 $cust_main->agentnum. ")";
720 my $agent = $agent_obj->agent;
721 my $pkgs = join(', ',
722 map { $_->cust_pkg->part_pkg->pkg }
723 grep { $_->pkgnum } $self->cust_bill_pkg
725 $description = eval qq("$dtempl");
730 if ( $method eq 'CC' ) {
731 $content{card_number} = $cust_main->payinfo;
732 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
733 $content{expiration} = "$2/$1";
734 } elsif ( $method eq 'CHECK' ) {
735 my($account_number,$routing_code) = $cust_main->payinfo;
736 ( $content{account_number}, $content{routing_code} ) =
737 split('@', $cust_main->payinfo);
738 $content{bank_name} = $cust_main->payname;
739 } elsif ( $method eq 'LEC' ) {
740 $content{phone} = $cust_main->payinfo;
744 new Business::OnlinePayment( $processor, @$options );
745 $transaction->content(
748 'password' => $password,
749 'action' => $action1,
750 'description' => $description,
752 'invoice_number' => $self->invnum,
753 'customer_id' => $self->custnum,
754 'last_name' => $paylast,
755 'first_name' => $payfirst,
757 'address' => $address,
758 'city' => $cust_main->city,
759 'state' => $cust_main->state,
760 'zip' => $cust_main->zip,
761 'country' => $cust_main->country,
762 'referer' => 'http://cleanwhisker.420.am/',
764 'phone' => $cust_main->daytime || $cust_main->night,
767 $transaction->submit();
769 if ( $transaction->is_success() && $action2 ) {
770 my $auth = $transaction->authorization;
771 my $ordernum = $transaction->can('order_number')
772 ? $transaction->order_number
775 #warn "********* $auth ***********\n";
776 #warn "********* $ordernum ***********\n";
778 new Business::OnlinePayment( $processor, @$options );
785 password => $password,
786 order_number => $ordernum,
788 authorization => $auth,
789 description => $description,
792 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
793 transaction_sequence_num local_transaction_date
794 local_transaction_time AVS_result_code )) {
795 $capture{$field} = $transaction->$field() if $transaction->can($field);
798 $capture->content( %capture );
802 unless ( $capture->is_success ) {
803 my $e = "Authorization sucessful but capture failed, invnum #".
804 $self->invnum. ': '. $capture->result_code.
805 ": ". $capture->error_message;
812 if ( $transaction->is_success() ) {
820 my $cust_pay = new FS::cust_pay ( {
821 'invnum' => $self->invnum,
824 'payby' => $method2payby{$method},
825 'payinfo' => $cust_main->payinfo,
826 'paybatch' => "$processor:". $transaction->authorization,
828 my $error = $cust_pay->insert;
830 # gah, even with transactions.
831 my $e = 'WARNING: Card/ACH debited but database not updated - '.
832 'error applying payment, invnum #' . $self->invnum.
833 " ($processor): $error";
839 #} elsif ( $options{'report_badcard'} ) {
842 my $perror = "$processor error, invnum #". $self->invnum. ': '.
843 $transaction->result_code. ": ". $transaction->error_message;
845 if ( $conf->exists('emaildecline')
846 && grep { $_ ne 'POST' } $cust_main->invoicing_list
848 my @templ = $conf->config('declinetemplate');
849 my $template = new Text::Template (
851 SOURCE => [ map "$_\n", @templ ],
852 ) or return "($perror) can't create template: $Text::Template::ERROR";
854 or return "($perror) can't compile template: $Text::Template::ERROR";
856 my $templ_hash = { error => $transaction->error_message };
858 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
859 $ENV{MAILADDRESS} = $invoice_from;
860 my $header = new Mail::Header ( [
861 "From: $invoice_from",
862 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
863 "Sender: $invoice_from",
864 "Reply-To: $invoice_from",
865 "Date: ". time2str("%a, %d %b %Y %X %z", time),
866 "Subject: Your payment could not be processed",
868 my $message = new Mail::Internet (
870 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
873 $message->smtpsend( Host => $smtpmachine )
874 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
875 or return "($perror) (customer # ". $self->custnum.
876 ") can't send card decline email to ".
877 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
878 " via server $smtpmachine with SMTP: $!";
886 =item realtime_card_cybercash
888 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
892 sub realtime_card_cybercash {
894 my $cust_main = $self->cust_main;
895 my $amount = $self->owed;
897 return "CyberCash CashRegister real-time card processing not enabled!"
898 unless $cybercash eq 'cybercash3.2';
900 my $address = $cust_main->address1;
901 $address .= ", ". $cust_main->address2 if $cust_main->address2;
904 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
905 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
910 my $paybatch = $self->invnum.
911 '-' . time2str("%y%m%d%H%M%S", time);
913 my $payname = $cust_main->payname ||
914 $cust_main->getfield('first').' '.$cust_main->getfield('last');
916 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
918 my @full_xaction = ( $xaction,
919 'Order-ID' => $paybatch,
920 'Amount' => "usd $amount",
921 'Card-Number' => $cust_main->getfield('payinfo'),
922 'Card-Name' => $payname,
923 'Card-Address' => $address,
924 'Card-City' => $cust_main->getfield('city'),
925 'Card-State' => $cust_main->getfield('state'),
926 'Card-Zip' => $cust_main->getfield('zip'),
927 'Card-Country' => $country,
932 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
934 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
935 my $cust_pay = new FS::cust_pay ( {
936 'invnum' => $self->invnum,
940 'payinfo' => $cust_main->payinfo,
941 'paybatch' => "$cybercash:$paybatch",
943 my $error = $cust_pay->insert;
945 # gah, even with transactions.
946 my $e = 'WARNING: Card debited but database not updated - '.
947 'error applying payment, invnum #' . $self->invnum.
948 " (CyberCash Order-ID $paybatch): $error";
954 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
955 # || $options{'report_badcard'}
958 return 'Cybercash error, invnum #' .
959 $self->invnum. ':'. $result{'MErrMsg'};
966 Adds a payment for this invoice to the pending credit card batch (see
967 L<FS::cust_pay_batch>).
973 my $cust_main = $self->cust_main;
975 my $cust_pay_batch = new FS::cust_pay_batch ( {
976 'invnum' => $self->getfield('invnum'),
977 'custnum' => $cust_main->getfield('custnum'),
978 'last' => $cust_main->getfield('last'),
979 'first' => $cust_main->getfield('first'),
980 'address1' => $cust_main->getfield('address1'),
981 'address2' => $cust_main->getfield('address2'),
982 'city' => $cust_main->getfield('city'),
983 'state' => $cust_main->getfield('state'),
984 'zip' => $cust_main->getfield('zip'),
985 'country' => $cust_main->getfield('country'),
987 'cardnum' => $cust_main->getfield('payinfo'),
988 'exp' => $cust_main->getfield('paydate'),
989 'payname' => $cust_main->getfield('payname'),
990 'amount' => $self->owed,
992 my $error = $cust_pay_batch->insert;
993 die $error if $error;
998 =item print_text [TIME];
1000 Returns an text invoice, as a list of lines.
1002 TIME an optional value used to control the printing of overdue messages. The
1003 default is now. It isn't the date of the invoice; that's the `_date' field.
1004 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
1005 L<Time::Local> and L<Date::Parse> for conversion functions.
1011 my( $self, $today, $template ) = @_;
1013 # my $invnum = $self->invnum;
1014 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
1015 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1016 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1018 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1019 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1020 #my $balance_due = $self->owed + $pr_total - $cr_total;
1021 my $balance_due = $self->owed + $pr_total;
1024 #my($description,$amount);
1028 foreach ( @pr_cust_bill ) {
1030 "Previous Balance, Invoice #". $_->invnum.
1031 " (". time2str("%x",$_->_date). ")",
1032 $money_char. sprintf("%10.2f",$_->owed)
1035 if (@pr_cust_bill) {
1036 push @buf,['','-----------'];
1037 push @buf,[ 'Total Previous Balance',
1038 $money_char. sprintf("%10.2f",$pr_total ) ];
1043 foreach ( $self->cust_bill_pkg ) {
1047 my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
1048 my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
1049 my($pkg)=$part_pkg->pkg;
1051 if ( $_->setup != 0 ) {
1052 push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
1054 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1057 if ( $_->recur != 0 ) {
1059 "$pkg (" . time2str("%x",$_->sdate) . " - " .
1060 time2str("%x",$_->edate) . ")",
1061 $money_char. sprintf("%10.2f",$_->recur)
1064 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1067 } else { #pkgnum Tax
1068 push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ]
1073 push @buf,['','-----------'];
1074 push @buf,['Total New Charges',
1075 $money_char. sprintf("%10.2f",$self->charged) ];
1078 push @buf,['','-----------'];
1079 push @buf,['Total Charges',
1080 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1084 foreach ( $self->cust_credited ) {
1086 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1088 my $reason = substr($_->cust_credit->reason,0,32);
1089 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1090 $reason = " ($reason) " if $reason;
1092 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1094 $money_char. sprintf("%10.2f",$_->amount)
1097 #foreach ( @cr_cust_credit ) {
1099 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1100 # $money_char. sprintf("%10.2f",$_->credited)
1104 #get & print payments
1105 foreach ( $self->cust_bill_pay ) {
1107 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1110 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1111 $money_char. sprintf("%10.2f",$_->amount )
1116 push @buf,['','-----------'];
1117 push @buf,['Balance Due', $money_char.
1118 sprintf("%10.2f", $balance_due ) ];
1120 #create the template
1121 my $templatefile = 'invoice_template';
1122 $templatefile .= "_$template" if $template;
1123 my @invoice_template = $conf->config($templatefile)
1124 or die "cannot load config file $templatefile";
1127 foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
1128 /invoice_lines\((\d+)\)/;
1129 $invoice_lines += $1;
1132 die "no invoice_lines() functions in template?" unless $wasfunc;
1133 my $invoice_template = new Text::Template (
1135 SOURCE => [ map "$_\n", @invoice_template ],
1136 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1137 $invoice_template->compile()
1138 or die "can't compile template: $Text::Template::ERROR";
1140 #setup template variables
1141 package FS::cust_bill::_template; #!
1142 use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
1144 $invnum = $self->invnum;
1145 $date = $self->_date;
1148 if ( $FS::cust_bill::invoice_lines ) {
1150 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1152 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1157 #format address (variable for the template)
1159 @address = ( '', '', '', '', '', '' );
1160 package FS::cust_bill; #!
1161 $FS::cust_bill::_template::address[$l++] =
1162 $cust_main->payname.
1163 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1164 ? " (P.O. #". $cust_main->payinfo. ")"
1168 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1169 if $cust_main->company;
1170 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1171 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1172 if $cust_main->address2;
1173 $FS::cust_bill::_template::address[$l++] =
1174 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1175 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1176 unless $cust_main->country eq 'US';
1178 # #overdue? (variable for the template)
1179 # $FS::cust_bill::_template::overdue = (
1181 # && $today > $self->_date
1182 ## && $self->printed > 1
1183 # && $self->printed > 0
1186 #and subroutine for the template
1188 sub FS::cust_bill::_template::invoice_lines {
1189 my $lines = shift or return @buf;
1191 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.10 2002-11-19 10:09:44 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