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
447 =item custnum - customer number
449 =item _date - invoice date
451 =item charged - total invoice amount
453 =item first - customer first name
455 =item last - customer first name
457 =item company - company name
459 =item address1 - address line 1
461 =item address2 - address line 1
471 =item pkg - line item description
473 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
475 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
477 =item sdate - start date for recurring fee
479 =item edate - end date for recurring fee
486 my($self, %opt) = @_;
488 #part one: create file
490 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
491 mkdir $spooldir, 0700 unless -d $spooldir;
493 my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
495 open(CSV, ">$file") or die "can't open $file: $!";
497 eval "use Text::CSV_XS";
500 my $csv = Text::CSV_XS->new({'always_quote'=>1});
502 my $cust_main = $self->cust_main;
508 time2str("%x", $self->_date),
509 sprintf("%.2f", $self->charged),
510 ( map { $cust_main->getfield($_) }
511 qw( first last company address1 address2 city state zip country ) ),
513 ) or die "can't create csv";
514 print CSV $csv->string. "\n";
516 #new charges (false laziness w/print_text)
517 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
519 my($pkg, $setup, $recur, $sdate, $edate);
520 if ( $cust_bill_pkg->pkgnum ) {
522 ($pkg, $setup, $recur, $sdate, $edate) = (
523 $cust_bill_pkg->cust_pkg->part_pkg->pkg,
524 ( $cust_bill_pkg->setup != 0
525 ? sprintf("%.2f", $cust_bill_pkg->setup )
527 ( $cust_bill_pkg->recur != 0
528 ? sprintf("%.2f", $cust_bill_pkg->recur )
530 time2str("%x", $cust_bill_pkg->sdate),
531 time2str("%x", $cust_bill_pkg->edate),
535 next unless $cust_bill_pkg->setup != 0;
536 ($pkg, $setup, $recur, $sdate, $edate) =
537 ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
543 ( map { '' } (1..11) ),
544 ($pkg, $setup, $recur, $sdate, $edate)
545 ) or die "can't create csv";
546 print CSV $csv->string. "\n";
550 close CSV or die "can't close CSV: $!";
555 if ( $opt{protocol} eq 'ftp' ) {
556 eval "use Net::FTP;";
558 $net = Net::FTP->new($opt{server}) or die @$;
560 die "unknown protocol: $opt{protocol}";
563 $net->login( $opt{username}, $opt{password} )
564 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
566 $net->binary or die "can't set binary mode";
568 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
570 $net->put($file) or die "can't put $file: $!";
580 Pays this invoice with a compliemntary payment. If there is an error,
581 returns the error, otherwise returns false.
587 my $cust_pay = new FS::cust_pay ( {
588 'invnum' => $self->invnum,
589 'paid' => $self->owed,
592 'payinfo' => $self->cust_main->payinfo,
600 Attempts to pay this invoice with a Business::OnlinePayment realtime gateway.
601 See http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
602 for supproted processors.
608 my $cust_main = $self->cust_main;
609 my $amount = $self->owed;
611 unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
612 return "Real-time card processing not enabled (processor $processor)";
614 my $bop_processor = $1; #hmm?
616 my $address = $cust_main->address1;
617 $address .= ", ". $cust_main->address2 if $cust_main->address2;
620 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
621 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
624 my($payname, $payfirst, $paylast);
625 if ( $cust_main->payname ) {
626 $payname = $cust_main->payname;
627 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
629 #$dbh->rollback if $oldAutoCommit;
630 return "Illegal payname $payname";
632 ($payfirst, $paylast) = ($1, $2);
634 $payfirst = $cust_main->getfield('first');
635 $paylast = $cust_main->getfield('last');
636 $payname = "$payfirst $paylast";
639 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
640 if ( $conf->exists('emailinvoiceauto')
641 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
642 push @invoicing_list, $cust_main->all_emails;
644 my $email = $invoicing_list[0];
646 my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
648 my $description = 'Internet Services';
649 if ( $conf->exists('business-onlinepayment-description') ) {
650 my $dtempl = $conf->config('business-onlinepayment-description');
652 my $agent_obj = $cust_main->agent
653 or die "can't retreive agent for $cust_main (agentnum ".
654 $cust_main->agentnum. ")";
655 my $agent = $agent_obj->agent;
656 my $pkgs = join(', ',
657 map { $_->cust_pkg->part_pkg->pkg }
658 grep { $_->pkgnum } $self->cust_bill_pkg
660 $description = eval qq("$dtempl");
665 new Business::OnlinePayment( $bop_processor, @bop_options );
666 $transaction->content(
668 'login' => $bop_login,
669 'password' => $bop_password,
670 'action' => $action1,
671 'description' => $description,
673 'invoice_number' => $self->invnum,
674 'customer_id' => $self->custnum,
675 'last_name' => $paylast,
676 'first_name' => $payfirst,
678 'address' => $address,
679 'city' => $cust_main->city,
680 'state' => $cust_main->state,
681 'zip' => $cust_main->zip,
682 'country' => $cust_main->country,
683 'card_number' => $cust_main->payinfo,
684 'expiration' => $exp,
685 'referer' => 'http://cleanwhisker.420.am/',
687 'phone' => $cust_main->daytime || $cust_main->night,
689 $transaction->submit();
691 if ( $transaction->is_success() && $action2 ) {
692 my $auth = $transaction->authorization;
693 my $ordernum = $transaction->can('order_number')
694 ? $transaction->order_number
697 #warn "********* $auth ***********\n";
698 #warn "********* $ordernum ***********\n";
700 new Business::OnlinePayment( $bop_processor, @bop_options );
706 password => $bop_password,
707 order_number => $ordernum,
709 authorization => $auth,
710 description => $description,
711 card_number => $cust_main->payinfo,
715 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
716 transaction_sequence_num local_transaction_date
717 local_transaction_time AVS_result_code )) {
718 $capture{$field} = $transaction->$field() if $transaction->can($field);
721 $capture->content( %capture );
725 unless ( $capture->is_success ) {
726 my $e = "Authorization sucessful but capture failed, invnum #".
727 $self->invnum. ': '. $capture->result_code.
728 ": ". $capture->error_message;
735 if ( $transaction->is_success() ) {
737 my $cust_pay = new FS::cust_pay ( {
738 'invnum' => $self->invnum,
742 'payinfo' => $cust_main->payinfo,
743 'paybatch' => "$processor:". $transaction->authorization,
745 my $error = $cust_pay->insert;
747 # gah, even with transactions.
748 my $e = 'WARNING: Card debited but database not updated - '.
749 'error applying payment, invnum #' . $self->invnum.
750 " ($processor): $error";
756 #} elsif ( $options{'report_badcard'} ) {
759 my $perror = "$processor error, invnum #". $self->invnum. ': '.
760 $transaction->result_code. ": ". $transaction->error_message;
762 if ( $conf->exists('emaildecline')
763 && grep { $_ ne 'POST' } $cust_main->invoicing_list
765 my @templ = $conf->config('declinetemplate');
766 my $template = new Text::Template (
768 SOURCE => [ map "$_\n", @templ ],
769 ) or return "($perror) can't create template: $Text::Template::ERROR";
771 or return "($perror) can't compile template: $Text::Template::ERROR";
773 my $templ_hash = { error => $transaction->error_message };
775 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
776 $ENV{MAILADDRESS} = $invoice_from;
777 my $header = new Mail::Header ( [
778 "From: $invoice_from",
779 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
780 "Sender: $invoice_from",
781 "Reply-To: $invoice_from",
782 "Date: ". time2str("%a, %d %b %Y %X %z", time),
783 "Subject: Your credit card could not be processed",
785 my $message = new Mail::Internet (
787 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
790 $message->smtpsend( Host => $smtpmachine )
791 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
792 or return "($perror) (customer # ". $self->custnum.
793 ") can't send card decline email to ".
794 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
795 " via server $smtpmachine with SMTP: $!";
803 =item realtime_card_cybercash
805 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
809 sub realtime_card_cybercash {
811 my $cust_main = $self->cust_main;
812 my $amount = $self->owed;
814 return "CyberCash CashRegister real-time card processing not enabled!"
815 unless $processor eq 'cybercash3.2';
817 my $address = $cust_main->address1;
818 $address .= ", ". $cust_main->address2 if $cust_main->address2;
821 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
822 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
827 my $paybatch = $self->invnum.
828 '-' . time2str("%y%m%d%H%M%S", time);
830 my $payname = $cust_main->payname ||
831 $cust_main->getfield('first').' '.$cust_main->getfield('last');
833 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
835 my @full_xaction = ( $xaction,
836 'Order-ID' => $paybatch,
837 'Amount' => "usd $amount",
838 'Card-Number' => $cust_main->getfield('payinfo'),
839 'Card-Name' => $payname,
840 'Card-Address' => $address,
841 'Card-City' => $cust_main->getfield('city'),
842 'Card-State' => $cust_main->getfield('state'),
843 'Card-Zip' => $cust_main->getfield('zip'),
844 'Card-Country' => $country,
849 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
851 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
852 my $cust_pay = new FS::cust_pay ( {
853 'invnum' => $self->invnum,
857 'payinfo' => $cust_main->payinfo,
858 'paybatch' => "$processor:$paybatch",
860 my $error = $cust_pay->insert;
862 # gah, even with transactions.
863 my $e = 'WARNING: Card debited but database not updated - '.
864 'error applying payment, invnum #' . $self->invnum.
865 " (CyberCash Order-ID $paybatch): $error";
871 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
872 # || $options{'report_badcard'}
875 return 'Cybercash error, invnum #' .
876 $self->invnum. ':'. $result{'MErrMsg'};
883 Adds a payment for this invoice to the pending credit card batch (see
884 L<FS::cust_pay_batch>).
890 my $cust_main = $self->cust_main;
892 my $cust_pay_batch = new FS::cust_pay_batch ( {
893 'invnum' => $self->getfield('invnum'),
894 'custnum' => $cust_main->getfield('custnum'),
895 'last' => $cust_main->getfield('last'),
896 'first' => $cust_main->getfield('first'),
897 'address1' => $cust_main->getfield('address1'),
898 'address2' => $cust_main->getfield('address2'),
899 'city' => $cust_main->getfield('city'),
900 'state' => $cust_main->getfield('state'),
901 'zip' => $cust_main->getfield('zip'),
902 'country' => $cust_main->getfield('country'),
904 'cardnum' => $cust_main->getfield('payinfo'),
905 'exp' => $cust_main->getfield('paydate'),
906 'payname' => $cust_main->getfield('payname'),
907 'amount' => $self->owed,
909 my $error = $cust_pay_batch->insert;
910 die $error if $error;
915 =item print_text [TIME];
917 Returns an text invoice, as a list of lines.
919 TIME an optional value used to control the printing of overdue messages. The
920 default is now. It isn't the date of the invoice; that's the `_date' field.
921 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
922 L<Time::Local> and L<Date::Parse> for conversion functions.
928 my( $self, $today, $template ) = @_;
930 # my $invnum = $self->invnum;
931 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
932 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
933 unless $cust_main->payname;
935 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
936 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
937 #my $balance_due = $self->owed + $pr_total - $cr_total;
938 my $balance_due = $self->owed + $pr_total;
941 #my($description,$amount);
945 foreach ( @pr_cust_bill ) {
947 "Previous Balance, Invoice #". $_->invnum.
948 " (". time2str("%x",$_->_date). ")",
949 $money_char. sprintf("%10.2f",$_->owed)
953 push @buf,['','-----------'];
954 push @buf,[ 'Total Previous Balance',
955 $money_char. sprintf("%10.2f",$pr_total ) ];
960 foreach ( $self->cust_bill_pkg ) {
964 my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
965 my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
966 my($pkg)=$part_pkg->pkg;
968 if ( $_->setup != 0 ) {
969 push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
971 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
974 if ( $_->recur != 0 ) {
976 "$pkg (" . time2str("%x",$_->sdate) . " - " .
977 time2str("%x",$_->edate) . ")",
978 $money_char. sprintf("%10.2f",$_->recur)
981 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
985 push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ]
990 push @buf,['','-----------'];
991 push @buf,['Total New Charges',
992 $money_char. sprintf("%10.2f",$self->charged) ];
995 push @buf,['','-----------'];
996 push @buf,['Total Charges',
997 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1001 foreach ( $self->cust_credited ) {
1003 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1005 my $reason = substr($_->cust_credit->reason,0,32);
1006 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1007 $reason = " ($reason) " if $reason;
1009 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1011 $money_char. sprintf("%10.2f",$_->amount)
1014 #foreach ( @cr_cust_credit ) {
1016 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1017 # $money_char. sprintf("%10.2f",$_->credited)
1021 #get & print payments
1022 foreach ( $self->cust_bill_pay ) {
1024 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1027 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1028 $money_char. sprintf("%10.2f",$_->amount )
1033 push @buf,['','-----------'];
1034 push @buf,['Balance Due', $money_char.
1035 sprintf("%10.2f", $balance_due ) ];
1037 #create the template
1038 my $templatefile = 'invoice_template';
1039 $templatefile .= "_$template" if $template;
1040 my @invoice_template = $conf->config($templatefile)
1041 or die "cannot load config file $templatefile";
1044 foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
1045 /invoice_lines\((\d+)\)/;
1046 $invoice_lines += $1;
1049 die "no invoice_lines() functions in template?" unless $wasfunc;
1050 my $invoice_template = new Text::Template (
1052 SOURCE => [ map "$_\n", @invoice_template ],
1053 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1054 $invoice_template->compile()
1055 or die "can't compile template: $Text::Template::ERROR";
1057 #setup template variables
1058 package FS::cust_bill::_template; #!
1059 use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
1061 $invnum = $self->invnum;
1062 $date = $self->_date;
1065 if ( $FS::cust_bill::invoice_lines ) {
1067 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1069 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1074 #format address (variable for the template)
1076 @address = ( '', '', '', '', '', '' );
1077 package FS::cust_bill; #!
1078 $FS::cust_bill::_template::address[$l++] =
1079 $cust_main->payname.
1080 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1081 ? " (P.O. #". $cust_main->payinfo. ")"
1085 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1086 if $cust_main->company;
1087 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1088 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1089 if $cust_main->address2;
1090 $FS::cust_bill::_template::address[$l++] =
1091 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1092 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1093 unless $cust_main->country eq 'US';
1095 # #overdue? (variable for the template)
1096 # $FS::cust_bill::_template::overdue = (
1098 # && $today > $self->_date
1099 ## && $self->printed > 1
1100 # && $self->printed > 0
1103 #and subroutine for the template
1105 sub FS::cust_bill::_template::invoice_lines {
1106 my $lines = shift or return @buf;
1108 scalar(@buf) ? shift @buf : [ '', '' ];
1115 $FS::cust_bill::_template::page = 1;
1119 push @collect, split("\n",
1120 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1122 $FS::cust_bill::_template::page++;
1125 map "$_\n", @collect;
1133 $Id: cust_bill.pm,v 1.41.2.3 2002-09-17 00:40:05 ivan Exp $
1139 print_text formatting (and some logic :/) is in source, but needs to be
1140 slurped in from a file. Also number of lines ($=).
1142 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1143 or something similar so the look can be completely customized?)
1147 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1148 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base