4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $lpr $invoice_from $smtpmachine );
6 use vars qw( $xaction $E_NoErr );
7 use vars qw( $bop_processor $bop_login $bop_password $bop_action @bop_options );
8 use vars qw( $ach_processor $ach_login $ach_password $ach_action @ach_options );
9 use vars qw( $invoice_lines @buf ); #yuck
10 use vars qw( $quiet );
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('business-onlinepayment') ) {
45 ) = $conf->config('business-onlinepayment');
46 $bop_action ||= 'normal authorization';
47 ( $ach_processor, $ach_login, $ach_password, $ach_action, @ach_options ) =
48 ( $bop_processor, $bop_login, $bop_password, $bop_action, @bop_options );
49 eval "use Business::OnlinePayment";
52 if ( $conf->exists('business-onlinepayment-ach') ) {
58 ) = $conf->config('business-onlinepayment-ach');
59 $ach_action ||= 'normal authorization';
60 eval "use Business::OnlinePayment";
67 FS::cust_bill - Object methods for cust_bill records
73 $record = new FS::cust_bill \%hash;
74 $record = new FS::cust_bill { 'column' => 'value' };
76 $error = $record->insert;
78 $error = $new_record->replace($old_record);
80 $error = $record->delete;
82 $error = $record->check;
84 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
86 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
88 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
90 @cust_pay_objects = $cust_bill->cust_pay;
92 $tax_amount = $record->tax;
94 @lines = $cust_bill->print_text;
95 @lines = $cust_bill->print_text $time;
99 An FS::cust_bill object represents an invoice; a declaration that a customer
100 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
101 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
102 following fields are currently supported:
106 =item invnum - primary key (assigned automatically for new invoices)
108 =item custnum - customer (see L<FS::cust_main>)
110 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
111 L<Time::Local> and L<Date::Parse> for conversion functions.
113 =item charged - amount of this invoice
115 =item printed - deprecated
117 =item closed - books closed flag, empty or `Y'
127 Creates a new invoice. To add the invoice to the database, see L<"insert">.
128 Invoices are normally created by calling the bill method of a customer object
129 (see L<FS::cust_main>).
133 sub table { 'cust_bill'; }
137 Adds this invoice to the database ("Posts" the invoice). If there is an error,
138 returns the error, otherwise returns false.
142 Currently unimplemented. I don't remove invoices because there would then be
143 no record you ever posted this invoice (which is bad, no?)
149 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
150 $self->SUPER::delete(@_);
153 =item replace OLD_RECORD
155 Replaces the OLD_RECORD with this one in the database. If there is an error,
156 returns the error, otherwise returns false.
158 Only printed may be changed. printed is normally updated by calling the
159 collect method of a customer object (see L<FS::cust_main>).
164 my( $new, $old ) = ( shift, shift );
165 return "Can't change custnum!" unless $old->custnum == $new->custnum;
166 #return "Can't change _date!" unless $old->_date eq $new->_date;
167 return "Can't change _date!" unless $old->_date == $new->_date;
168 return "Can't change charged!" unless $old->charged == $new->charged;
170 $new->SUPER::replace($old);
175 Checks all fields to make sure this is a valid invoice. If there is an error,
176 returns the error, otherwise returns false. Called by the insert and replace
185 $self->ut_numbern('invnum')
186 || $self->ut_number('custnum')
187 || $self->ut_numbern('_date')
188 || $self->ut_money('charged')
189 || $self->ut_numbern('printed')
190 || $self->ut_enum('closed', [ '', 'Y' ])
192 return $error if $error;
194 return "Unknown customer"
195 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
197 $self->_date(time) unless $self->_date;
199 $self->printed(0) if $self->printed eq '';
206 Returns a list consisting of the total previous balance for this customer,
207 followed by the previous outstanding invoices (as FS::cust_bill objects also).
214 my @cust_bill = sort { $a->_date <=> $b->_date }
215 grep { $_->owed != 0 && $_->_date < $self->_date }
216 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
218 foreach ( @cust_bill ) { $total += $_->owed; }
224 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
230 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
233 =item cust_bill_event
235 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
240 sub cust_bill_event {
242 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
248 Returns the customer (see L<FS::cust_main>) for this invoice.
254 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
259 Depreciated. See the cust_credited method.
261 #Returns a list consisting of the total previous credited (see
262 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
263 #outstanding credits (FS::cust_credit objects).
269 croak "FS::cust_bill->cust_credit depreciated; see ".
270 "FS::cust_bill->cust_credit_bill";
273 #my @cust_credit = sort { $a->_date <=> $b->_date }
274 # grep { $_->credited != 0 && $_->_date < $self->_date }
275 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
277 #foreach (@cust_credit) { $total += $_->credited; }
278 #$total, @cust_credit;
283 Depreciated. See the cust_bill_pay method.
285 #Returns all payments (see L<FS::cust_pay>) for this invoice.
291 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
293 #sort { $a->_date <=> $b->_date }
294 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
300 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
306 sort { $a->_date <=> $b->_date }
307 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
312 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
318 sort { $a->_date <=> $b->_date }
319 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
325 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
332 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
334 foreach (@taxlines) { $total += $_->setup; }
340 Returns the amount owed (still outstanding) on this invoice, which is charged
341 minus all payment applications (see L<FS::cust_bill_pay>) and credit
342 applications (see L<FS::cust_credit_bill>).
348 my $balance = $self->charged;
349 $balance -= $_->amount foreach ( $self->cust_bill_pay );
350 $balance -= $_->amount foreach ( $self->cust_credited );
351 $balance = sprintf( "%.2f", $balance);
352 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
358 Sends this invoice to the destinations configured for this customer: send
359 emails or print. See L<FS::cust_main_invoice>.
364 my($self,$template) = @_;
365 my @print_text = $self->print_text('', $template);
366 my @invoicing_list = $self->cust_main->invoicing_list;
368 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
370 #better to notify this person than silence
371 @invoicing_list = ($invoice_from) unless @invoicing_list;
373 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
374 #$ENV{SMTPHOSTS} = $smtpmachine;
375 $ENV{MAILADDRESS} = $invoice_from;
376 my $header = new Mail::Header ( [
377 "From: $invoice_from",
378 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
379 "Sender: $invoice_from",
380 "Reply-To: $invoice_from",
381 "Date: ". time2str("%a, %d %b %Y %X %z", time),
384 my $message = new Mail::Internet (
386 'Body' => [ @print_text ], #( date)
389 $message->smtpsend( Host => $smtpmachine )
390 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
391 or return "(customer # ". $self->custnum. ") can't send invoice email".
392 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
393 " via server $smtpmachine with SMTP: $!";
397 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
399 or return "Can't open pipe to $lpr: $!";
400 print LPR @print_text;
402 or return $! ? "Error closing $lpr: $!"
403 : "Exit status $? from $lpr";
410 =item send_csv OPTIONS
412 Sends invoice as a CSV data-file to a remote host with the specified protocol.
416 protocol - currently only "ftp"
422 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
423 and YYMMDDHHMMSS is a timestamp.
425 The fields of the CSV file is as follows:
427 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
431 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
433 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
434 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
435 fields are filled in.
437 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
438 first two fields (B<record_type> and B<invnum>) and the last five fields
439 (B<pkg> through B<edate>) are filled in.
441 =item invnum - invoice number
443 =item custnum - customer number
445 =item _date - invoice date
447 =item charged - total invoice amount
449 =item first - customer first name
451 =item last - customer first name
453 =item company - company name
455 =item address1 - address line 1
457 =item address2 - address line 1
467 =item pkg - line item description
469 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
471 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
473 =item sdate - start date for recurring fee
475 =item edate - end date for recurring fee
482 my($self, %opt) = @_;
484 #part one: create file
486 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
487 mkdir $spooldir, 0700 unless -d $spooldir;
489 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
491 open(CSV, ">$file") or die "can't open $file: $!";
493 eval "use Text::CSV_XS";
496 my $csv = Text::CSV_XS->new({'always_quote'=>1});
498 my $cust_main = $self->cust_main;
504 time2str("%x", $self->_date),
505 sprintf("%.2f", $self->charged),
506 ( map { $cust_main->getfield($_) }
507 qw( first last company address1 address2 city state zip country ) ),
509 ) or die "can't create csv";
510 print CSV $csv->string. "\n";
512 #new charges (false laziness w/print_text)
513 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
515 my($pkg, $setup, $recur, $sdate, $edate);
516 if ( $cust_bill_pkg->pkgnum ) {
518 ($pkg, $setup, $recur, $sdate, $edate) = (
519 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
520 ( $cust_bill_pkg->setup != 0
521 ? sprintf("%.2f", $cust_bill_pkg->setup )
523 ( $cust_bill_pkg->recur != 0
524 ? sprintf("%.2f", $cust_bill_pkg->recur )
526 time2str("%x", $cust_bill_pkg->sdate),
527 time2str("%x", $cust_bill_pkg->edate),
531 next unless $cust_bill_pkg->setup != 0;
532 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
533 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
535 ($pkg, $setup, $recur, $sdate, $edate) =
536 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
542 ( map { '' } (1..11) ),
543 ($pkg, $setup, $recur, $sdate, $edate)
544 ) or die "can't create csv";
545 print CSV $csv->string. "\n";
549 close CSV or die "can't close CSV: $!";
554 if ( $opt{protocol} eq 'ftp' ) {
555 eval "use Net::FTP;";
557 $net = Net::FTP->new($opt{server}) or die @$;
559 die "unknown protocol: $opt{protocol}";
562 $net->login( $opt{username}, $opt{password} )
563 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
565 $net->binary or die "can't set binary mode";
567 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
569 $net->put($file) or die "can't put $file: $!";
579 Pays this invoice with a compliemntary payment. If there is an error,
580 returns the error, otherwise returns false.
586 my $cust_pay = new FS::cust_pay ( {
587 'invnum' => $self->invnum,
588 'paid' => $self->owed,
591 'payinfo' => $self->cust_main->payinfo,
599 Attempts to pay this invoice with a credit card payment via a
600 Business::OnlinePayment realtime gateway. See
601 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
602 for supported processors.
621 Attempts to pay this invoice with an electronic check (ACH) payment via a
622 Business::OnlinePayment realtime gateway. See
623 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
624 for supported processors.
643 Attempts to pay this invoice with phone bill (LEC) payment via a
644 Business::OnlinePayment realtime gateway. See
645 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
646 for supported processors.
664 my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
665 my $cust_main = $self->cust_main;
666 my $amount = $self->owed;
668 my $address = $cust_main->address1;
669 $address .= ", ". $cust_main->address2 if $cust_main->address2;
671 my($payname, $payfirst, $paylast);
672 if ( $cust_main->payname && $method ne 'ECHECK' ) {
673 $payname = $cust_main->payname;
674 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
676 #$dbh->rollback if $oldAutoCommit;
677 return "Illegal payname $payname";
679 ($payfirst, $paylast) = ($1, $2);
681 $payfirst = $cust_main->getfield('first');
682 $paylast = $cust_main->getfield('last');
683 $payname = "$payfirst $paylast";
686 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
687 if ( $conf->exists('emailinvoiceauto')
688 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
689 push @invoicing_list, $cust_main->all_emails;
691 my $email = $invoicing_list[0];
693 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
695 my $description = 'Internet Services';
696 if ( $conf->exists('business-onlinepayment-description') ) {
697 my $dtempl = $conf->config('business-onlinepayment-description');
699 my $agent_obj = $cust_main->agent
700 or die "can't retreive agent for $cust_main (agentnum ".
701 $cust_main->agentnum. ")";
702 my $agent = $agent_obj->agent;
703 my $pkgs = join(', ',
704 map { $_->cust_pkg->part_pkg->pkg }
705 grep { $_->pkgnum } $self->cust_bill_pkg
707 $description = eval qq("$dtempl");
712 if ( $method eq 'CC' ) {
713 $content{card_number} = $cust_main->payinfo;
714 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
715 $content{expiration} = "$2/$1";
716 } elsif ( $method eq 'ECHECK' ) {
717 my($account_number,$routing_code) = $cust_main->payinfo;
718 ( $content{account_number}, $content{routing_code} ) =
719 split('@', $cust_main->payinfo);
720 $content{bank_name} = $cust_main->payname;
721 } elsif ( $method eq 'LEC' ) {
722 $content{phone} = $cust_main->payinfo;
726 new Business::OnlinePayment( $processor, @$options );
727 $transaction->content(
730 'password' => $password,
731 'action' => $action1,
732 'description' => $description,
734 'invoice_number' => $self->invnum,
735 'customer_id' => $self->custnum,
736 'last_name' => $paylast,
737 'first_name' => $payfirst,
739 'address' => $address,
740 'city' => $cust_main->city,
741 'state' => $cust_main->state,
742 'zip' => $cust_main->zip,
743 'country' => $cust_main->country,
744 'referer' => 'http://cleanwhisker.420.am/',
746 'phone' => $cust_main->daytime || $cust_main->night,
749 $transaction->submit();
751 if ( $transaction->is_success() && $action2 ) {
752 my $auth = $transaction->authorization;
753 my $ordernum = $transaction->can('order_number')
754 ? $transaction->order_number
757 #warn "********* $auth ***********\n";
758 #warn "********* $ordernum ***********\n";
760 new Business::OnlinePayment( $processor, @$options );
767 password => $password,
768 order_number => $ordernum,
770 authorization => $auth,
771 description => $description,
774 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
775 transaction_sequence_num local_transaction_date
776 local_transaction_time AVS_result_code )) {
777 $capture{$field} = $transaction->$field() if $transaction->can($field);
780 $capture->content( %capture );
784 unless ( $capture->is_success ) {
785 my $e = "Authorization sucessful but capture failed, invnum #".
786 $self->invnum. ': '. $capture->result_code.
787 ": ". $capture->error_message;
794 if ( $transaction->is_success() ) {
802 my $cust_pay = new FS::cust_pay ( {
803 'invnum' => $self->invnum,
806 'payby' => method2payby{$method},
807 'payinfo' => $cust_main->payinfo,
808 'paybatch' => "$processor:". $transaction->authorization,
810 my $error = $cust_pay->insert;
812 # gah, even with transactions.
813 my $e = 'WARNING: Card/ACH debited but database not updated - '.
814 'error applying payment, invnum #' . $self->invnum.
815 " ($processor): $error";
821 #} elsif ( $options{'report_badcard'} ) {
824 my $perror = "$processor error, invnum #". $self->invnum. ': '.
825 $transaction->result_code. ": ". $transaction->error_message;
827 if ( !$quiet && $conf->exists('emaildecline')
828 && grep { $_ ne 'POST' } $cust_main->invoicing_list
830 my @templ = $conf->config('declinetemplate');
831 my $template = new Text::Template (
833 SOURCE => [ map "$_\n", @templ ],
834 ) or return "($perror) can't create template: $Text::Template::ERROR";
836 or return "($perror) can't compile template: $Text::Template::ERROR";
838 my $templ_hash = { error => $transaction->error_message };
840 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
841 $ENV{MAILADDRESS} = $invoice_from;
842 my $header = new Mail::Header ( [
843 "From: $invoice_from",
844 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
845 "Sender: $invoice_from",
846 "Reply-To: $invoice_from",
847 "Date: ". time2str("%a, %d %b %Y %X %z", time),
848 "Subject: Your payment could not be processed",
850 my $message = new Mail::Internet (
852 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
855 $message->smtpsend( Host => $smtpmachine )
856 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
857 or return "($perror) (customer # ". $self->custnum.
858 ") can't send card decline email to ".
859 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
860 " via server $smtpmachine with SMTP: $!";
870 Adds a payment for this invoice to the pending credit card batch (see
871 L<FS::cust_pay_batch>).
877 my $cust_main = $self->cust_main;
879 my $cust_pay_batch = new FS::cust_pay_batch ( {
880 'invnum' => $self->getfield('invnum'),
881 'custnum' => $cust_main->getfield('custnum'),
882 'last' => $cust_main->getfield('last'),
883 'first' => $cust_main->getfield('first'),
884 'address1' => $cust_main->getfield('address1'),
885 'address2' => $cust_main->getfield('address2'),
886 'city' => $cust_main->getfield('city'),
887 'state' => $cust_main->getfield('state'),
888 'zip' => $cust_main->getfield('zip'),
889 'country' => $cust_main->getfield('country'),
891 'cardnum' => $cust_main->getfield('payinfo'),
892 'exp' => $cust_main->getfield('paydate'),
893 'payname' => $cust_main->getfield('payname'),
894 'amount' => $self->owed,
896 my $error = $cust_pay_batch->insert;
897 die $error if $error;
902 =item print_text [TIME];
904 Returns an text invoice, as a list of lines.
906 TIME an optional value used to control the printing of overdue messages. The
907 default is now. It isn't the date of the invoice; that's the `_date' field.
908 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
909 L<Time::Local> and L<Date::Parse> for conversion functions.
915 my( $self, $today, $template ) = @_;
917 # my $invnum = $self->invnum;
918 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
919 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
920 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
922 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
923 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
924 #my $balance_due = $self->owed + $pr_total - $cr_total;
925 my $balance_due = $self->owed + $pr_total;
928 #my($description,$amount);
932 foreach ( @pr_cust_bill ) {
934 "Previous Balance, Invoice #". $_->invnum.
935 " (". time2str("%x",$_->_date). ")",
936 $money_char. sprintf("%10.2f",$_->owed)
940 push @buf,['','-----------'];
941 push @buf,[ 'Total Previous Balance',
942 $money_char. sprintf("%10.2f",$pr_total ) ];
947 foreach ( ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
948 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
953 my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
954 my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
955 my($pkg)=$part_pkg->pkg;
957 if ( $_->setup != 0 ) {
958 push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
960 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
963 if ( $_->recur != 0 ) {
965 "$pkg (" . time2str("%x",$_->sdate) . " - " .
966 time2str("%x",$_->edate) . ")",
967 $money_char. sprintf("%10.2f",$_->recur)
970 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
974 my $itemdesc = defined $_->dbdef_table->column('itemdesc')
975 ? ( $_->itemdesc || 'Tax' )
977 push @buf,[$itemdesc, $money_char. sprintf("%10.2f",$_->setup) ]
982 push @buf,['','-----------'];
983 push @buf,['Total New Charges',
984 $money_char. sprintf("%10.2f",$self->charged) ];
987 push @buf,['','-----------'];
988 push @buf,['Total Charges',
989 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
993 foreach ( $self->cust_credited ) {
995 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
997 my $reason = substr($_->cust_credit->reason,0,32);
998 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
999 $reason = " ($reason) " if $reason;
1001 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1003 $money_char. sprintf("%10.2f",$_->amount)
1006 #foreach ( @cr_cust_credit ) {
1008 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1009 # $money_char. sprintf("%10.2f",$_->credited)
1013 #get & print payments
1014 foreach ( $self->cust_bill_pay ) {
1016 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1019 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1020 $money_char. sprintf("%10.2f",$_->amount )
1025 push @buf,['','-----------'];
1026 push @buf,['Balance Due', $money_char.
1027 sprintf("%10.2f", $balance_due ) ];
1029 #create the template
1030 my $templatefile = 'invoice_template';
1031 $templatefile .= "_$template" if $template;
1032 my @invoice_template = $conf->config($templatefile)
1033 or die "cannot load config file $templatefile";
1036 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1037 /invoice_lines\((\d*)\)/;
1038 $invoice_lines += $1 || scalar(@buf);
1041 die "no invoice_lines() functions in template?" unless $wasfunc;
1042 my $invoice_template = new Text::Template (
1044 SOURCE => [ map "$_\n", @invoice_template ],
1045 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1046 $invoice_template->compile()
1047 or die "can't compile template: $Text::Template::ERROR";
1049 #setup template variables
1050 package FS::cust_bill::_template; #!
1051 use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
1053 $invnum = $self->invnum;
1054 $date = $self->_date;
1057 if ( $FS::cust_bill::invoice_lines ) {
1059 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1061 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1066 #format address (variable for the template)
1068 @address = ( '', '', '', '', '', '' );
1069 package FS::cust_bill; #!
1070 $FS::cust_bill::_template::address[$l++] =
1071 $cust_main->payname.
1072 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1073 ? " (P.O. #". $cust_main->payinfo. ")"
1077 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1078 if $cust_main->company;
1079 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1080 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1081 if $cust_main->address2;
1082 $FS::cust_bill::_template::address[$l++] =
1083 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1084 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1085 unless $cust_main->country eq 'US';
1087 # #overdue? (variable for the template)
1088 # $FS::cust_bill::_template::overdue = (
1090 # && $today > $self->_date
1091 ## && $self->printed > 1
1092 # && $self->printed > 0
1095 #and subroutine for the template
1096 sub FS::cust_bill::_template::invoice_lines {
1097 my $lines = shift || scalar(@buf);
1099 scalar(@buf) ? shift @buf : [ '', '' ];
1105 $FS::cust_bill::_template::page = 1;
1109 push @collect, split("\n",
1110 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1112 $FS::cust_bill::_template::page++;
1115 map "$_\n", @collect;
1123 $Id: cust_bill.pm,v 1.58 2002-12-23 14:22:48 steve Exp $
1129 print_text formatting (and some logic :/) is in source, but needs to be
1130 slurped in from a file. Also number of lines ($=).
1132 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1133 or something similar so the look can be completely customized?)
1137 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1138 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base