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 or !@invoicing_list ) { #email
378 #better to notify this person than silence
379 @invoicing_list = ($invoice_from) unless @invoicing_list;
381 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
382 #$ENV{SMTPHOSTS} = $smtpmachine;
383 $ENV{MAILADDRESS} = $invoice_from;
384 my $header = new Mail::Header ( [
385 "From: $invoice_from",
386 "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
387 "Sender: $invoice_from",
388 "Reply-To: $invoice_from",
389 "Date: ". time2str("%a, %d %b %Y %X %z", time),
392 my $message = new Mail::Internet (
394 'Body' => [ @print_text ], #( date)
397 $message->smtpsend( Host => $smtpmachine )
398 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
399 or return "(customer # ". $self->custnum. ") can't send invoice email".
400 " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
401 " via server $smtpmachine with SMTP: $!";
405 if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
407 or return "Can't open pipe to $lpr: $!";
408 print LPR @print_text;
410 or return $! ? "Error closing $lpr: $!"
411 : "Exit status $? from $lpr";
418 =item send_csv OPTIONS
420 Sends invoice as a CSV data-file to a remote host with the specified protocol.
424 protocol - currently only "ftp"
430 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
431 and YYMMDDHHMMSS is a timestamp.
433 The fields of the CSV file is as follows:
435 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
439 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
441 If B<record_type> is C<cust_bill>, this is a primary invoice record. The
442 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
443 fields are filled in.
445 If B<record_type> is C<cust_bill_pkg>, this is a line item record. Only the
446 first two fields (B<record_type> and B<invnum>) and the last five fields
447 (B<pkg> through B<edate>) are filled in.
449 =item invnum - invoice number
451 =item custnum - customer number
453 =item _date - invoice date
455 =item charged - total invoice amount
457 =item first - customer first name
459 =item last - customer first name
461 =item company - company name
463 =item address1 - address line 1
465 =item address2 - address line 1
475 =item pkg - line item description
477 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
479 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
481 =item sdate - start date for recurring fee
483 =item edate - end date for recurring fee
490 my($self, %opt) = @_;
492 #part one: create file
494 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
495 mkdir $spooldir, 0700 unless -d $spooldir;
497 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
499 open(CSV, ">$file") or die "can't open $file: $!";
501 eval "use Text::CSV_XS";
504 my $csv = Text::CSV_XS->new({'always_quote'=>1});
506 my $cust_main = $self->cust_main;
512 time2str("%x", $self->_date),
513 sprintf("%.2f", $self->charged),
514 ( map { $cust_main->getfield($_) }
515 qw( first last company address1 address2 city state zip country ) ),
517 ) or die "can't create csv";
518 print CSV $csv->string. "\n";
520 #new charges (false laziness w/print_text)
521 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
523 my($pkg, $setup, $recur, $sdate, $edate);
524 if ( $cust_bill_pkg->pkgnum ) {
526 ($pkg, $setup, $recur, $sdate, $edate) = (
527 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
528 ( $cust_bill_pkg->setup != 0
529 ? sprintf("%.2f", $cust_bill_pkg->setup )
531 ( $cust_bill_pkg->recur != 0
532 ? sprintf("%.2f", $cust_bill_pkg->recur )
534 time2str("%x", $cust_bill_pkg->sdate),
535 time2str("%x", $cust_bill_pkg->edate),
539 next unless $cust_bill_pkg->setup != 0;
540 ($pkg, $setup, $recur, $sdate, $edate) =
541 ( 'Tax', 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 Business::OnlinePayment realtime gateway.
605 See http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
606 for supproted processors.
612 my $cust_main = $self->cust_main;
613 my $amount = $self->owed;
615 unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
616 return "Real-time card processing not enabled (processor $processor)";
618 my $bop_processor = $1; #hmm?
620 my $address = $cust_main->address1;
621 $address .= ", ". $cust_main->address2 if $cust_main->address2;
624 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
625 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
628 my($payname, $payfirst, $paylast);
629 if ( $cust_main->payname ) {
630 $payname = $cust_main->payname;
631 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
633 #$dbh->rollback if $oldAutoCommit;
634 return "Illegal payname $payname";
636 ($payfirst, $paylast) = ($1, $2);
638 $payfirst = $cust_main->getfield('first');
639 $paylast = $cust_main->getfield('last');
640 $payname = "$payfirst $paylast";
643 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
644 if ( $conf->exists('emailinvoiceauto')
645 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
646 push @invoicing_list, $cust_main->all_emails;
648 my $email = $invoicing_list[0];
650 my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
652 my $description = 'Internet Services';
653 if ( $conf->exists('business-onlinepayment-description') ) {
654 my $dtempl = $conf->config('business-onlinepayment-description');
656 my $agent_obj = $cust_main->agent
657 or die "can't retreive agent for $cust_main (agentnum ".
658 $cust_main->agentnum. ")";
659 my $agent = $agent_obj->agent;
660 my $pkgs = join(', ',
661 map { $_->cust_pkg->part_pkg->pkg }
662 grep { $_->pkgnum } $self->cust_bill_pkg
664 $description = eval qq("$dtempl");
669 new Business::OnlinePayment( $bop_processor, @bop_options );
670 $transaction->content(
672 'login' => $bop_login,
673 'password' => $bop_password,
674 'action' => $action1,
675 'description' => $description,
677 'invoice_number' => $self->invnum,
678 'customer_id' => $self->custnum,
679 'last_name' => $paylast,
680 'first_name' => $payfirst,
682 'address' => $address,
683 'city' => $cust_main->city,
684 'state' => $cust_main->state,
685 'zip' => $cust_main->zip,
686 'country' => $cust_main->country,
687 'card_number' => $cust_main->payinfo,
688 'expiration' => $exp,
689 'referer' => 'http://cleanwhisker.420.am/',
691 'phone' => $cust_main->daytime || $cust_main->night,
693 $transaction->submit();
695 if ( $transaction->is_success() && $action2 ) {
696 my $auth = $transaction->authorization;
697 my $ordernum = $transaction->can('order_number')
698 ? $transaction->order_number
701 #warn "********* $auth ***********\n";
702 #warn "********* $ordernum ***********\n";
704 new Business::OnlinePayment( $bop_processor, @bop_options );
710 password => $bop_password,
711 order_number => $ordernum,
713 authorization => $auth,
714 description => $description,
715 card_number => $cust_main->payinfo,
719 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
720 transaction_sequence_num local_transaction_date
721 local_transaction_time AVS_result_code )) {
722 $capture{$field} = $transaction->$field() if $transaction->can($field);
725 $capture->content( %capture );
729 unless ( $capture->is_success ) {
730 my $e = "Authorization sucessful but capture failed, invnum #".
731 $self->invnum. ': '. $capture->result_code.
732 ": ". $capture->error_message;
739 if ( $transaction->is_success() ) {
741 my $cust_pay = new FS::cust_pay ( {
742 'invnum' => $self->invnum,
746 'payinfo' => $cust_main->payinfo,
747 'paybatch' => "$processor:". $transaction->authorization,
749 my $error = $cust_pay->insert;
751 # gah, even with transactions.
752 my $e = 'WARNING: Card debited but database not updated - '.
753 'error applying payment, invnum #' . $self->invnum.
754 " ($processor): $error";
760 #} elsif ( $options{'report_badcard'} ) {
763 my $perror = "$processor error, invnum #". $self->invnum. ': '.
764 $transaction->result_code. ": ". $transaction->error_message;
766 if ( $conf->exists('emaildecline')
767 && grep { $_ ne 'POST' } $cust_main->invoicing_list
769 my @templ = $conf->config('declinetemplate');
770 my $template = new Text::Template (
772 SOURCE => [ map "$_\n", @templ ],
773 ) or return "($perror) can't create template: $Text::Template::ERROR";
775 or return "($perror) can't compile template: $Text::Template::ERROR";
777 my $templ_hash = { error => $transaction->error_message };
779 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
780 $ENV{MAILADDRESS} = $invoice_from;
781 my $header = new Mail::Header ( [
782 "From: $invoice_from",
783 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
784 "Sender: $invoice_from",
785 "Reply-To: $invoice_from",
786 "Date: ". time2str("%a, %d %b %Y %X %z", time),
787 "Subject: Your credit card could not be processed",
789 my $message = new Mail::Internet (
791 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
794 $message->smtpsend( Host => $smtpmachine )
795 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
796 or return "($perror) (customer # ". $self->custnum.
797 ") can't send card decline email to ".
798 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
799 " via server $smtpmachine with SMTP: $!";
807 =item realtime_card_cybercash
809 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
813 sub realtime_card_cybercash {
815 my $cust_main = $self->cust_main;
816 my $amount = $self->owed;
818 return "CyberCash CashRegister real-time card processing not enabled!"
819 unless $processor eq 'cybercash3.2';
821 my $address = $cust_main->address1;
822 $address .= ", ". $cust_main->address2 if $cust_main->address2;
825 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
826 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
831 my $paybatch = $self->invnum.
832 '-' . time2str("%y%m%d%H%M%S", time);
834 my $payname = $cust_main->payname ||
835 $cust_main->getfield('first').' '.$cust_main->getfield('last');
837 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
839 my @full_xaction = ( $xaction,
840 'Order-ID' => $paybatch,
841 'Amount' => "usd $amount",
842 'Card-Number' => $cust_main->getfield('payinfo'),
843 'Card-Name' => $payname,
844 'Card-Address' => $address,
845 'Card-City' => $cust_main->getfield('city'),
846 'Card-State' => $cust_main->getfield('state'),
847 'Card-Zip' => $cust_main->getfield('zip'),
848 'Card-Country' => $country,
853 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
855 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
856 my $cust_pay = new FS::cust_pay ( {
857 'invnum' => $self->invnum,
861 'payinfo' => $cust_main->payinfo,
862 'paybatch' => "$processor:$paybatch",
864 my $error = $cust_pay->insert;
866 # gah, even with transactions.
867 my $e = 'WARNING: Card debited but database not updated - '.
868 'error applying payment, invnum #' . $self->invnum.
869 " (CyberCash Order-ID $paybatch): $error";
875 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
876 # || $options{'report_badcard'}
879 return 'Cybercash error, invnum #' .
880 $self->invnum. ':'. $result{'MErrMsg'};
887 Adds a payment for this invoice to the pending credit card batch (see
888 L<FS::cust_pay_batch>).
894 my $cust_main = $self->cust_main;
896 my $cust_pay_batch = new FS::cust_pay_batch ( {
897 'invnum' => $self->getfield('invnum'),
898 'custnum' => $cust_main->getfield('custnum'),
899 'last' => $cust_main->getfield('last'),
900 'first' => $cust_main->getfield('first'),
901 'address1' => $cust_main->getfield('address1'),
902 'address2' => $cust_main->getfield('address2'),
903 'city' => $cust_main->getfield('city'),
904 'state' => $cust_main->getfield('state'),
905 'zip' => $cust_main->getfield('zip'),
906 'country' => $cust_main->getfield('country'),
908 'cardnum' => $cust_main->getfield('payinfo'),
909 'exp' => $cust_main->getfield('paydate'),
910 'payname' => $cust_main->getfield('payname'),
911 'amount' => $self->owed,
913 my $error = $cust_pay_batch->insert;
914 die $error if $error;
919 =item print_text [TIME];
921 Returns an text invoice, as a list of lines.
923 TIME an optional value used to control the printing of overdue messages. The
924 default is now. It isn't the date of the invoice; that's the `_date' field.
925 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
926 L<Time::Local> and L<Date::Parse> for conversion functions.
932 my( $self, $today, $template ) = @_;
934 # my $invnum = $self->invnum;
935 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
936 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
937 unless $cust_main->payname;
939 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
940 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
941 #my $balance_due = $self->owed + $pr_total - $cr_total;
942 my $balance_due = $self->owed + $pr_total;
945 #my($description,$amount);
949 foreach ( @pr_cust_bill ) {
951 "Previous Balance, Invoice #". $_->invnum.
952 " (". time2str("%x",$_->_date). ")",
953 $money_char. sprintf("%10.2f",$_->owed)
957 push @buf,['','-----------'];
958 push @buf,[ 'Total Previous Balance',
959 $money_char. sprintf("%10.2f",$pr_total ) ];
964 foreach ( $self->cust_bill_pkg ) {
968 my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
969 my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
970 my($pkg)=$part_pkg->pkg;
972 if ( $_->setup != 0 ) {
973 push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
975 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
978 if ( $_->recur != 0 ) {
980 "$pkg (" . time2str("%x",$_->sdate) . " - " .
981 time2str("%x",$_->edate) . ")",
982 $money_char. sprintf("%10.2f",$_->recur)
985 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
989 push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ]
994 push @buf,['','-----------'];
995 push @buf,['Total New Charges',
996 $money_char. sprintf("%10.2f",$self->charged) ];
999 push @buf,['','-----------'];
1000 push @buf,['Total Charges',
1001 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1005 foreach ( $self->cust_credited ) {
1007 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1009 my $reason = substr($_->cust_credit->reason,0,32);
1010 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1011 $reason = " ($reason) " if $reason;
1013 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1015 $money_char. sprintf("%10.2f",$_->amount)
1018 #foreach ( @cr_cust_credit ) {
1020 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1021 # $money_char. sprintf("%10.2f",$_->credited)
1025 #get & print payments
1026 foreach ( $self->cust_bill_pay ) {
1028 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1031 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1032 $money_char. sprintf("%10.2f",$_->amount )
1037 push @buf,['','-----------'];
1038 push @buf,['Balance Due', $money_char.
1039 sprintf("%10.2f", $balance_due ) ];
1041 #create the template
1042 my $templatefile = 'invoice_template';
1043 $templatefile .= "_$template" if $template;
1044 my @invoice_template = $conf->config($templatefile)
1045 or die "cannot load config file $templatefile";
1048 foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
1049 /invoice_lines\((\d+)\)/;
1050 $invoice_lines += $1;
1053 die "no invoice_lines() functions in template?" unless $wasfunc;
1054 my $invoice_template = new Text::Template (
1056 SOURCE => [ map "$_\n", @invoice_template ],
1057 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1058 $invoice_template->compile()
1059 or die "can't compile template: $Text::Template::ERROR";
1061 #setup template variables
1062 package FS::cust_bill::_template; #!
1063 use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
1065 $invnum = $self->invnum;
1066 $date = $self->_date;
1069 if ( $FS::cust_bill::invoice_lines ) {
1071 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1073 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1078 #format address (variable for the template)
1080 @address = ( '', '', '', '', '', '' );
1081 package FS::cust_bill; #!
1082 $FS::cust_bill::_template::address[$l++] =
1083 $cust_main->payname.
1084 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1085 ? " (P.O. #". $cust_main->payinfo. ")"
1089 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1090 if $cust_main->company;
1091 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1092 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1093 if $cust_main->address2;
1094 $FS::cust_bill::_template::address[$l++] =
1095 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1096 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1097 unless $cust_main->country eq 'US';
1099 # #overdue? (variable for the template)
1100 # $FS::cust_bill::_template::overdue = (
1102 # && $today > $self->_date
1103 ## && $self->printed > 1
1104 # && $self->printed > 0
1107 #and subroutine for the template
1109 sub FS::cust_bill::_template::invoice_lines {
1110 my $lines = shift or return @buf;
1112 scalar(@buf) ? shift @buf : [ '', '' ];
1119 $FS::cust_bill::_template::page = 1;
1123 push @collect, split("\n",
1124 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1126 $FS::cust_bill::_template::page++;
1129 map "$_\n", @collect;
1137 $Id: cust_bill.pm,v 1.41.2.4 2002-10-04 12:10:37 ivan Exp $
1143 print_text formatting (and some logic :/) is in source, but needs to be
1144 slurped in from a file. Also number of lines ($=).
1146 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1147 or something similar so the look can be completely customized?)
1151 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1152 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base