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 ( $bop_processor,$bop_login, $bop_password, $bop_action ) = ( '', '', '', '');
41 ( $ach_processor,$ach_login, $ach_password, $ach_action ) = ( '', '', '', '');
44 if ( $conf->exists('business-onlinepayment') ) {
50 ) = $conf->config('business-onlinepayment');
51 $bop_action ||= 'normal authorization';
52 ( $ach_processor, $ach_login, $ach_password, $ach_action, @ach_options ) =
53 ( $bop_processor, $bop_login, $bop_password, $bop_action, @bop_options );
54 eval "use Business::OnlinePayment";
57 if ( $conf->exists('business-onlinepayment-ach') ) {
63 ) = $conf->config('business-onlinepayment-ach');
64 $ach_action ||= 'normal authorization';
65 eval "use Business::OnlinePayment";
72 FS::cust_bill - Object methods for cust_bill records
78 $record = new FS::cust_bill \%hash;
79 $record = new FS::cust_bill { 'column' => 'value' };
81 $error = $record->insert;
83 $error = $new_record->replace($old_record);
85 $error = $record->delete;
87 $error = $record->check;
89 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
91 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
93 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
95 @cust_pay_objects = $cust_bill->cust_pay;
97 $tax_amount = $record->tax;
99 @lines = $cust_bill->print_text;
100 @lines = $cust_bill->print_text $time;
104 An FS::cust_bill object represents an invoice; a declaration that a customer
105 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
106 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
107 following fields are currently supported:
111 =item invnum - primary key (assigned automatically for new invoices)
113 =item custnum - customer (see L<FS::cust_main>)
115 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
116 L<Time::Local> and L<Date::Parse> for conversion functions.
118 =item charged - amount of this invoice
120 =item printed - deprecated
122 =item closed - books closed flag, empty or `Y'
132 Creates a new invoice. To add the invoice to the database, see L<"insert">.
133 Invoices are normally created by calling the bill method of a customer object
134 (see L<FS::cust_main>).
138 sub table { 'cust_bill'; }
142 Adds this invoice to the database ("Posts" the invoice). If there is an error,
143 returns the error, otherwise returns false.
147 Currently unimplemented. I don't remove invoices because there would then be
148 no record you ever posted this invoice (which is bad, no?)
154 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
155 $self->SUPER::delete(@_);
158 =item replace OLD_RECORD
160 Replaces the OLD_RECORD with this one in the database. If there is an error,
161 returns the error, otherwise returns false.
163 Only printed may be changed. printed is normally updated by calling the
164 collect method of a customer object (see L<FS::cust_main>).
169 my( $new, $old ) = ( shift, shift );
170 return "Can't change custnum!" unless $old->custnum == $new->custnum;
171 #return "Can't change _date!" unless $old->_date eq $new->_date;
172 return "Can't change _date!" unless $old->_date == $new->_date;
173 return "Can't change charged!" unless $old->charged == $new->charged;
175 $new->SUPER::replace($old);
180 Checks all fields to make sure this is a valid invoice. If there is an error,
181 returns the error, otherwise returns false. Called by the insert and replace
190 $self->ut_numbern('invnum')
191 || $self->ut_number('custnum')
192 || $self->ut_numbern('_date')
193 || $self->ut_money('charged')
194 || $self->ut_numbern('printed')
195 || $self->ut_enum('closed', [ '', 'Y' ])
197 return $error if $error;
199 return "Unknown customer"
200 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
202 $self->_date(time) unless $self->_date;
204 $self->printed(0) if $self->printed eq '';
211 Returns a list consisting of the total previous balance for this customer,
212 followed by the previous outstanding invoices (as FS::cust_bill objects also).
219 my @cust_bill = sort { $a->_date <=> $b->_date }
220 grep { $_->owed != 0 && $_->_date < $self->_date }
221 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
223 foreach ( @cust_bill ) { $total += $_->owed; }
229 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
235 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
238 =item cust_bill_event
240 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
245 sub cust_bill_event {
247 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
253 Returns the customer (see L<FS::cust_main>) for this invoice.
259 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
264 Depreciated. See the cust_credited method.
266 #Returns a list consisting of the total previous credited (see
267 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
268 #outstanding credits (FS::cust_credit objects).
274 croak "FS::cust_bill->cust_credit depreciated; see ".
275 "FS::cust_bill->cust_credit_bill";
278 #my @cust_credit = sort { $a->_date <=> $b->_date }
279 # grep { $_->credited != 0 && $_->_date < $self->_date }
280 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
282 #foreach (@cust_credit) { $total += $_->credited; }
283 #$total, @cust_credit;
288 Depreciated. See the cust_bill_pay method.
290 #Returns all payments (see L<FS::cust_pay>) for this invoice.
296 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
298 #sort { $a->_date <=> $b->_date }
299 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
305 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
311 sort { $a->_date <=> $b->_date }
312 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
317 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
323 sort { $a->_date <=> $b->_date }
324 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
330 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
337 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
339 foreach (@taxlines) { $total += $_->setup; }
345 Returns the amount owed (still outstanding) on this invoice, which is charged
346 minus all payment applications (see L<FS::cust_bill_pay>) and credit
347 applications (see L<FS::cust_credit_bill>).
353 my $balance = $self->charged;
354 $balance -= $_->amount foreach ( $self->cust_bill_pay );
355 $balance -= $_->amount foreach ( $self->cust_credited );
356 $balance = sprintf( "%.2f", $balance);
357 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
363 Sends this invoice to the destinations configured for this customer: send
364 emails or print. See L<FS::cust_main_invoice>.
369 my($self,$template) = @_;
370 my @print_text = $self->print_text('', $template);
371 my @invoicing_list = $self->cust_main->invoicing_list;
373 if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
375 #better to notify this person than silence
376 @invoicing_list = ($invoice_from) unless @invoicing_list;
378 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
379 #$ENV{SMTPHOSTS} = $smtpmachine;
380 $ENV{MAILADDRESS} = $invoice_from;
381 my $header = new Mail::Header ( [
382 "From: $invoice_from",
383 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
384 "Sender: $invoice_from",
385 "Reply-To: $invoice_from",
386 "Date: ". time2str("%a, %d %b %Y %X %z", time),
389 my $message = new Mail::Internet (
391 'Body' => [ @print_text ], #( date)
394 $message->smtpsend( Host => $smtpmachine )
395 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
396 or return "(customer # ". $self->custnum. ") can't send invoice email".
397 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
398 " via server $smtpmachine with SMTP: $!";
402 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
404 or return "Can't open pipe to $lpr: $!";
405 print LPR @print_text;
407 or return $! ? "Error closing $lpr: $!"
408 : "Exit status $? from $lpr";
415 =item send_csv OPTIONS
417 Sends invoice as a CSV data-file to a remote host with the specified protocol.
421 protocol - currently only "ftp"
427 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
428 and YYMMDDHHMMSS is a timestamp.
430 The fields of the CSV file is as follows:
432 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
436 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
438 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
439 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
440 fields are filled in.
442 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
443 first two fields (B<record_type> and B<invnum>) and the last five fields
444 (B<pkg> through B<edate>) are filled in.
446 =item invnum - invoice number
448 =item custnum - customer number
450 =item _date - invoice date
452 =item charged - total invoice amount
454 =item first - customer first name
456 =item last - customer first name
458 =item company - company name
460 =item address1 - address line 1
462 =item address2 - address line 1
472 =item pkg - line item description
474 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
476 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
478 =item sdate - start date for recurring fee
480 =item edate - end date for recurring fee
487 my($self, %opt) = @_;
489 #part one: create file
491 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
492 mkdir $spooldir, 0700 unless -d $spooldir;
494 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
496 open(CSV, ">$file") or die "can't open $file: $!";
498 eval "use Text::CSV_XS";
501 my $csv = Text::CSV_XS->new({'always_quote'=>1});
503 my $cust_main = $self->cust_main;
509 time2str("%x", $self->_date),
510 sprintf("%.2f", $self->charged),
511 ( map { $cust_main->getfield($_) }
512 qw( first last company address1 address2 city state zip country ) ),
514 ) or die "can't create csv";
515 print CSV $csv->string. "\n";
517 #new charges (false laziness w/print_text)
518 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
520 my($pkg, $setup, $recur, $sdate, $edate);
521 if ( $cust_bill_pkg->pkgnum ) {
523 ($pkg, $setup, $recur, $sdate, $edate) = (
524 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
525 ( $cust_bill_pkg->setup != 0
526 ? sprintf("%.2f", $cust_bill_pkg->setup )
528 ( $cust_bill_pkg->recur != 0
529 ? sprintf("%.2f", $cust_bill_pkg->recur )
531 time2str("%x", $cust_bill_pkg->sdate),
532 time2str("%x", $cust_bill_pkg->edate),
536 next unless $cust_bill_pkg->setup != 0;
537 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
538 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
540 ($pkg, $setup, $recur, $sdate, $edate) =
541 ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
547 ( map { '' } (1..11) ),
548 ($pkg, $setup, $recur, $sdate, $edate)
549 ) or die "can't create csv";
550 print CSV $csv->string. "\n";
554 close CSV or die "can't close CSV: $!";
559 if ( $opt{protocol} eq 'ftp' ) {
560 eval "use Net::FTP;";
562 $net = Net::FTP->new($opt{server}) or die @$;
564 die "unknown protocol: $opt{protocol}";
567 $net->login( $opt{username}, $opt{password} )
568 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
570 $net->binary or die "can't set binary mode";
572 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
574 $net->put($file) or die "can't put $file: $!";
584 Pays this invoice with a compliemntary payment. If there is an error,
585 returns the error, otherwise returns false.
591 my $cust_pay = new FS::cust_pay ( {
592 'invnum' => $self->invnum,
593 'paid' => $self->owed,
596 'payinfo' => $self->cust_main->payinfo,
604 Attempts to pay this invoice with a credit card payment via a
605 Business::OnlinePayment realtime gateway. See
606 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
607 for supported processors.
626 Attempts to pay this invoice with an electronic check (ACH) payment via a
627 Business::OnlinePayment realtime gateway. See
628 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
629 for supported processors.
648 Attempts to pay this invoice with phone bill (LEC) payment via a
649 Business::OnlinePayment realtime gateway. See
650 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
651 for supported processors.
669 my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
671 #trim an extraneous blank line
672 pop @$options if scalar(@$options) % 2 && $options->[-1] =~ /^\s*$/;
674 my $cust_main = $self->cust_main;
675 my $amount = $self->owed;
677 my $address = $cust_main->address1;
678 $address .= ", ". $cust_main->address2 if $cust_main->address2;
680 my($payname, $payfirst, $paylast);
681 if ( $cust_main->payname && $method ne 'ECHECK' ) {
682 $payname = $cust_main->payname;
683 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
685 #$dbh->rollback if $oldAutoCommit;
686 return "Illegal payname $payname";
688 ($payfirst, $paylast) = ($1, $2);
690 $payfirst = $cust_main->getfield('first');
691 $paylast = $cust_main->getfield('last');
692 $payname = "$payfirst $paylast";
695 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
696 if ( $conf->exists('emailinvoiceauto')
697 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
698 push @invoicing_list, $cust_main->all_emails;
700 my $email = $invoicing_list[0];
702 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
704 my $description = 'Internet Services';
705 if ( $conf->exists('business-onlinepayment-description') ) {
706 my $dtempl = $conf->config('business-onlinepayment-description');
708 my $agent_obj = $cust_main->agent
709 or die "can't retreive agent for $cust_main (agentnum ".
710 $cust_main->agentnum. ")";
711 my $agent = $agent_obj->agent;
712 my $pkgs = join(', ',
713 map { $_->cust_pkg->part_pkg->pkg }
714 grep { $_->pkgnum } $self->cust_bill_pkg
716 $description = eval qq("$dtempl");
721 if ( $method eq 'CC' ) {
722 $content{card_number} = $cust_main->payinfo;
723 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
724 $content{expiration} = "$2/$1";
725 } elsif ( $method eq 'ECHECK' ) {
726 my($account_number,$routing_code) = $cust_main->payinfo;
727 ( $content{account_number}, $content{routing_code} ) =
728 split('@', $cust_main->payinfo);
729 $content{bank_name} = $cust_main->payname;
730 } elsif ( $method eq 'LEC' ) {
731 $content{phone} = $cust_main->payinfo;
735 new Business::OnlinePayment( $processor, @$options );
736 $transaction->content(
739 'password' => $password,
740 'action' => $action1,
741 'description' => $description,
743 'invoice_number' => $self->invnum,
744 'customer_id' => $self->custnum,
745 'last_name' => $paylast,
746 'first_name' => $payfirst,
748 'address' => $address,
749 'city' => $cust_main->city,
750 'state' => $cust_main->state,
751 'zip' => $cust_main->zip,
752 'country' => $cust_main->country,
753 'referer' => 'http://cleanwhisker.420.am/',
755 'phone' => $cust_main->daytime || $cust_main->night,
758 $transaction->submit();
760 if ( $transaction->is_success() && $action2 ) {
761 my $auth = $transaction->authorization;
762 my $ordernum = $transaction->can('order_number')
763 ? $transaction->order_number
766 #warn "********* $auth ***********\n";
767 #warn "********* $ordernum ***********\n";
769 new Business::OnlinePayment( $processor, @$options );
776 password => $password,
777 order_number => $ordernum,
779 authorization => $auth,
780 description => $description,
783 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
784 transaction_sequence_num local_transaction_date
785 local_transaction_time AVS_result_code )) {
786 $capture{$field} = $transaction->$field() if $transaction->can($field);
789 $capture->content( %capture );
793 unless ( $capture->is_success ) {
794 my $e = "Authorization sucessful but capture failed, invnum #".
795 $self->invnum. ': '. $capture->result_code.
796 ": ". $capture->error_message;
803 if ( $transaction->is_success() ) {
811 my $cust_pay = new FS::cust_pay ( {
812 'invnum' => $self->invnum,
815 'payby' => method2payby{$method},
816 'payinfo' => $cust_main->payinfo,
817 'paybatch' => "$processor:". $transaction->authorization,
819 my $error = $cust_pay->insert;
821 # gah, even with transactions.
822 my $e = 'WARNING: Card/ACH debited but database not updated - '.
823 'error applying payment, invnum #' . $self->invnum.
824 " ($processor): $error";
830 #} elsif ( $options{'report_badcard'} ) {
833 my $perror = "$processor error, invnum #". $self->invnum. ': '.
834 $transaction->result_code. ": ". $transaction->error_message;
836 if ( !$quiet && $conf->exists('emaildecline')
837 && grep { $_ ne 'POST' } $cust_main->invoicing_list
839 my @templ = $conf->config('declinetemplate');
840 my $template = new Text::Template (
842 SOURCE => [ map "$_\n", @templ ],
843 ) or return "($perror) can't create template: $Text::Template::ERROR";
845 or return "($perror) can't compile template: $Text::Template::ERROR";
847 my $templ_hash = { error => $transaction->error_message };
849 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
850 $ENV{MAILADDRESS} = $invoice_from;
851 my $header = new Mail::Header ( [
852 "From: $invoice_from",
853 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
854 "Sender: $invoice_from",
855 "Reply-To: $invoice_from",
856 "Date: ". time2str("%a, %d %b %Y %X %z", time),
857 "Subject: Your payment could not be processed",
859 my $message = new Mail::Internet (
861 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
864 $message->smtpsend( Host => $smtpmachine )
865 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
866 or return "($perror) (customer # ". $self->custnum.
867 ") can't send card decline email to ".
868 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
869 " via server $smtpmachine with SMTP: $!";
879 Adds a payment for this invoice to the pending credit card batch (see
880 L<FS::cust_pay_batch>).
886 my $cust_main = $self->cust_main;
888 my $cust_pay_batch = new FS::cust_pay_batch ( {
889 'invnum' => $self->getfield('invnum'),
890 'custnum' => $cust_main->getfield('custnum'),
891 'last' => $cust_main->getfield('last'),
892 'first' => $cust_main->getfield('first'),
893 'address1' => $cust_main->getfield('address1'),
894 'address2' => $cust_main->getfield('address2'),
895 'city' => $cust_main->getfield('city'),
896 'state' => $cust_main->getfield('state'),
897 'zip' => $cust_main->getfield('zip'),
898 'country' => $cust_main->getfield('country'),
900 'cardnum' => $cust_main->getfield('payinfo'),
901 'exp' => $cust_main->getfield('paydate'),
902 'payname' => $cust_main->getfield('payname'),
903 'amount' => $self->owed,
905 my $error = $cust_pay_batch->insert;
906 die $error if $error;
911 =item print_text [TIME];
913 Returns an text invoice, as a list of lines.
915 TIME an optional value used to control the printing of overdue messages. The
916 default is now. It isn't the date of the invoice; that's the `_date' field.
917 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
918 L<Time::Local> and L<Date::Parse> for conversion functions.
924 my( $self, $today, $template ) = @_;
926 # my $invnum = $self->invnum;
927 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
928 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
929 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
931 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
932 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
933 #my $balance_due = $self->owed + $pr_total - $cr_total;
934 my $balance_due = $self->owed + $pr_total;
937 #my($description,$amount);
941 foreach ( @pr_cust_bill ) {
943 "Previous Balance, Invoice #". $_->invnum.
944 " (". time2str("%x",$_->_date). ")",
945 $money_char. sprintf("%10.2f",$_->owed)
949 push @buf,['','-----------'];
950 push @buf,[ 'Total Previous Balance',
951 $money_char. sprintf("%10.2f",$pr_total ) ];
956 foreach my $cust_bill_pkg (
957 ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first
958 ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes
961 if ( $cust_bill_pkg->pkgnum ) {
963 my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
964 my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
965 my $pkg = $part_pkg->pkg;
967 if ( $cust_bill_pkg->setup != 0 ) {
968 push @buf, [ "$pkg Setup",
969 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
971 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
974 if ( $cust_bill_pkg->recur != 0 ) {
976 "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
977 time2str("%x", $cust_bill_pkg->edate) . ")",
978 $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
981 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
984 push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details;
987 my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
988 ? ( $cust_bill_pkg->itemdesc || 'Tax' )
990 push @buf, [ $itemdesc,
991 $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ]
992 if $cust_bill_pkg->setup != 0;
996 push @buf,['','-----------'];
997 push @buf,['Total New Charges',
998 $money_char. sprintf("%10.2f",$self->charged) ];
1001 push @buf,['','-----------'];
1002 push @buf,['Total Charges',
1003 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1007 foreach ( $self->cust_credited ) {
1009 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1011 my $reason = substr($_->cust_credit->reason,0,32);
1012 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1013 $reason = " ($reason) " if $reason;
1015 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1017 $money_char. sprintf("%10.2f",$_->amount)
1020 #foreach ( @cr_cust_credit ) {
1022 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1023 # $money_char. sprintf("%10.2f",$_->credited)
1027 #get & print payments
1028 foreach ( $self->cust_bill_pay ) {
1030 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1033 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1034 $money_char. sprintf("%10.2f",$_->amount )
1039 push @buf,['','-----------'];
1040 push @buf,['Balance Due', $money_char.
1041 sprintf("%10.2f", $balance_due ) ];
1043 #create the template
1044 my $templatefile = 'invoice_template';
1045 $templatefile .= "_$template" if $template;
1046 my @invoice_template = $conf->config($templatefile)
1047 or die "cannot load config file $templatefile";
1050 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1051 /invoice_lines\((\d*)\)/;
1052 $invoice_lines += $1 || scalar(@buf);
1055 die "no invoice_lines() functions in template?" unless $wasfunc;
1056 my $invoice_template = new Text::Template (
1058 SOURCE => [ map "$_\n", @invoice_template ],
1059 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1060 $invoice_template->compile()
1061 or die "can't compile template: $Text::Template::ERROR";
1063 #setup template variables
1064 package FS::cust_bill::_template; #!
1065 use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1067 $invnum = $self->invnum;
1068 $date = $self->_date;
1070 $agent = $self->cust_main->agent->agent;
1072 if ( $FS::cust_bill::invoice_lines ) {
1074 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1076 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1081 #format address (variable for the template)
1083 @address = ( '', '', '', '', '', '' );
1084 package FS::cust_bill; #!
1085 $FS::cust_bill::_template::address[$l++] =
1086 $cust_main->payname.
1087 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1088 ? " (P.O. #". $cust_main->payinfo. ")"
1092 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1093 if $cust_main->company;
1094 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1095 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1096 if $cust_main->address2;
1097 $FS::cust_bill::_template::address[$l++] =
1098 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1099 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1100 unless $cust_main->country eq 'US';
1102 # #overdue? (variable for the template)
1103 # $FS::cust_bill::_template::overdue = (
1105 # && $today > $self->_date
1106 ## && $self->printed > 1
1107 # && $self->printed > 0
1110 #and subroutine for the template
1111 sub FS::cust_bill::_template::invoice_lines {
1112 my $lines = shift || scalar(@buf);
1114 scalar(@buf) ? shift @buf : [ '', '' ];
1120 $FS::cust_bill::_template::page = 1;
1124 push @collect, split("\n",
1125 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1127 $FS::cust_bill::_template::page++;
1130 map "$_\n", @collect;
1140 print_text formatting (and some logic :/) is in source, but needs to be
1141 slurped in from a file. Also number of lines ($=).
1143 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1144 or something similar so the look can be completely customized?)
1148 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1149 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base