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 ) { #email invoice
356 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
357 #$ENV{SMTPHOSTS} = $smtpmachine;
358 $ENV{MAILADDRESS} = $invoice_from;
359 my $header = new Mail::Header ( [
360 "From: $invoice_from",
361 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
362 "Sender: $invoice_from",
363 "Reply-To: $invoice_from",
364 "Date: ". time2str("%a, %d %b %Y %X %z", time),
367 my $message = new Mail::Internet (
369 'Body' => [ @print_text ], #( date)
372 $message->smtpsend( Host => $smtpmachine )
373 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
374 or return "(customer # ". $self->custnum. ") can't send invoice email".
375 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
376 " via server $smtpmachine with SMTP: $!";
380 if ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) { #postal
382 or return "Can't open pipe to $lpr: $!";
383 print LPR @print_text;
385 or return $! ? "Error closing $lpr: $!"
386 : "Exit status $? from $lpr";
393 =item send_csv OPTIONS
395 Sends invoice as a CSV data-file to a remote host with the specified protocol.
399 protocol - currently only "ftp"
405 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
406 and YYMMDDHHMMSS is a timestamp.
408 The fields of the CSV file is as follows:
410 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
414 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
416 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
417 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
418 fields are filled in.
420 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
421 first two fields (B<record_type> and B<invnum>) and the last five fields
422 (B<pkg> through B<edate>) are filled in.
424 =item invnum - invoice number
426 =item custnum - customer number
428 =item _date - invoice date
430 =item charged - total invoice amount
432 =item first - customer first name
434 =item last - customer first name
436 =item company - company name
438 =item address1 - address line 1
440 =item address2 - address line 1
450 =item pkg - line item description
452 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
454 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
456 =item sdate - start date for recurring fee
458 =item edate - end date for recurring fee
465 my($self, %opt) = @_;
467 #part one: create file
469 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
470 mkdir $spooldir, 0700 unless -d $spooldir;
472 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
474 open(CSV, ">$file") or die "can't open $file: $!";
476 eval "use Text::CSV_XS";
479 my $csv = Text::CSV_XS->new({'always_quote'=>1});
481 my $cust_main = $self->cust_main;
487 time2str("%x", $self->_date),
488 sprintf("%.2f", $self->charged),
489 ( map { $cust_main->getfield($_) }
490 qw( first last company address1 address2 city state zip country ) ),
492 ) or die "can't create csv";
493 print CSV $csv->string. "\n";
495 #new charges (false laziness w/print_text)
496 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
498 my($pkg, $setup, $recur, $sdate, $edate);
499 if ( $cust_bill_pkg->pkgnum ) {
501 ($pkg, $setup, $recur, $sdate, $edate) = (
502 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
503 ( $cust_bill_pkg->setup != 0
504 ? sprintf("%.2f", $cust_bill_pkg->setup )
506 ( $cust_bill_pkg->recur != 0
507 ? sprintf("%.2f", $cust_bill_pkg->recur )
509 time2str("%x", $cust_bill_pkg->sdate),
510 time2str("%x", $cust_bill_pkg->edate),
514 next unless $cust_bill_pkg->setup != 0;
515 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
516 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
518 ($pkg, $setup, $recur, $sdate, $edate) =
519 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
525 ( map { '' } (1..11) ),
526 ($pkg, $setup, $recur, $sdate, $edate)
527 ) or die "can't create csv";
528 print CSV $csv->string. "\n";
532 close CSV or die "can't close CSV: $!";
537 if ( $opt{protocol} eq 'ftp' ) {
538 eval "use Net::FTP;";
540 $net = Net::FTP->new($opt{server}) or die @$;
542 die "unknown protocol: $opt{protocol}";
545 $net->login( $opt{username}, $opt{password} )
546 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
548 $net->binary or die "can't set binary mode";
550 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
552 $net->put($file) or die "can't put $file: $!";
562 Pays this invoice with a compliemntary payment. If there is an error,
563 returns the error, otherwise returns false.
569 my $cust_pay = new FS::cust_pay ( {
570 'invnum' => $self->invnum,
571 'paid' => $self->owed,
574 'payinfo' => $self->cust_main->payinfo,
582 Attempts to pay this invoice with a Business::OnlinePayment realtime gateway.
583 See http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
584 for supproted processors.
590 my $cust_main = $self->cust_main;
591 my $amount = $self->owed;
593 unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
594 return "Real-time card processing not enabled (processor $processor)";
596 my $bop_processor = $1; #hmm?
598 my $address = $cust_main->address1;
599 $address .= ", ". $cust_main->address2 if $cust_main->address2;
602 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
603 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
606 my($payname, $payfirst, $paylast);
607 if ( $cust_main->payname ) {
608 $payname = $cust_main->payname;
609 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
611 #$dbh->rollback if $oldAutoCommit;
612 return "Illegal payname $payname";
614 ($payfirst, $paylast) = ($1, $2);
616 $payfirst = $cust_main->getfield('first');
617 $paylast = $cust_main->getfield('last');
618 $payname = "$payfirst $paylast";
621 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
622 if ( $conf->exists('emailinvoiceauto')
623 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
624 push @invoicing_list, $cust_main->all_emails;
626 my $email = $invoicing_list[0];
628 my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
630 my $description = 'Internet Services';
631 if ( $conf->exists('business-onlinepayment-description') ) {
632 my $dtempl = $conf->config('business-onlinepayment-description');
634 my $agent_obj = $cust_main->agent
635 or die "can't retreive agent for $cust_main (agentnum ".
636 $cust_main->agentnum. ")";
637 my $agent = $agent_obj->agent;
638 my $pkgs = join(', ',
639 map { $_->cust_pkg->part_pkg->pkg }
640 grep { $_->pkgnum } $self->cust_bill_pkg
642 $description = eval qq("$dtempl");
647 new Business::OnlinePayment( $bop_processor, @bop_options );
648 $transaction->content(
650 'login' => $bop_login,
651 'password' => $bop_password,
652 'action' => $action1,
653 'description' => $description,
655 'invoice_number' => $self->invnum,
656 'customer_id' => $self->custnum,
657 'last_name' => $paylast,
658 'first_name' => $payfirst,
660 'address' => $address,
661 'city' => $cust_main->city,
662 'state' => $cust_main->state,
663 'zip' => $cust_main->zip,
664 'country' => $cust_main->country,
665 'card_number' => $cust_main->payinfo,
666 'expiration' => $exp,
667 'referer' => 'http://cleanwhisker.420.am/',
669 'phone' => $cust_main->daytime || $cust_main->night,
671 $transaction->submit();
673 if ( $transaction->is_success() && $action2 ) {
674 my $auth = $transaction->authorization;
675 my $ordernum = $transaction->can('order_number')
676 ? $transaction->order_number
679 #warn "********* $auth ***********\n";
680 #warn "********* $ordernum ***********\n";
682 new Business::OnlinePayment( $bop_processor, @bop_options );
688 password => $bop_password,
689 order_number => $ordernum,
691 authorization => $auth,
692 description => $description,
693 card_number => $cust_main->payinfo,
697 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
698 transaction_sequence_num local_transaction_date
699 local_transaction_time AVS_result_code )) {
700 $capture{$field} = $transaction->$field() if $transaction->can($field);
703 $capture->content( %capture );
707 unless ( $capture->is_success ) {
708 my $e = "Authorization sucessful but capture failed, invnum #".
709 $self->invnum. ': '. $capture->result_code.
710 ": ". $capture->error_message;
717 if ( $transaction->is_success() ) {
719 my $cust_pay = new FS::cust_pay ( {
720 'invnum' => $self->invnum,
724 'payinfo' => $cust_main->payinfo,
725 'paybatch' => "$processor:". $transaction->authorization,
727 my $error = $cust_pay->insert;
729 # gah, even with transactions.
730 my $e = 'WARNING: Card debited but database not updated - '.
731 'error applying payment, invnum #' . $self->invnum.
732 " ($processor): $error";
738 #} elsif ( $options{'report_badcard'} ) {
741 my $perror = "$processor error, invnum #". $self->invnum. ': '.
742 $transaction->result_code. ": ". $transaction->error_message;
744 if ( $conf->exists('emaildecline')
745 && grep { $_ ne 'POST' } $cust_main->invoicing_list
747 my @templ = $conf->config('declinetemplate');
748 my $template = new Text::Template (
750 SOURCE => [ map "$_\n", @templ ],
751 ) or return "($perror) can't create template: $Text::Template::ERROR";
753 or return "($perror) can't compile template: $Text::Template::ERROR";
755 my $templ_hash = { error => $transaction->error_message };
757 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
758 $ENV{MAILADDRESS} = $invoice_from;
759 my $header = new Mail::Header ( [
760 "From: $invoice_from",
761 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
762 "Sender: $invoice_from",
763 "Reply-To: $invoice_from",
764 "Date: ". time2str("%a, %d %b %Y %X %z", time),
765 "Subject: Your credit card could not be processed",
767 my $message = new Mail::Internet (
769 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
772 $message->smtpsend( Host => $smtpmachine )
773 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
774 or return "($perror) (customer # ". $self->custnum.
775 ") can't send card decline email to ".
776 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
777 " via server $smtpmachine with SMTP: $!";
787 Adds a payment for this invoice to the pending credit card batch (see
788 L<FS::cust_pay_batch>).
794 my $cust_main = $self->cust_main;
796 my $cust_pay_batch = new FS::cust_pay_batch ( {
797 'invnum' => $self->getfield('invnum'),
798 'custnum' => $cust_main->getfield('custnum'),
799 'last' => $cust_main->getfield('last'),
800 'first' => $cust_main->getfield('first'),
801 'address1' => $cust_main->getfield('address1'),
802 'address2' => $cust_main->getfield('address2'),
803 'city' => $cust_main->getfield('city'),
804 'state' => $cust_main->getfield('state'),
805 'zip' => $cust_main->getfield('zip'),
806 'country' => $cust_main->getfield('country'),
808 'cardnum' => $cust_main->getfield('payinfo'),
809 'exp' => $cust_main->getfield('paydate'),
810 'payname' => $cust_main->getfield('payname'),
811 'amount' => $self->owed,
813 my $error = $cust_pay_batch->insert;
814 die $error if $error;
819 =item print_text [TIME];
821 Returns an text invoice, as a list of lines.
823 TIME an optional value used to control the printing of overdue messages. The
824 default is now. It isn't the date of the invoice; that's the `_date' field.
825 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
826 L<Time::Local> and L<Date::Parse> for conversion functions.
832 my( $self, $today, $template ) = @_;
834 # my $invnum = $self->invnum;
835 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
836 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
837 unless $cust_main->payname;
839 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
840 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
841 #my $balance_due = $self->owed + $pr_total - $cr_total;
842 my $balance_due = $self->owed + $pr_total;
845 #my($description,$amount);
849 foreach ( @pr_cust_bill ) {
851 "Previous Balance, Invoice #". $_->invnum.
852 " (". time2str("%x",$_->_date). ")",
853 $money_char. sprintf("%10.2f",$_->owed)
857 push @buf,['','-----------'];
858 push @buf,[ 'Total Previous Balance',
859 $money_char. sprintf("%10.2f",$pr_total ) ];
864 foreach ( ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
865 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
870 my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
871 my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
872 my($pkg)=$part_pkg->pkg;
874 if ( $_->setup != 0 ) {
875 push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
877 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
880 if ( $_->recur != 0 ) {
882 "$pkg (" . time2str("%x",$_->sdate) . " - " .
883 time2str("%x",$_->edate) . ")",
884 $money_char. sprintf("%10.2f",$_->recur)
887 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
891 my $itemdesc = defined $_->dbdef_table->column('itemdesc')
892 ? ( $_->itemdesc || 'Tax' )
894 push @buf,[$itemdesc, $money_char. sprintf("%10.2f",$_->setup) ]
899 push @buf,['','-----------'];
900 push @buf,['Total New Charges',
901 $money_char. sprintf("%10.2f",$self->charged) ];
904 push @buf,['','-----------'];
905 push @buf,['Total Charges',
906 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
910 foreach ( $self->cust_credited ) {
912 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
914 my $reason = substr($_->cust_credit->reason,0,32);
915 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
916 $reason = " ($reason) " if $reason;
918 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
920 $money_char. sprintf("%10.2f",$_->amount)
923 #foreach ( @cr_cust_credit ) {
925 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
926 # $money_char. sprintf("%10.2f",$_->credited)
930 #get & print payments
931 foreach ( $self->cust_bill_pay ) {
933 #something more elaborate if $_->amount ne ->cust_pay->paid ?
936 "Payment received ". time2str("%x",$_->cust_pay->_date ),
937 $money_char. sprintf("%10.2f",$_->amount )
942 push @buf,['','-----------'];
943 push @buf,['Balance Due', $money_char.
944 sprintf("%10.2f", $balance_due ) ];
947 my $templatefile = 'invoice_template';
948 $templatefile .= "_$template" if $template;
949 my @invoice_template = $conf->config($templatefile)
950 or die "cannot load config file $templatefile";
953 foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
954 /invoice_lines\((\d+)\)/;
955 $invoice_lines += $1;
958 die "no invoice_lines() functions in template?" unless $wasfunc;
959 my $invoice_template = new Text::Template (
961 SOURCE => [ map "$_\n", @invoice_template ],
962 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
963 $invoice_template->compile()
964 or die "can't compile template: $Text::Template::ERROR";
966 #setup template variables
967 package FS::cust_bill::_template; #!
968 use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
970 $invnum = $self->invnum;
971 $date = $self->_date;
974 if ( $FS::cust_bill::invoice_lines ) {
976 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
978 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
983 #format address (variable for the template)
985 @address = ( '', '', '', '', '', '' );
986 package FS::cust_bill; #!
987 $FS::cust_bill::_template::address[$l++] =
989 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
990 ? " (P.O. #". $cust_main->payinfo. ")"
994 $FS::cust_bill::_template::address[$l++] = $cust_main->company
995 if $cust_main->company;
996 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
997 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
998 if $cust_main->address2;
999 $FS::cust_bill::_template::address[$l++] =
1000 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1001 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1002 unless $cust_main->country eq 'US';
1004 # #overdue? (variable for the template)
1005 # $FS::cust_bill::_template::overdue = (
1007 # && $today > $self->_date
1008 ## && $self->printed > 1
1009 # && $self->printed > 0
1012 #and subroutine for the template
1014 sub FS::cust_bill::_template::invoice_lines {
1015 my $lines = shift or return @buf;
1017 scalar(@buf) ? shift @buf : [ '', '' ];
1024 $FS::cust_bill::_template::page = 1;
1028 push @collect, split("\n",
1029 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1031 $FS::cust_bill::_template::page++;
1034 map "$_\n", @collect;
1042 $Id: cust_bill.pm,v 1.46 2002-09-21 11:17:39 ivan Exp $
1048 print_text formatting (and some logic :/) is in source, but needs to be
1049 slurped in from a file. Also number of lines ($=).
1051 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1052 or something similar so the look can be completely customized?)
1056 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1057 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base