4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $lpr $invoice_from $smtpmachine );
6 use vars qw( $processor );
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( $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 eval "use Business::OnlinePayment";
47 $processor="Business::OnlinePayment::$bop_processor";
54 FS::cust_bill - Object methods for cust_bill records
60 $record = new FS::cust_bill \%hash;
61 $record = new FS::cust_bill { 'column' => 'value' };
63 $error = $record->insert;
65 $error = $new_record->replace($old_record);
67 $error = $record->delete;
69 $error = $record->check;
71 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
73 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
75 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
77 @cust_pay_objects = $cust_bill->cust_pay;
79 $tax_amount = $record->tax;
81 @lines = $cust_bill->print_text;
82 @lines = $cust_bill->print_text $time;
86 An FS::cust_bill object represents an invoice; a declaration that a customer
87 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
88 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
89 following fields are currently supported:
93 =item invnum - primary key (assigned automatically for new invoices)
95 =item custnum - customer (see L<FS::cust_main>)
97 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
98 L<Time::Local> and L<Date::Parse> for conversion functions.
100 =item charged - amount of this invoice
102 =item printed - deprecated
104 =item closed - books closed flag, empty or `Y'
114 Creates a new invoice. To add the invoice to the database, see L<"insert">.
115 Invoices are normally created by calling the bill method of a customer object
116 (see L<FS::cust_main>).
120 sub table { 'cust_bill'; }
124 Adds this invoice to the database ("Posts" the invoice). If there is an error,
125 returns the error, otherwise returns false.
129 Currently unimplemented. I don't remove invoices because there would then be
130 no record you ever posted this invoice (which is bad, no?)
136 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
137 $self->SUPER::delete(@_);
140 =item replace OLD_RECORD
142 Replaces the OLD_RECORD with this one in the database. If there is an error,
143 returns the error, otherwise returns false.
145 Only printed may be changed. printed is normally updated by calling the
146 collect method of a customer object (see L<FS::cust_main>).
151 my( $new, $old ) = ( shift, shift );
152 return "Can't change custnum!" unless $old->custnum == $new->custnum;
153 #return "Can't change _date!" unless $old->_date eq $new->_date;
154 return "Can't change _date!" unless $old->_date == $new->_date;
155 return "Can't change charged!" unless $old->charged == $new->charged;
157 $new->SUPER::replace($old);
162 Checks all fields to make sure this is a valid invoice. If there is an error,
163 returns the error, otherwise returns false. Called by the insert and replace
172 $self->ut_numbern('invnum')
173 || $self->ut_number('custnum')
174 || $self->ut_numbern('_date')
175 || $self->ut_money('charged')
176 || $self->ut_numbern('printed')
177 || $self->ut_enum('closed', [ '', 'Y' ])
179 return $error if $error;
181 return "Unknown customer"
182 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
184 $self->_date(time) unless $self->_date;
186 $self->printed(0) if $self->printed eq '';
193 Returns a list consisting of the total previous balance for this customer,
194 followed by the previous outstanding invoices (as FS::cust_bill objects also).
201 my @cust_bill = sort { $a->_date <=> $b->_date }
202 grep { $_->owed != 0 && $_->_date < $self->_date }
203 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
205 foreach ( @cust_bill ) { $total += $_->owed; }
211 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
217 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
220 =item cust_bill_event
222 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
227 sub cust_bill_event {
229 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
235 Returns the customer (see L<FS::cust_main>) for this invoice.
241 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
246 Depreciated. See the cust_credited method.
248 #Returns a list consisting of the total previous credited (see
249 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
250 #outstanding credits (FS::cust_credit objects).
256 croak "FS::cust_bill->cust_credit depreciated; see ".
257 "FS::cust_bill->cust_credit_bill";
260 #my @cust_credit = sort { $a->_date <=> $b->_date }
261 # grep { $_->credited != 0 && $_->_date < $self->_date }
262 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
264 #foreach (@cust_credit) { $total += $_->credited; }
265 #$total, @cust_credit;
270 Depreciated. See the cust_bill_pay method.
272 #Returns all payments (see L<FS::cust_pay>) for this invoice.
278 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
280 #sort { $a->_date <=> $b->_date }
281 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
287 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
293 sort { $a->_date <=> $b->_date }
294 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
299 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
305 sort { $a->_date <=> $b->_date }
306 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
312 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
319 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
321 foreach (@taxlines) { $total += $_->setup; }
327 Returns the amount owed (still outstanding) on this invoice, which is charged
328 minus all payment applications (see L<FS::cust_bill_pay>) and credit
329 applications (see L<FS::cust_credit_bill>).
335 my $balance = $self->charged;
336 $balance -= $_->amount foreach ( $self->cust_bill_pay );
337 $balance -= $_->amount foreach ( $self->cust_credited );
338 $balance = sprintf( "%.2f", $balance);
339 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
345 Sends this invoice to the destinations configured for this customer: send
346 emails or print. See L<FS::cust_main_invoice>.
351 my($self,$template) = @_;
352 my @print_text = $self->print_text('', $template);
353 my @invoicing_list = $self->cust_main->invoicing_list;
355 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
357 #better to notify this person than silence
358 @invoicing_list = ($invoice_from) unless @invoicing_list;
360 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
361 #$ENV{SMTPHOSTS} = $smtpmachine;
362 $ENV{MAILADDRESS} = $invoice_from;
363 my $header = new Mail::Header ( [
364 "From: $invoice_from",
365 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
366 "Sender: $invoice_from",
367 "Reply-To: $invoice_from",
368 "Date: ". time2str("%a, %d %b %Y %X %z", time),
371 my $message = new Mail::Internet (
373 'Body' => [ @print_text ], #( date)
376 $message->smtpsend( Host => $smtpmachine )
377 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
378 or return "(customer # ". $self->custnum. ") can't send invoice email".
379 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
380 " via server $smtpmachine with SMTP: $!";
384 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
386 or return "Can't open pipe to $lpr: $!";
387 print LPR @print_text;
389 or return $! ? "Error closing $lpr: $!"
390 : "Exit status $? from $lpr";
397 =item send_csv OPTIONS
399 Sends invoice as a CSV data-file to a remote host with the specified protocol.
403 protocol - currently only "ftp"
409 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
410 and YYMMDDHHMMSS is a timestamp.
412 The fields of the CSV file is as follows:
414 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
418 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
420 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
421 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
422 fields are filled in.
424 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
425 first two fields (B<record_type> and B<invnum>) and the last five fields
426 (B<pkg> through B<edate>) are filled in.
428 =item invnum - invoice number
430 =item custnum - customer number
432 =item _date - invoice date
434 =item charged - total invoice amount
436 =item first - customer first name
438 =item last - customer first name
440 =item company - company name
442 =item address1 - address line 1
444 =item address2 - address line 1
454 =item pkg - line item description
456 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
458 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
460 =item sdate - start date for recurring fee
462 =item edate - end date for recurring fee
469 my($self, %opt) = @_;
471 #part one: create file
473 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
474 mkdir $spooldir, 0700 unless -d $spooldir;
476 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
478 open(CSV, ">$file") or die "can't open $file: $!";
480 eval "use Text::CSV_XS";
483 my $csv = Text::CSV_XS->new({'always_quote'=>1});
485 my $cust_main = $self->cust_main;
491 time2str("%x", $self->_date),
492 sprintf("%.2f", $self->charged),
493 ( map { $cust_main->getfield($_) }
494 qw( first last company address1 address2 city state zip country ) ),
496 ) or die "can't create csv";
497 print CSV $csv->string. "\n";
499 #new charges (false laziness w/print_text)
500 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
502 my($pkg, $setup, $recur, $sdate, $edate);
503 if ( $cust_bill_pkg->pkgnum ) {
505 ($pkg, $setup, $recur, $sdate, $edate) = (
506 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
507 ( $cust_bill_pkg->setup != 0
508 ? sprintf("%.2f", $cust_bill_pkg->setup )
510 ( $cust_bill_pkg->recur != 0
511 ? sprintf("%.2f", $cust_bill_pkg->recur )
513 time2str("%x", $cust_bill_pkg->sdate),
514 time2str("%x", $cust_bill_pkg->edate),
518 next unless $cust_bill_pkg->setup != 0;
519 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
520 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
522 ($pkg, $setup, $recur, $sdate, $edate) =
523 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
529 ( map { '' } (1..11) ),
530 ($pkg, $setup, $recur, $sdate, $edate)
531 ) or die "can't create csv";
532 print CSV $csv->string. "\n";
536 close CSV or die "can't close CSV: $!";
541 if ( $opt{protocol} eq 'ftp' ) {
542 eval "use Net::FTP;";
544 $net = Net::FTP->new($opt{server}) or die @$;
546 die "unknown protocol: $opt{protocol}";
549 $net->login( $opt{username}, $opt{password} )
550 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
552 $net->binary or die "can't set binary mode";
554 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
556 $net->put($file) or die "can't put $file: $!";
566 Pays this invoice with a compliemntary payment. If there is an error,
567 returns the error, otherwise returns false.
573 my $cust_pay = new FS::cust_pay ( {
574 'invnum' => $self->invnum,
575 'paid' => $self->owed,
578 'payinfo' => $self->cust_main->payinfo,
586 Attempts to pay this invoice with a credit card payment via a
587 Business::OnlinePayment realtime gateway. See
588 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
589 for supported processors.
595 $self->realtime_bop('CC', @_);
600 Attempts to pay this invoice with an electronic check (ACH) payment via a
601 Business::OnlinePayment realtime gateway. See
602 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
603 for supported processors.
609 $self->realtime_bop('CHECK', @_);
615 my $cust_main = $self->cust_main;
616 my $amount = $self->owed;
618 unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
619 return "Real-time card/ACH processing not enabled (processor $processor)";
621 my $bop_processor = $1; #hmm?
623 my $address = $cust_main->address1;
624 $address .= ", ". $cust_main->address2 if $cust_main->address2;
626 my($payname, $payfirst, $paylast);
627 if ( $cust_main->payname && $method ne 'CHECK' ) {
628 $payname = $cust_main->payname;
629 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
631 #$dbh->rollback if $oldAutoCommit;
632 return "Illegal payname $payname";
634 ($payfirst, $paylast) = ($1, $2);
636 $payfirst = $cust_main->getfield('first');
637 $paylast = $cust_main->getfield('last');
638 $payname = "$payfirst $paylast";
641 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
642 if ( $conf->exists('emailinvoiceauto')
643 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
644 push @invoicing_list, $cust_main->all_emails;
646 my $email = $invoicing_list[0];
648 my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
650 my $description = 'Internet Services';
651 if ( $conf->exists('business-onlinepayment-description') ) {
652 my $dtempl = $conf->config('business-onlinepayment-description');
654 my $agent_obj = $cust_main->agent
655 or die "can't retreive agent for $cust_main (agentnum ".
656 $cust_main->agentnum. ")";
657 my $agent = $agent_obj->agent;
658 my $pkgs = join(', ',
659 map { $_->cust_pkg->part_pkg->pkg }
660 grep { $_->pkgnum } $self->cust_bill_pkg
662 $description = eval qq("$dtempl");
667 if ( $method eq 'CC' ) {
668 $content{card_number} = $cust_main->payinfo;
669 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
670 $content{expiration} = "$2/$1";
671 } elsif ( $method eq 'CHECK' ) {
672 my($account_number,$routing_code) = $cust_main->payinfo;
673 ( $content{account_number}, $content{routing_code} ) =
674 split('@', $cust_main->payinfo);
675 $content{bank_name} = $cust_main->payname;
679 new Business::OnlinePayment( $bop_processor, @bop_options );
680 $transaction->content(
683 'login' => $bop_login,
684 'password' => $bop_password,
685 'action' => $action1,
686 'description' => $description,
688 'invoice_number' => $self->invnum,
689 'customer_id' => $self->custnum,
690 'last_name' => $paylast,
691 'first_name' => $payfirst,
693 'address' => $address,
694 'city' => $cust_main->city,
695 'state' => $cust_main->state,
696 'zip' => $cust_main->zip,
697 'country' => $cust_main->country,
698 'referer' => 'http://cleanwhisker.420.am/',
700 'phone' => $cust_main->daytime || $cust_main->night,
702 $transaction->submit();
704 if ( $transaction->is_success() && $action2 ) {
705 my $auth = $transaction->authorization;
706 my $ordernum = $transaction->can('order_number')
707 ? $transaction->order_number
710 #warn "********* $auth ***********\n";
711 #warn "********* $ordernum ***********\n";
713 new Business::OnlinePayment( $bop_processor, @bop_options );
720 password => $bop_password,
721 order_number => $ordernum,
723 authorization => $auth,
724 description => $description,
727 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
728 transaction_sequence_num local_transaction_date
729 local_transaction_time AVS_result_code )) {
730 $capture{$field} = $transaction->$field() if $transaction->can($field);
733 $capture->content( %capture );
737 unless ( $capture->is_success ) {
738 my $e = "Authorization sucessful but capture failed, invnum #".
739 $self->invnum. ': '. $capture->result_code.
740 ": ". $capture->error_message;
747 if ( $transaction->is_success() ) {
754 my $cust_pay = new FS::cust_pay ( {
755 'invnum' => $self->invnum,
758 'payby' => method2payby{$method},
759 'payinfo' => $cust_main->payinfo,
760 'paybatch' => "$processor:". $transaction->authorization,
762 my $error = $cust_pay->insert;
764 # gah, even with transactions.
765 my $e = 'WARNING: Card/ACH debited but database not updated - '.
766 'error applying payment, invnum #' . $self->invnum.
767 " ($processor): $error";
773 #} elsif ( $options{'report_badcard'} ) {
776 my $perror = "$processor error, invnum #". $self->invnum. ': '.
777 $transaction->result_code. ": ". $transaction->error_message;
779 if ( $conf->exists('emaildecline')
780 && grep { $_ ne 'POST' } $cust_main->invoicing_list
782 my @templ = $conf->config('declinetemplate');
783 my $template = new Text::Template (
785 SOURCE => [ map "$_\n", @templ ],
786 ) or return "($perror) can't create template: $Text::Template::ERROR";
788 or return "($perror) can't compile template: $Text::Template::ERROR";
790 my $templ_hash = { error => $transaction->error_message };
792 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
793 $ENV{MAILADDRESS} = $invoice_from;
794 my $header = new Mail::Header ( [
795 "From: $invoice_from",
796 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
797 "Sender: $invoice_from",
798 "Reply-To: $invoice_from",
799 "Date: ". time2str("%a, %d %b %Y %X %z", time),
800 "Subject: Your payment could not be processed",
802 my $message = new Mail::Internet (
804 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
807 $message->smtpsend( Host => $smtpmachine )
808 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
809 or return "($perror) (customer # ". $self->custnum.
810 ") can't send card decline email to ".
811 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
812 " via server $smtpmachine with SMTP: $!";
822 Adds a payment for this invoice to the pending credit card batch (see
823 L<FS::cust_pay_batch>).
829 my $cust_main = $self->cust_main;
831 my $cust_pay_batch = new FS::cust_pay_batch ( {
832 'invnum' => $self->getfield('invnum'),
833 'custnum' => $cust_main->getfield('custnum'),
834 'last' => $cust_main->getfield('last'),
835 'first' => $cust_main->getfield('first'),
836 'address1' => $cust_main->getfield('address1'),
837 'address2' => $cust_main->getfield('address2'),
838 'city' => $cust_main->getfield('city'),
839 'state' => $cust_main->getfield('state'),
840 'zip' => $cust_main->getfield('zip'),
841 'country' => $cust_main->getfield('country'),
843 'cardnum' => $cust_main->getfield('payinfo'),
844 'exp' => $cust_main->getfield('paydate'),
845 'payname' => $cust_main->getfield('payname'),
846 'amount' => $self->owed,
848 my $error = $cust_pay_batch->insert;
849 die $error if $error;
854 =item print_text [TIME];
856 Returns an text invoice, as a list of lines.
858 TIME an optional value used to control the printing of overdue messages. The
859 default is now. It isn't the date of the invoice; that's the `_date' field.
860 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
861 L<Time::Local> and L<Date::Parse> for conversion functions.
867 my( $self, $today, $template ) = @_;
869 # my $invnum = $self->invnum;
870 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
871 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
872 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
874 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
875 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
876 #my $balance_due = $self->owed + $pr_total - $cr_total;
877 my $balance_due = $self->owed + $pr_total;
880 #my($description,$amount);
884 foreach ( @pr_cust_bill ) {
886 "Previous Balance, Invoice #". $_->invnum.
887 " (". time2str("%x",$_->_date). ")",
888 $money_char. sprintf("%10.2f",$_->owed)
892 push @buf,['','-----------'];
893 push @buf,[ 'Total Previous Balance',
894 $money_char. sprintf("%10.2f",$pr_total ) ];
899 foreach ( ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
900 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
905 my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
906 my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
907 my($pkg)=$part_pkg->pkg;
909 if ( $_->setup != 0 ) {
910 push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
912 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
915 if ( $_->recur != 0 ) {
917 "$pkg (" . time2str("%x",$_->sdate) . " - " .
918 time2str("%x",$_->edate) . ")",
919 $money_char. sprintf("%10.2f",$_->recur)
922 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
926 my $itemdesc = defined $_->dbdef_table->column('itemdesc')
927 ? ( $_->itemdesc || 'Tax' )
929 push @buf,[$itemdesc, $money_char. sprintf("%10.2f",$_->setup) ]
934 push @buf,['','-----------'];
935 push @buf,['Total New Charges',
936 $money_char. sprintf("%10.2f",$self->charged) ];
939 push @buf,['','-----------'];
940 push @buf,['Total Charges',
941 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
945 foreach ( $self->cust_credited ) {
947 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
949 my $reason = substr($_->cust_credit->reason,0,32);
950 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
951 $reason = " ($reason) " if $reason;
953 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
955 $money_char. sprintf("%10.2f",$_->amount)
958 #foreach ( @cr_cust_credit ) {
960 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
961 # $money_char. sprintf("%10.2f",$_->credited)
965 #get & print payments
966 foreach ( $self->cust_bill_pay ) {
968 #something more elaborate if $_->amount ne ->cust_pay->paid ?
971 "Payment received ". time2str("%x",$_->cust_pay->_date ),
972 $money_char. sprintf("%10.2f",$_->amount )
977 push @buf,['','-----------'];
978 push @buf,['Balance Due', $money_char.
979 sprintf("%10.2f", $balance_due ) ];
982 my $templatefile = 'invoice_template';
983 $templatefile .= "_$template" if $template;
984 my @invoice_template = $conf->config($templatefile)
985 or die "cannot load config file $templatefile";
988 foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
989 /invoice_lines\((\d+)\)/;
990 $invoice_lines += $1;
993 die "no invoice_lines() functions in template?" unless $wasfunc;
994 my $invoice_template = new Text::Template (
996 SOURCE => [ map "$_\n", @invoice_template ],
997 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
998 $invoice_template->compile()
999 or die "can't compile template: $Text::Template::ERROR";
1001 #setup template variables
1002 package FS::cust_bill::_template; #!
1003 use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
1005 $invnum = $self->invnum;
1006 $date = $self->_date;
1009 if ( $FS::cust_bill::invoice_lines ) {
1011 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1013 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1018 #format address (variable for the template)
1020 @address = ( '', '', '', '', '', '' );
1021 package FS::cust_bill; #!
1022 $FS::cust_bill::_template::address[$l++] =
1023 $cust_main->payname.
1024 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1025 ? " (P.O. #". $cust_main->payinfo. ")"
1029 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1030 if $cust_main->company;
1031 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1032 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1033 if $cust_main->address2;
1034 $FS::cust_bill::_template::address[$l++] =
1035 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1036 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1037 unless $cust_main->country eq 'US';
1039 # #overdue? (variable for the template)
1040 # $FS::cust_bill::_template::overdue = (
1042 # && $today > $self->_date
1043 ## && $self->printed > 1
1044 # && $self->printed > 0
1047 #and subroutine for the template
1049 sub FS::cust_bill::_template::invoice_lines {
1050 my $lines = shift or return @buf;
1052 scalar(@buf) ? shift @buf : [ '', '' ];
1059 $FS::cust_bill::_template::page = 1;
1063 push @collect, split("\n",
1064 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1066 $FS::cust_bill::_template::page++;
1069 map "$_\n", @collect;
1077 $Id: cust_bill.pm,v 1.50 2002-10-15 09:54:24 ivan Exp $
1083 print_text formatting (and some logic :/) is in source, but needs to be
1084 slurped in from a file. Also number of lines ($=).
1086 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1087 or something similar so the look can be completely customized?)
1091 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1092 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base