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
11 use Mail::Internet 1.44;
14 use FS::UID qw( datasrc );
15 use FS::Record qw( qsearch qsearchs );
17 use FS::cust_bill_pkg;
21 use FS::cust_credit_bill;
22 use FS::cust_pay_batch;
23 use FS::cust_bill_event;
25 @ISA = qw( FS::Record );
27 #ask FS::UID to run this stuff for us later
28 $FS::UID::callback{'FS::cust_bill'} = sub {
32 $money_char = $conf->config('money_char') || '$';
34 $lpr = $conf->config('lpr');
35 $invoice_from = $conf->config('invoice_from');
36 $smtpmachine = $conf->config('smtpmachine');
38 if ( $conf->exists('business-onlinepayment') ) {
44 ) = $conf->config('business-onlinepayment');
45 $bop_action ||= 'normal authorization';
46 ( $ach_processor, $ach_login, $ach_password, $ach_action, @ach_options ) =
47 ( $bop_processor, $bop_login, $bop_password, $bop_action, @bop_options );
48 eval "use Business::OnlinePayment";
51 if ( $conf->exists('business-onlinepayment-ach') ) {
57 ) = $conf->config('business-onlinepayment-ach');
58 $ach_action ||= 'normal authorization';
59 eval "use Business::OnlinePayment";
66 FS::cust_bill - Object methods for cust_bill records
72 $record = new FS::cust_bill \%hash;
73 $record = new FS::cust_bill { 'column' => 'value' };
75 $error = $record->insert;
77 $error = $new_record->replace($old_record);
79 $error = $record->delete;
81 $error = $record->check;
83 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
85 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
87 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
89 @cust_pay_objects = $cust_bill->cust_pay;
91 $tax_amount = $record->tax;
93 @lines = $cust_bill->print_text;
94 @lines = $cust_bill->print_text $time;
98 An FS::cust_bill object represents an invoice; a declaration that a customer
99 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
100 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
101 following fields are currently supported:
105 =item invnum - primary key (assigned automatically for new invoices)
107 =item custnum - customer (see L<FS::cust_main>)
109 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
110 L<Time::Local> and L<Date::Parse> for conversion functions.
112 =item charged - amount of this invoice
114 =item printed - deprecated
116 =item closed - books closed flag, empty or `Y'
126 Creates a new invoice. To add the invoice to the database, see L<"insert">.
127 Invoices are normally created by calling the bill method of a customer object
128 (see L<FS::cust_main>).
132 sub table { 'cust_bill'; }
136 Adds this invoice to the database ("Posts" the invoice). If there is an error,
137 returns the error, otherwise returns false.
141 Currently unimplemented. I don't remove invoices because there would then be
142 no record you ever posted this invoice (which is bad, no?)
148 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
149 $self->SUPER::delete(@_);
152 =item replace OLD_RECORD
154 Replaces the OLD_RECORD with this one in the database. If there is an error,
155 returns the error, otherwise returns false.
157 Only printed may be changed. printed is normally updated by calling the
158 collect method of a customer object (see L<FS::cust_main>).
163 my( $new, $old ) = ( shift, shift );
164 return "Can't change custnum!" unless $old->custnum == $new->custnum;
165 #return "Can't change _date!" unless $old->_date eq $new->_date;
166 return "Can't change _date!" unless $old->_date == $new->_date;
167 return "Can't change charged!" unless $old->charged == $new->charged;
169 $new->SUPER::replace($old);
174 Checks all fields to make sure this is a valid invoice. If there is an error,
175 returns the error, otherwise returns false. Called by the insert and replace
184 $self->ut_numbern('invnum')
185 || $self->ut_number('custnum')
186 || $self->ut_numbern('_date')
187 || $self->ut_money('charged')
188 || $self->ut_numbern('printed')
189 || $self->ut_enum('closed', [ '', 'Y' ])
191 return $error if $error;
193 return "Unknown customer"
194 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
196 $self->_date(time) unless $self->_date;
198 $self->printed(0) if $self->printed eq '';
205 Returns a list consisting of the total previous balance for this customer,
206 followed by the previous outstanding invoices (as FS::cust_bill objects also).
213 my @cust_bill = sort { $a->_date <=> $b->_date }
214 grep { $_->owed != 0 && $_->_date < $self->_date }
215 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
217 foreach ( @cust_bill ) { $total += $_->owed; }
223 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
229 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
232 =item cust_bill_event
234 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
239 sub cust_bill_event {
241 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
247 Returns the customer (see L<FS::cust_main>) for this invoice.
253 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
258 Depreciated. See the cust_credited method.
260 #Returns a list consisting of the total previous credited (see
261 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
262 #outstanding credits (FS::cust_credit objects).
268 croak "FS::cust_bill->cust_credit depreciated; see ".
269 "FS::cust_bill->cust_credit_bill";
272 #my @cust_credit = sort { $a->_date <=> $b->_date }
273 # grep { $_->credited != 0 && $_->_date < $self->_date }
274 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
276 #foreach (@cust_credit) { $total += $_->credited; }
277 #$total, @cust_credit;
282 Depreciated. See the cust_bill_pay method.
284 #Returns all payments (see L<FS::cust_pay>) for this invoice.
290 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
292 #sort { $a->_date <=> $b->_date }
293 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
299 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
305 sort { $a->_date <=> $b->_date }
306 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
311 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
317 sort { $a->_date <=> $b->_date }
318 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
324 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
331 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
333 foreach (@taxlines) { $total += $_->setup; }
339 Returns the amount owed (still outstanding) on this invoice, which is charged
340 minus all payment applications (see L<FS::cust_bill_pay>) and credit
341 applications (see L<FS::cust_credit_bill>).
347 my $balance = $self->charged;
348 $balance -= $_->amount foreach ( $self->cust_bill_pay );
349 $balance -= $_->amount foreach ( $self->cust_credited );
350 $balance = sprintf( "%.2f", $balance);
351 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
357 Sends this invoice to the destinations configured for this customer: send
358 emails or print. See L<FS::cust_main_invoice>.
363 my($self,$template) = @_;
364 my @print_text = $self->print_text('', $template);
365 my @invoicing_list = $self->cust_main->invoicing_list;
367 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
369 #better to notify this person than silence
370 @invoicing_list = ($invoice_from) unless @invoicing_list;
372 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
373 #$ENV{SMTPHOSTS} = $smtpmachine;
374 $ENV{MAILADDRESS} = $invoice_from;
375 my $header = new Mail::Header ( [
376 "From: $invoice_from",
377 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
378 "Sender: $invoice_from",
379 "Reply-To: $invoice_from",
380 "Date: ". time2str("%a, %d %b %Y %X %z", time),
383 my $message = new Mail::Internet (
385 'Body' => [ @print_text ], #( date)
388 $message->smtpsend( Host => $smtpmachine )
389 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
390 or return "(customer # ". $self->custnum. ") can't send invoice email".
391 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
392 " via server $smtpmachine with SMTP: $!";
396 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
398 or return "Can't open pipe to $lpr: $!";
399 print LPR @print_text;
401 or return $! ? "Error closing $lpr: $!"
402 : "Exit status $? from $lpr";
409 =item send_csv OPTIONS
411 Sends invoice as a CSV data-file to a remote host with the specified protocol.
415 protocol - currently only "ftp"
421 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
422 and YYMMDDHHMMSS is a timestamp.
424 The fields of the CSV file is as follows:
426 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
430 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
432 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
433 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
434 fields are filled in.
436 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
437 first two fields (B<record_type> and B<invnum>) and the last five fields
438 (B<pkg> through B<edate>) are filled in.
440 =item invnum - invoice number
442 =item custnum - customer number
444 =item _date - invoice date
446 =item charged - total invoice amount
448 =item first - customer first name
450 =item last - customer first name
452 =item company - company name
454 =item address1 - address line 1
456 =item address2 - address line 1
466 =item pkg - line item description
468 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
470 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
472 =item sdate - start date for recurring fee
474 =item edate - end date for recurring fee
481 my($self, %opt) = @_;
483 #part one: create file
485 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
486 mkdir $spooldir, 0700 unless -d $spooldir;
488 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
490 open(CSV, ">$file") or die "can't open $file: $!";
492 eval "use Text::CSV_XS";
495 my $csv = Text::CSV_XS->new({'always_quote'=>1});
497 my $cust_main = $self->cust_main;
503 time2str("%x", $self->_date),
504 sprintf("%.2f", $self->charged),
505 ( map { $cust_main->getfield($_) }
506 qw( first last company address1 address2 city state zip country ) ),
508 ) or die "can't create csv";
509 print CSV $csv->string. "\n";
511 #new charges (false laziness w/print_text)
512 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
514 my($pkg, $setup, $recur, $sdate, $edate);
515 if ( $cust_bill_pkg->pkgnum ) {
517 ($pkg, $setup, $recur, $sdate, $edate) = (
518 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
519 ( $cust_bill_pkg->setup != 0
520 ? sprintf("%.2f", $cust_bill_pkg->setup )
522 ( $cust_bill_pkg->recur != 0
523 ? sprintf("%.2f", $cust_bill_pkg->recur )
525 time2str("%x", $cust_bill_pkg->sdate),
526 time2str("%x", $cust_bill_pkg->edate),
530 next unless $cust_bill_pkg->setup != 0;
531 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
532 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
534 ($pkg, $setup, $recur, $sdate, $edate) =
535 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
541 ( map { '' } (1..11) ),
542 ($pkg, $setup, $recur, $sdate, $edate)
543 ) or die "can't create csv";
544 print CSV $csv->string. "\n";
548 close CSV or die "can't close CSV: $!";
553 if ( $opt{protocol} eq 'ftp' ) {
554 eval "use Net::FTP;";
556 $net = Net::FTP->new($opt{server}) or die @$;
558 die "unknown protocol: $opt{protocol}";
561 $net->login( $opt{username}, $opt{password} )
562 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
564 $net->binary or die "can't set binary mode";
566 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
568 $net->put($file) or die "can't put $file: $!";
578 Pays this invoice with a compliemntary payment. If there is an error,
579 returns the error, otherwise returns false.
585 my $cust_pay = new FS::cust_pay ( {
586 'invnum' => $self->invnum,
587 'paid' => $self->owed,
590 'payinfo' => $self->cust_main->payinfo,
598 Attempts to pay this invoice with a credit card payment via a
599 Business::OnlinePayment realtime gateway. See
600 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
601 for supported processors.
620 Attempts to pay this invoice with an electronic check (ACH) payment via a
621 Business::OnlinePayment realtime gateway. See
622 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
623 for supported processors.
642 Attempts to pay this invoice with phone bill (LEC) payment via a
643 Business::OnlinePayment realtime gateway. See
644 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
645 for supported processors.
663 my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
664 my $cust_main = $self->cust_main;
665 my $amount = $self->owed;
667 my $address = $cust_main->address1;
668 $address .= ", ". $cust_main->address2 if $cust_main->address2;
670 my($payname, $payfirst, $paylast);
671 if ( $cust_main->payname && $method ne 'ECHECK' ) {
672 $payname = $cust_main->payname;
673 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
675 #$dbh->rollback if $oldAutoCommit;
676 return "Illegal payname $payname";
678 ($payfirst, $paylast) = ($1, $2);
680 $payfirst = $cust_main->getfield('first');
681 $paylast = $cust_main->getfield('last');
682 $payname = "$payfirst $paylast";
685 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
686 if ( $conf->exists('emailinvoiceauto')
687 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
688 push @invoicing_list, $cust_main->all_emails;
690 my $email = $invoicing_list[0];
692 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
694 my $description = 'Internet Services';
695 if ( $conf->exists('business-onlinepayment-description') ) {
696 my $dtempl = $conf->config('business-onlinepayment-description');
698 my $agent_obj = $cust_main->agent
699 or die "can't retreive agent for $cust_main (agentnum ".
700 $cust_main->agentnum. ")";
701 my $agent = $agent_obj->agent;
702 my $pkgs = join(', ',
703 map { $_->cust_pkg->part_pkg->pkg }
704 grep { $_->pkgnum } $self->cust_bill_pkg
706 $description = eval qq("$dtempl");
711 if ( $method eq 'CC' ) {
712 $content{card_number} = $cust_main->payinfo;
713 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
714 $content{expiration} = "$2/$1";
715 } elsif ( $method eq 'ECHECK' ) {
716 my($account_number,$routing_code) = $cust_main->payinfo;
717 ( $content{account_number}, $content{routing_code} ) =
718 split('@', $cust_main->payinfo);
719 $content{bank_name} = $cust_main->payname;
720 } elsif ( $method eq 'LEC' ) {
721 $content{phone} = $cust_main->payinfo;
725 new Business::OnlinePayment( $processor, @$options );
726 $transaction->content(
729 'password' => $password,
730 'action' => $action1,
731 'description' => $description,
733 'invoice_number' => $self->invnum,
734 'customer_id' => $self->custnum,
735 'last_name' => $paylast,
736 'first_name' => $payfirst,
738 'address' => $address,
739 'city' => $cust_main->city,
740 'state' => $cust_main->state,
741 'zip' => $cust_main->zip,
742 'country' => $cust_main->country,
743 'referer' => 'http://cleanwhisker.420.am/',
745 'phone' => $cust_main->daytime || $cust_main->night,
748 $transaction->submit();
750 if ( $transaction->is_success() && $action2 ) {
751 my $auth = $transaction->authorization;
752 my $ordernum = $transaction->can('order_number')
753 ? $transaction->order_number
756 #warn "********* $auth ***********\n";
757 #warn "********* $ordernum ***********\n";
759 new Business::OnlinePayment( $processor, @$options );
766 password => $password,
767 order_number => $ordernum,
769 authorization => $auth,
770 description => $description,
773 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
774 transaction_sequence_num local_transaction_date
775 local_transaction_time AVS_result_code )) {
776 $capture{$field} = $transaction->$field() if $transaction->can($field);
779 $capture->content( %capture );
783 unless ( $capture->is_success ) {
784 my $e = "Authorization sucessful but capture failed, invnum #".
785 $self->invnum. ': '. $capture->result_code.
786 ": ". $capture->error_message;
793 if ( $transaction->is_success() ) {
801 my $cust_pay = new FS::cust_pay ( {
802 'invnum' => $self->invnum,
805 'payby' => method2payby{$method},
806 'payinfo' => $cust_main->payinfo,
807 'paybatch' => "$processor:". $transaction->authorization,
809 my $error = $cust_pay->insert;
811 # gah, even with transactions.
812 my $e = 'WARNING: Card/ACH debited but database not updated - '.
813 'error applying payment, invnum #' . $self->invnum.
814 " ($processor): $error";
820 #} elsif ( $options{'report_badcard'} ) {
823 my $perror = "$processor error, invnum #". $self->invnum. ': '.
824 $transaction->result_code. ": ". $transaction->error_message;
826 if ( $conf->exists('emaildecline')
827 && grep { $_ ne 'POST' } $cust_main->invoicing_list
829 my @templ = $conf->config('declinetemplate');
830 my $template = new Text::Template (
832 SOURCE => [ map "$_\n", @templ ],
833 ) or return "($perror) can't create template: $Text::Template::ERROR";
835 or return "($perror) can't compile template: $Text::Template::ERROR";
837 my $templ_hash = { error => $transaction->error_message };
839 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
840 $ENV{MAILADDRESS} = $invoice_from;
841 my $header = new Mail::Header ( [
842 "From: $invoice_from",
843 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
844 "Sender: $invoice_from",
845 "Reply-To: $invoice_from",
846 "Date: ". time2str("%a, %d %b %Y %X %z", time),
847 "Subject: Your payment could not be processed",
849 my $message = new Mail::Internet (
851 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
854 $message->smtpsend( Host => $smtpmachine )
855 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
856 or return "($perror) (customer # ". $self->custnum.
857 ") can't send card decline email to ".
858 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
859 " via server $smtpmachine with SMTP: $!";
869 Adds a payment for this invoice to the pending credit card batch (see
870 L<FS::cust_pay_batch>).
876 my $cust_main = $self->cust_main;
878 my $cust_pay_batch = new FS::cust_pay_batch ( {
879 'invnum' => $self->getfield('invnum'),
880 'custnum' => $cust_main->getfield('custnum'),
881 'last' => $cust_main->getfield('last'),
882 'first' => $cust_main->getfield('first'),
883 'address1' => $cust_main->getfield('address1'),
884 'address2' => $cust_main->getfield('address2'),
885 'city' => $cust_main->getfield('city'),
886 'state' => $cust_main->getfield('state'),
887 'zip' => $cust_main->getfield('zip'),
888 'country' => $cust_main->getfield('country'),
890 'cardnum' => $cust_main->getfield('payinfo'),
891 'exp' => $cust_main->getfield('paydate'),
892 'payname' => $cust_main->getfield('payname'),
893 'amount' => $self->owed,
895 my $error = $cust_pay_batch->insert;
896 die $error if $error;
901 =item print_text [TIME];
903 Returns an text invoice, as a list of lines.
905 TIME an optional value used to control the printing of overdue messages. The
906 default is now. It isn't the date of the invoice; that's the `_date' field.
907 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
908 L<Time::Local> and L<Date::Parse> for conversion functions.
914 my( $self, $today, $template ) = @_;
916 # my $invnum = $self->invnum;
917 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
918 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
919 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
921 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
922 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
923 #my $balance_due = $self->owed + $pr_total - $cr_total;
924 my $balance_due = $self->owed + $pr_total;
927 #my($description,$amount);
931 foreach ( @pr_cust_bill ) {
933 "Previous Balance, Invoice #". $_->invnum.
934 " (". time2str("%x",$_->_date). ")",
935 $money_char. sprintf("%10.2f",$_->owed)
939 push @buf,['','-----------'];
940 push @buf,[ 'Total Previous Balance',
941 $money_char. sprintf("%10.2f",$pr_total ) ];
946 foreach ( ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
947 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
952 my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
953 my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
954 my($pkg)=$part_pkg->pkg;
956 if ( $_->setup != 0 ) {
957 push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
959 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
962 if ( $_->recur != 0 ) {
964 "$pkg (" . time2str("%x",$_->sdate) . " - " .
965 time2str("%x",$_->edate) . ")",
966 $money_char. sprintf("%10.2f",$_->recur)
969 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
973 my $itemdesc = defined $_->dbdef_table->column('itemdesc')
974 ? ( $_->itemdesc || 'Tax' )
976 push @buf,[$itemdesc, $money_char. sprintf("%10.2f",$_->setup) ]
981 push @buf,['','-----------'];
982 push @buf,['Total New Charges',
983 $money_char. sprintf("%10.2f",$self->charged) ];
986 push @buf,['','-----------'];
987 push @buf,['Total Charges',
988 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
992 foreach ( $self->cust_credited ) {
994 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
996 my $reason = substr($_->cust_credit->reason,0,32);
997 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
998 $reason = " ($reason) " if $reason;
1000 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1002 $money_char. sprintf("%10.2f",$_->amount)
1005 #foreach ( @cr_cust_credit ) {
1007 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1008 # $money_char. sprintf("%10.2f",$_->credited)
1012 #get & print payments
1013 foreach ( $self->cust_bill_pay ) {
1015 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1018 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1019 $money_char. sprintf("%10.2f",$_->amount )
1024 push @buf,['','-----------'];
1025 push @buf,['Balance Due', $money_char.
1026 sprintf("%10.2f", $balance_due ) ];
1028 #create the template
1029 my $templatefile = 'invoice_template';
1030 $templatefile .= "_$template" if $template;
1031 my @invoice_template = $conf->config($templatefile)
1032 or die "cannot load config file $templatefile";
1035 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1036 /invoice_lines\((\d*)\)/;
1037 $invoice_lines += $1 || scalar(@buf);
1040 die "no invoice_lines() functions in template?" unless $wasfunc;
1041 my $invoice_template = new Text::Template (
1043 SOURCE => [ map "$_\n", @invoice_template ],
1044 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1045 $invoice_template->compile()
1046 or die "can't compile template: $Text::Template::ERROR";
1048 #setup template variables
1049 package FS::cust_bill::_template; #!
1050 use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
1052 $invnum = $self->invnum;
1053 $date = $self->_date;
1056 if ( $FS::cust_bill::invoice_lines ) {
1058 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1060 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1065 #format address (variable for the template)
1067 @address = ( '', '', '', '', '', '' );
1068 package FS::cust_bill; #!
1069 $FS::cust_bill::_template::address[$l++] =
1070 $cust_main->payname.
1071 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1072 ? " (P.O. #". $cust_main->payinfo. ")"
1076 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1077 if $cust_main->company;
1078 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1079 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1080 if $cust_main->address2;
1081 $FS::cust_bill::_template::address[$l++] =
1082 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1083 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1084 unless $cust_main->country eq 'US';
1086 # #overdue? (variable for the template)
1087 # $FS::cust_bill::_template::overdue = (
1089 # && $today > $self->_date
1090 ## && $self->printed > 1
1091 # && $self->printed > 0
1094 #and subroutine for the template
1095 sub FS::cust_bill::_template::invoice_lines {
1096 my $lines = shift || scalar(@buf);
1098 scalar(@buf) ? shift @buf : [ '', '' ];
1104 $FS::cust_bill::_template::page = 1;
1108 push @collect, split("\n",
1109 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1111 $FS::cust_bill::_template::page++;
1114 map "$_\n", @collect;
1122 $Id: cust_bill.pm,v 1.57 2002-12-17 21:31:20 ivan Exp $
1128 print_text formatting (and some logic :/) is in source, but needs to be
1129 slurped in from a file. Also number of lines ($=).
1131 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1132 or something similar so the look can be completely customized?)
1136 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1137 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base