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('cybercash3.2') ) {
40 #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
41 require CCMckDirectLib3_2;
43 require CCMckErrno3_2;
44 #qw(MCKGetErrorMessage $E_NoErr);
45 import CCMckErrno3_2 qw($E_NoErr);
48 ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
49 my $status = &CCMckLib3_2::InitConfig($merchant_conf);
50 if ( $status != $E_NoErr ) {
51 warn "CCMckLib3_2::InitConfig error:\n";
52 foreach my $key (keys %CCMckLib3_2::Config) {
53 warn " $key => $CCMckLib3_2::Config{$key}\n"
55 my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
56 die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
58 $processor='cybercash3.2';
59 } elsif ( $conf->exists('business-onlinepayment') ) {
65 ) = $conf->config('business-onlinepayment');
66 $bop_action ||= 'normal authorization';
67 eval "use Business::OnlinePayment";
68 $processor="Business::OnlinePayment::$bop_processor";
75 FS::cust_bill - Object methods for cust_bill records
81 $record = new FS::cust_bill \%hash;
82 $record = new FS::cust_bill { 'column' => 'value' };
84 $error = $record->insert;
86 $error = $new_record->replace($old_record);
88 $error = $record->delete;
90 $error = $record->check;
92 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
94 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
96 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
98 @cust_pay_objects = $cust_bill->cust_pay;
100 $tax_amount = $record->tax;
102 @lines = $cust_bill->print_text;
103 @lines = $cust_bill->print_text $time;
107 An FS::cust_bill object represents an invoice; a declaration that a customer
108 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
109 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
110 following fields are currently supported:
114 =item invnum - primary key (assigned automatically for new invoices)
116 =item custnum - customer (see L<FS::cust_main>)
118 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
119 L<Time::Local> and L<Date::Parse> for conversion functions.
121 =item charged - amount of this invoice
123 =item printed - deprecated
125 =item closed - books closed flag, empty or `Y'
135 Creates a new invoice. To add the invoice to the database, see L<"insert">.
136 Invoices are normally created by calling the bill method of a customer object
137 (see L<FS::cust_main>).
141 sub table { 'cust_bill'; }
145 Adds this invoice to the database ("Posts" the invoice). If there is an error,
146 returns the error, otherwise returns false.
150 Currently unimplemented. I don't remove invoices because there would then be
151 no record you ever posted this invoice (which is bad, no?)
157 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
158 $self->SUPER::delete(@_);
161 =item replace OLD_RECORD
163 Replaces the OLD_RECORD with this one in the database. If there is an error,
164 returns the error, otherwise returns false.
166 Only printed may be changed. printed is normally updated by calling the
167 collect method of a customer object (see L<FS::cust_main>).
172 my( $new, $old ) = ( shift, shift );
173 return "Can't change custnum!" unless $old->custnum == $new->custnum;
174 #return "Can't change _date!" unless $old->_date eq $new->_date;
175 return "Can't change _date!" unless $old->_date == $new->_date;
176 return "Can't change charged!" unless $old->charged == $new->charged;
178 $new->SUPER::replace($old);
183 Checks all fields to make sure this is a valid invoice. If there is an error,
184 returns the error, otherwise returns false. Called by the insert and replace
193 $self->ut_numbern('invnum')
194 || $self->ut_number('custnum')
195 || $self->ut_numbern('_date')
196 || $self->ut_money('charged')
197 || $self->ut_numbern('printed')
198 || $self->ut_enum('closed', [ '', 'Y' ])
200 return $error if $error;
202 return "Unknown customer"
203 unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
205 $self->_date(time) unless $self->_date;
207 $self->printed(0) if $self->printed eq '';
214 Returns a list consisting of the total previous balance for this customer,
215 followed by the previous outstanding invoices (as FS::cust_bill objects also).
222 my @cust_bill = sort { $a->_date <=> $b->_date }
223 grep { $_->owed != 0 && $_->_date < $self->_date }
224 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
226 foreach ( @cust_bill ) { $total += $_->owed; }
232 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
238 qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
241 =item cust_bill_event
243 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
248 sub cust_bill_event {
250 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
256 Returns the customer (see L<FS::cust_main>) for this invoice.
262 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
267 Depreciated. See the cust_credited method.
269 #Returns a list consisting of the total previous credited (see
270 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
271 #outstanding credits (FS::cust_credit objects).
277 croak "FS::cust_bill->cust_credit depreciated; see ".
278 "FS::cust_bill->cust_credit_bill";
281 #my @cust_credit = sort { $a->_date <=> $b->_date }
282 # grep { $_->credited != 0 && $_->_date < $self->_date }
283 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
285 #foreach (@cust_credit) { $total += $_->credited; }
286 #$total, @cust_credit;
291 Depreciated. See the cust_bill_pay method.
293 #Returns all payments (see L<FS::cust_pay>) for this invoice.
299 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
301 #sort { $a->_date <=> $b->_date }
302 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
308 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
314 sort { $a->_date <=> $b->_date }
315 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
320 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
326 sort { $a->_date <=> $b->_date }
327 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
333 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
340 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
342 foreach (@taxlines) { $total += $_->setup; }
348 Returns the amount owed (still outstanding) on this invoice, which is charged
349 minus all payment applications (see L<FS::cust_bill_pay>) and credit
350 applications (see L<FS::cust_credit_bill>).
356 my $balance = $self->charged;
357 $balance -= $_->amount foreach ( $self->cust_bill_pay );
358 $balance -= $_->amount foreach ( $self->cust_credited );
359 $balance = sprintf( "%.2f", $balance);
360 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
366 Sends this invoice to the destinations configured for this customer: send
367 emails or print. See L<FS::cust_main_invoice>.
372 my($self,$template) = @_;
373 my @print_text = $self->print_text('', $template);
374 my @invoicing_list = $self->cust_main->invoicing_list;
376 if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
377 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
378 #$ENV{SMTPHOSTS} = $smtpmachine;
379 $ENV{MAILADDRESS} = $invoice_from;
380 my $header = new Mail::Header ( [
381 "From: $invoice_from",
382 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
383 "Sender: $invoice_from",
384 "Reply-To: $invoice_from",
385 "Date: ". time2str("%a, %d %b %Y %X %z", time),
388 my $message = new Mail::Internet (
390 'Body' => [ @print_text ], #( date)
393 $message->smtpsend( Host => $smtpmachine )
394 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
395 or return "(customer # ". $self->custnum. ") can't send invoice email".
396 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
397 " via server $smtpmachine with SMTP: $!";
401 if ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) { #postal
403 or return "Can't open pipe to $lpr: $!";
404 print LPR @print_text;
406 or return $! ? "Error closing $lpr: $!"
407 : "Exit status $? from $lpr";
414 =item send_csv OPTIONS
416 Sends invoice as a CSV data-file to a remote host with the specified protocol.
420 protocol - currently only "ftp"
426 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
427 and YYMMDDHHMMSS is a timestamp.
429 The fields of the CSV file is as follows:
431 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
435 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
437 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
438 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
439 fields are filled in.
441 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
442 first two fields (B<record_type> and B<invnum>) and the last five fields
443 (B<pkg> through B<edate>) are filled in.
445 =item invnum - invoice number
446 =item custnum - customer number
447 =item _date - invoice date
448 =item charged - total invoice amount
449 =item first - customer first name
450 =item last - customer first name
451 =item company - company name
452 =item address1 - address line 1
453 =item address2 - address line 1
459 =item pkg - line item description
460 =item setup - line item setup fee (only or both of B<setup> and B<recur> will be defined)
461 =item recur - line item recurring fee (only or both of B<setup> and B<recur> will be defined)
462 =item sdate - start date for recurring fee
463 =item edate - end date for recurring fee
470 my($self, %opt) = @_;
472 #part one: create file
474 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
475 mkdir $spooldir, 0700 unless -d $spooldir;
477 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
479 open(CSV, ">$file") or die "can't open $file: $!";
481 eval "use Text::CSV_XS";
484 my $csv = Text::CSV_XS->new({'always_quote'=>1});
486 my $cust_main = $self->cust_main;
492 time2str("%x", $self->_date),
493 sprintf("%.2f", $self->charged),
494 ( map { $cust_main->getfield($_) }
495 qw( first last company address1 address2 city state zip country ) ),
497 ) or die "can't create csv";
498 print CSV $csv->string. "\n";
500 #new charges (false laziness w/print_text)
501 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
503 my($pkg, $setup, $recur, $sdate, $edate);
504 if ( $cust_bill_pkg->pkgnum ) {
506 ($pkg, $setup, $recur, $sdate, $edate) = (
507 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
508 ( $cust_bill_pkg->setup != 0
509 ? sprintf("%.2f", $cust_bill_pkg->setup )
511 ( $cust_bill_pkg->recur != 0
512 ? sprintf("%.2f", $cust_bill_pkg->recur )
514 time2str("%x", $cust_bill_pkg->sdate),
515 time2str("%x", $cust_bill_pkg->edate),
519 next unless $cust_bill_pkg->setup != 0;
520 ($pkg, $setup, $recur, $sdate, $edate) =
521 ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
527 ( map { '' } (1..11) ),
528 ($pkg, $setup, $recur, $sdate, $edate)
529 ) or die "can't create csv";
530 print CSV $csv->string. "\n";
534 close CSV or die "can't close CSV: $!";
539 if ( $opt{protocol} eq 'ftp' ) {
540 eval "use Net::FTP;";
542 $net = Net::FTP->new($opt{server}) or die @$;
544 die "unknown protocol: $opt{protocol}";
547 $net->login( $opt{username}, $opt{password} )
548 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
550 $net->binary or die "can't set binary mode";
552 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
554 $net->put($file) or die "can't put $file: $!";
564 Pays this invoice with a compliemntary payment. If there is an error,
565 returns the error, otherwise returns false.
571 my $cust_pay = new FS::cust_pay ( {
572 'invnum' => $self->invnum,
573 'paid' => $self->owed,
576 'payinfo' => $self->cust_main->payinfo,
584 Attempts to pay this invoice with a Business::OnlinePayment realtime gateway.
585 See http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
586 for supproted processors.
592 my $cust_main = $self->cust_main;
593 my $amount = $self->owed;
595 unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
596 return "Real-time card processing not enabled (processor $processor)";
598 my $bop_processor = $1; #hmm?
600 my $address = $cust_main->address1;
601 $address .= ", ". $cust_main->address2 if $cust_main->address2;
604 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
605 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
608 my($payname, $payfirst, $paylast);
609 if ( $cust_main->payname ) {
610 $payname = $cust_main->payname;
611 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
613 #$dbh->rollback if $oldAutoCommit;
614 return "Illegal payname $payname";
616 ($payfirst, $paylast) = ($1, $2);
618 $payfirst = $cust_main->getfield('first');
619 $paylast = $cust_main->getfield('last');
620 $payname = "$payfirst $paylast";
623 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
624 if ( $conf->exists('emailinvoiceauto')
625 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
626 push @invoicing_list, $cust_main->all_emails;
628 my $email = $invoicing_list[0];
630 my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
632 my $description = 'Internet Services';
633 if ( $conf->exists('business-onlinepayment-description') ) {
634 my $dtempl = $conf->config('business-onlinepayment-description');
636 my $agent_obj = $cust_main->agent
637 or die "can't retreive agent for $cust_main (agentnum ".
638 $cust_main->agentnum. ")";
639 my $agent = $agent_obj->agent;
640 my $pkgs = join(', ',
641 map { $_->cust_pkg->part_pkg->pkg }
642 grep { $_->pkgnum } $self->cust_bill_pkg
644 $description = eval qq("$dtempl");
649 new Business::OnlinePayment( $bop_processor, @bop_options );
650 $transaction->content(
652 'login' => $bop_login,
653 'password' => $bop_password,
654 'action' => $action1,
655 'description' => $description,
657 'invoice_number' => $self->invnum,
658 'customer_id' => $self->custnum,
659 'last_name' => $paylast,
660 'first_name' => $payfirst,
662 'address' => $address,
663 'city' => $cust_main->city,
664 'state' => $cust_main->state,
665 'zip' => $cust_main->zip,
666 'country' => $cust_main->country,
667 'card_number' => $cust_main->payinfo,
668 'expiration' => $exp,
669 'referer' => 'http://cleanwhisker.420.am/',
671 'phone' => $cust_main->daytime || $cust_main->night,
673 $transaction->submit();
675 if ( $transaction->is_success() && $action2 ) {
676 my $auth = $transaction->authorization;
677 my $ordernum = $transaction->can('order_number')
678 ? $transaction->order_number
681 #warn "********* $auth ***********\n";
682 #warn "********* $ordernum ***********\n";
684 new Business::OnlinePayment( $bop_processor, @bop_options );
690 password => $bop_password,
691 order_number => $ordernum,
693 authorization => $auth,
694 description => $description,
695 card_number => $cust_main->payinfo,
699 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
700 transaction_sequence_num local_transaction_date
701 local_transaction_time AVS_result_code )) {
702 $capture{$field} = $transaction->$field() if $transaction->can($field);
705 $capture->content( %capture );
709 unless ( $capture->is_success ) {
710 my $e = "Authorization sucessful but capture failed, invnum #".
711 $self->invnum. ': '. $capture->result_code.
712 ": ". $capture->error_message;
719 if ( $transaction->is_success() ) {
721 my $cust_pay = new FS::cust_pay ( {
722 'invnum' => $self->invnum,
726 'payinfo' => $cust_main->payinfo,
727 'paybatch' => "$processor:". $transaction->authorization,
729 my $error = $cust_pay->insert;
731 # gah, even with transactions.
732 my $e = 'WARNING: Card debited but database not updated - '.
733 'error applying payment, invnum #' . $self->invnum.
734 " ($processor): $error";
740 #} elsif ( $options{'report_badcard'} ) {
743 my $perror = "$processor error, invnum #". $self->invnum. ': '.
744 $transaction->result_code. ": ". $transaction->error_message;
746 if ( $conf->exists('emaildecline')
747 && grep { $_ ne 'POST' } $cust_main->invoicing_list
749 my @templ = $conf->config('declinetemplate');
750 my $template = new Text::Template (
752 SOURCE => [ map "$_\n", @templ ],
753 ) or return "($perror) can't create template: $Text::Template::ERROR";
755 or return "($perror) can't compile template: $Text::Template::ERROR";
757 my $templ_hash = { error => $transaction->error_message };
759 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
760 $ENV{MAILADDRESS} = $invoice_from;
761 my $header = new Mail::Header ( [
762 "From: $invoice_from",
763 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
764 "Sender: $invoice_from",
765 "Reply-To: $invoice_from",
766 "Date: ". time2str("%a, %d %b %Y %X %z", time),
767 "Subject: Your credit card could not be processed",
769 my $message = new Mail::Internet (
771 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
774 $message->smtpsend( Host => $smtpmachine )
775 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
776 or return "($perror) (customer # ". $self->custnum.
777 ") can't send card decline email to ".
778 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
779 " via server $smtpmachine with SMTP: $!";
787 =item realtime_card_cybercash
789 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
793 sub realtime_card_cybercash {
795 my $cust_main = $self->cust_main;
796 my $amount = $self->owed;
798 return "CyberCash CashRegister real-time card processing not enabled!"
799 unless $processor eq 'cybercash3.2';
801 my $address = $cust_main->address1;
802 $address .= ", ". $cust_main->address2 if $cust_main->address2;
805 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
806 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
811 my $paybatch = $self->invnum.
812 '-' . time2str("%y%m%d%H%M%S", time);
814 my $payname = $cust_main->payname ||
815 $cust_main->getfield('first').' '.$cust_main->getfield('last');
817 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
819 my @full_xaction = ( $xaction,
820 'Order-ID' => $paybatch,
821 'Amount' => "usd $amount",
822 'Card-Number' => $cust_main->getfield('payinfo'),
823 'Card-Name' => $payname,
824 'Card-Address' => $address,
825 'Card-City' => $cust_main->getfield('city'),
826 'Card-State' => $cust_main->getfield('state'),
827 'Card-Zip' => $cust_main->getfield('zip'),
828 'Card-Country' => $country,
833 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
835 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
836 my $cust_pay = new FS::cust_pay ( {
837 'invnum' => $self->invnum,
841 'payinfo' => $cust_main->payinfo,
842 'paybatch' => "$processor:$paybatch",
844 my $error = $cust_pay->insert;
846 # gah, even with transactions.
847 my $e = 'WARNING: Card debited but database not updated - '.
848 'error applying payment, invnum #' . $self->invnum.
849 " (CyberCash Order-ID $paybatch): $error";
855 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
856 # || $options{'report_badcard'}
859 return 'Cybercash error, invnum #' .
860 $self->invnum. ':'. $result{'MErrMsg'};
867 Adds a payment for this invoice to the pending credit card batch (see
868 L<FS::cust_pay_batch>).
874 my $cust_main = $self->cust_main;
876 my $cust_pay_batch = new FS::cust_pay_batch ( {
877 'invnum' => $self->getfield('invnum'),
878 'custnum' => $cust_main->getfield('custnum'),
879 'last' => $cust_main->getfield('last'),
880 'first' => $cust_main->getfield('first'),
881 'address1' => $cust_main->getfield('address1'),
882 'address2' => $cust_main->getfield('address2'),
883 'city' => $cust_main->getfield('city'),
884 'state' => $cust_main->getfield('state'),
885 'zip' => $cust_main->getfield('zip'),
886 'country' => $cust_main->getfield('country'),
888 'cardnum' => $cust_main->getfield('payinfo'),
889 'exp' => $cust_main->getfield('paydate'),
890 'payname' => $cust_main->getfield('payname'),
891 'amount' => $self->owed,
893 my $error = $cust_pay_batch->insert;
894 die $error if $error;
899 =item print_text [TIME];
901 Returns an text invoice, as a list of lines.
903 TIME an optional value used to control the printing of overdue messages. The
904 default is now. It isn't the date of the invoice; that's the `_date' field.
905 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
906 L<Time::Local> and L<Date::Parse> for conversion functions.
912 my( $self, $today, $template ) = @_;
914 # my $invnum = $self->invnum;
915 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
916 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
917 unless $cust_main->payname;
919 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
920 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
921 #my $balance_due = $self->owed + $pr_total - $cr_total;
922 my $balance_due = $self->owed + $pr_total;
925 #my($description,$amount);
929 foreach ( @pr_cust_bill ) {
931 "Previous Balance, Invoice #". $_->invnum.
932 " (". time2str("%x",$_->_date). ")",
933 $money_char. sprintf("%10.2f",$_->owed)
937 push @buf,['','-----------'];
938 push @buf,[ 'Total Previous Balance',
939 $money_char. sprintf("%10.2f",$pr_total ) ];
944 foreach ( $self->cust_bill_pkg ) {
948 my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
949 my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
950 my($pkg)=$part_pkg->pkg;
952 if ( $_->setup != 0 ) {
953 push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
955 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
958 if ( $_->recur != 0 ) {
960 "$pkg (" . time2str("%x",$_->sdate) . " - " .
961 time2str("%x",$_->edate) . ")",
962 $money_char. sprintf("%10.2f",$_->recur)
965 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
969 push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ]
974 push @buf,['','-----------'];
975 push @buf,['Total New Charges',
976 $money_char. sprintf("%10.2f",$self->charged) ];
979 push @buf,['','-----------'];
980 push @buf,['Total Charges',
981 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
985 foreach ( $self->cust_credited ) {
987 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
989 my $reason = substr($_->cust_credit->reason,0,32);
990 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
991 $reason = " ($reason) " if $reason;
993 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
995 $money_char. sprintf("%10.2f",$_->amount)
998 #foreach ( @cr_cust_credit ) {
1000 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1001 # $money_char. sprintf("%10.2f",$_->credited)
1005 #get & print payments
1006 foreach ( $self->cust_bill_pay ) {
1008 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1011 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1012 $money_char. sprintf("%10.2f",$_->amount )
1017 push @buf,['','-----------'];
1018 push @buf,['Balance Due', $money_char.
1019 sprintf("%10.2f", $balance_due ) ];
1021 #create the template
1022 my $templatefile = 'invoice_template';
1023 $templatefile .= "_$template" if $template;
1024 my @invoice_template = $conf->config($templatefile)
1025 or die "cannot load config file $templatefile";
1028 foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
1029 /invoice_lines\((\d+)\)/;
1030 $invoice_lines += $1;
1033 die "no invoice_lines() functions in template?" unless $wasfunc;
1034 my $invoice_template = new Text::Template (
1036 SOURCE => [ map "$_\n", @invoice_template ],
1037 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1038 $invoice_template->compile()
1039 or die "can't compile template: $Text::Template::ERROR";
1041 #setup template variables
1042 package FS::cust_bill::_template; #!
1043 use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
1045 $invnum = $self->invnum;
1046 $date = $self->_date;
1049 if ( $FS::cust_bill::invoice_lines ) {
1051 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1053 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1058 #format address (variable for the template)
1060 @address = ( '', '', '', '', '', '' );
1061 package FS::cust_bill; #!
1062 $FS::cust_bill::_template::address[$l++] =
1063 $cust_main->payname.
1064 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1065 ? " (P.O. #". $cust_main->payinfo. ")"
1069 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1070 if $cust_main->company;
1071 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1072 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1073 if $cust_main->address2;
1074 $FS::cust_bill::_template::address[$l++] =
1075 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1076 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1077 unless $cust_main->country eq 'US';
1079 # #overdue? (variable for the template)
1080 # $FS::cust_bill::_template::overdue = (
1082 # && $today > $self->_date
1083 ## && $self->printed > 1
1084 # && $self->printed > 0
1087 #and subroutine for the template
1089 sub FS::cust_bill::_template::invoice_lines {
1090 my $lines = shift or return @buf;
1092 scalar(@buf) ? shift @buf : [ '', '' ];
1099 $FS::cust_bill::_template::page = 1;
1103 push @collect, split("\n",
1104 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1106 $FS::cust_bill::_template::page++;
1109 map "$_\n", @collect;
1117 $Id: cust_bill.pm,v 1.41.2.2 2002-09-17 00:33:14 ivan Exp $
1123 print_text formatting (and some logic :/) is in source, but needs to be
1124 slurped in from a file. Also number of lines ($=).
1126 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1127 or something similar so the look can be completely customized?)
1131 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1132 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base