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 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.
613 $self->realtime_bop('CC', @_);
618 Attempts to pay this invoice with an electronic check (ACH) payment via a
619 Business::OnlinePayment realtime gateway. See
620 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
621 for supported processors.
627 $self->realtime_bop('ECHECK', @_);
633 my $cust_main = $self->cust_main;
634 my $amount = $self->owed;
636 unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
637 return "Real-time card/ACH processing not enabled (processor $processor)";
639 my $bop_processor = $1; #hmm?
641 my $address = $cust_main->address1;
642 $address .= ", ". $cust_main->address2 if $cust_main->address2;
644 my($payname, $payfirst, $paylast);
645 if ( $cust_main->payname && $method ne 'ECHECK' ) {
646 $payname = $cust_main->payname;
647 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
649 #$dbh->rollback if $oldAutoCommit;
650 return "Illegal payname $payname";
652 ($payfirst, $paylast) = ($1, $2);
654 $payfirst = $cust_main->getfield('first');
655 $paylast = $cust_main->getfield('last');
656 $payname = "$payfirst $paylast";
659 my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
660 if ( $conf->exists('emailinvoiceauto')
661 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
662 push @invoicing_list, $cust_main->all_emails;
664 my $email = $invoicing_list[0];
666 my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
668 my $description = 'Internet Services';
669 if ( $conf->exists('business-onlinepayment-description') ) {
670 my $dtempl = $conf->config('business-onlinepayment-description');
672 my $agent_obj = $cust_main->agent
673 or die "can't retreive agent for $cust_main (agentnum ".
674 $cust_main->agentnum. ")";
675 my $agent = $agent_obj->agent;
676 my $pkgs = join(', ',
677 map { $_->cust_pkg->part_pkg->pkg }
678 grep { $_->pkgnum } $self->cust_bill_pkg
680 $description = eval qq("$dtempl");
685 if ( $method eq 'CC' ) {
686 $content{card_number} = $cust_main->payinfo;
687 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
688 $content{expiration} = "$2/$1";
689 } elsif ( $method eq 'ECHECK' ) {
690 my($account_number,$routing_code) = $cust_main->payinfo;
691 ( $content{account_number}, $content{routing_code} ) =
692 split('@', $cust_main->payinfo);
693 $content{bank_name} = $cust_main->payname;
697 new Business::OnlinePayment( $bop_processor, @bop_options );
698 $transaction->content(
701 'login' => $bop_login,
702 'password' => $bop_password,
703 'action' => $action1,
704 'description' => $description,
706 'invoice_number' => $self->invnum,
707 'customer_id' => $self->custnum,
708 'last_name' => $paylast,
709 'first_name' => $payfirst,
711 'address' => $address,
712 'city' => $cust_main->city,
713 'state' => $cust_main->state,
714 'zip' => $cust_main->zip,
715 'country' => $cust_main->country,
716 'referer' => 'http://cleanwhisker.420.am/',
718 'phone' => $cust_main->daytime || $cust_main->night,
720 $transaction->submit();
722 if ( $transaction->is_success() && $action2 ) {
723 my $auth = $transaction->authorization;
724 my $ordernum = $transaction->can('order_number')
725 ? $transaction->order_number
728 #warn "********* $auth ***********\n";
729 #warn "********* $ordernum ***********\n";
731 new Business::OnlinePayment( $bop_processor, @bop_options );
738 password => $bop_password,
739 order_number => $ordernum,
741 authorization => $auth,
742 description => $description,
745 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
746 transaction_sequence_num local_transaction_date
747 local_transaction_time AVS_result_code )) {
748 $capture{$field} = $transaction->$field() if $transaction->can($field);
751 $capture->content( %capture );
755 unless ( $capture->is_success ) {
756 my $e = "Authorization sucessful but capture failed, invnum #".
757 $self->invnum. ': '. $capture->result_code.
758 ": ". $capture->error_message;
765 if ( $transaction->is_success() ) {
772 my $cust_pay = new FS::cust_pay ( {
773 'invnum' => $self->invnum,
776 'payby' => $method2payby{$method},
777 'payinfo' => $cust_main->payinfo,
778 'paybatch' => "$processor:". $transaction->authorization,
780 my $error = $cust_pay->insert;
782 # gah, even with transactions.
783 my $e = 'WARNING: Card/ACH debited but database not updated - '.
784 'error applying payment, invnum #' . $self->invnum.
785 " ($processor): $error";
791 #} elsif ( $options{'report_badcard'} ) {
794 my $perror = "$processor error, invnum #". $self->invnum. ': '.
795 $transaction->result_code. ": ". $transaction->error_message;
797 if ( $conf->exists('emaildecline')
798 && grep { $_ ne 'POST' } $cust_main->invoicing_list
800 my @templ = $conf->config('declinetemplate');
801 my $template = new Text::Template (
803 SOURCE => [ map "$_\n", @templ ],
804 ) or return "($perror) can't create template: $Text::Template::ERROR";
806 or return "($perror) can't compile template: $Text::Template::ERROR";
808 my $templ_hash = { error => $transaction->error_message };
810 #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
811 $ENV{MAILADDRESS} = $invoice_from;
812 my $header = new Mail::Header ( [
813 "From: $invoice_from",
814 "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
815 "Sender: $invoice_from",
816 "Reply-To: $invoice_from",
817 "Date: ". time2str("%a, %d %b %Y %X %z", time),
818 "Subject: Your payment could not be processed",
820 my $message = new Mail::Internet (
822 'Body' => [ $template->fill_in(HASH => $templ_hash) ],
825 $message->smtpsend( Host => $smtpmachine )
826 or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
827 or return "($perror) (customer # ". $self->custnum.
828 ") can't send card decline email to ".
829 join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
830 " via server $smtpmachine with SMTP: $!";
838 =item realtime_card_cybercash
840 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
844 sub realtime_card_cybercash {
846 my $cust_main = $self->cust_main;
847 my $amount = $self->owed;
849 return "CyberCash CashRegister real-time card processing not enabled!"
850 unless $processor eq 'cybercash3.2';
852 my $address = $cust_main->address1;
853 $address .= ", ". $cust_main->address2 if $cust_main->address2;
856 #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
857 $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
862 my $paybatch = $self->invnum.
863 '-' . time2str("%y%m%d%H%M%S", time);
865 my $payname = $cust_main->payname ||
866 $cust_main->getfield('first').' '.$cust_main->getfield('last');
868 my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
870 my @full_xaction = ( $xaction,
871 'Order-ID' => $paybatch,
872 'Amount' => "usd $amount",
873 'Card-Number' => $cust_main->getfield('payinfo'),
874 'Card-Name' => $payname,
875 'Card-Address' => $address,
876 'Card-City' => $cust_main->getfield('city'),
877 'Card-State' => $cust_main->getfield('state'),
878 'Card-Zip' => $cust_main->getfield('zip'),
879 'Card-Country' => $country,
884 %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
886 if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
887 my $cust_pay = new FS::cust_pay ( {
888 'invnum' => $self->invnum,
892 'payinfo' => $cust_main->payinfo,
893 'paybatch' => "$processor:$paybatch",
895 my $error = $cust_pay->insert;
897 # gah, even with transactions.
898 my $e = 'WARNING: Card debited but database not updated - '.
899 'error applying payment, invnum #' . $self->invnum.
900 " (CyberCash Order-ID $paybatch): $error";
906 # } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
907 # || $options{'report_badcard'}
910 return 'Cybercash error, invnum #' .
911 $self->invnum. ':'. $result{'MErrMsg'};
918 Adds a payment for this invoice to the pending credit card batch (see
919 L<FS::cust_pay_batch>).
925 my $cust_main = $self->cust_main;
927 my $cust_pay_batch = new FS::cust_pay_batch ( {
928 'invnum' => $self->getfield('invnum'),
929 'custnum' => $cust_main->getfield('custnum'),
930 'last' => $cust_main->getfield('last'),
931 'first' => $cust_main->getfield('first'),
932 'address1' => $cust_main->getfield('address1'),
933 'address2' => $cust_main->getfield('address2'),
934 'city' => $cust_main->getfield('city'),
935 'state' => $cust_main->getfield('state'),
936 'zip' => $cust_main->getfield('zip'),
937 'country' => $cust_main->getfield('country'),
939 'cardnum' => $cust_main->getfield('payinfo'),
940 'exp' => $cust_main->getfield('paydate'),
941 'payname' => $cust_main->getfield('payname'),
942 'amount' => $self->owed,
944 my $error = $cust_pay_batch->insert;
945 die $error if $error;
950 =item print_text [TIME];
952 Returns an text invoice, as a list of lines.
954 TIME an optional value used to control the printing of overdue messages. The
955 default is now. It isn't the date of the invoice; that's the `_date' field.
956 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
957 L<Time::Local> and L<Date::Parse> for conversion functions.
963 my( $self, $today, $template ) = @_;
965 # my $invnum = $self->invnum;
966 my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
967 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
968 unless $cust_main->payname && $cust_main->payby ne 'CHEK';
970 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
971 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
972 #my $balance_due = $self->owed + $pr_total - $cr_total;
973 my $balance_due = $self->owed + $pr_total;
976 #my($description,$amount);
980 foreach ( @pr_cust_bill ) {
982 "Previous Balance, Invoice #". $_->invnum.
983 " (". time2str("%x",$_->_date). ")",
984 $money_char. sprintf("%10.2f",$_->owed)
988 push @buf,['','-----------'];
989 push @buf,[ 'Total Previous Balance',
990 $money_char. sprintf("%10.2f",$pr_total ) ];
995 foreach ( $self->cust_bill_pkg ) {
999 my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
1000 my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
1001 my($pkg)=$part_pkg->pkg;
1003 if ( $_->setup != 0 ) {
1004 push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
1006 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1009 if ( $_->recur != 0 ) {
1011 "$pkg (" . time2str("%x",$_->sdate) . " - " .
1012 time2str("%x",$_->edate) . ")",
1013 $money_char. sprintf("%10.2f",$_->recur)
1016 map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1019 } else { #pkgnum Tax
1020 push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ]
1025 push @buf,['','-----------'];
1026 push @buf,['Total New Charges',
1027 $money_char. sprintf("%10.2f",$self->charged) ];
1030 push @buf,['','-----------'];
1031 push @buf,['Total Charges',
1032 $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1036 foreach ( $self->cust_credited ) {
1038 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1040 my $reason = substr($_->cust_credit->reason,0,32);
1041 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1042 $reason = " ($reason) " if $reason;
1044 "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1046 $money_char. sprintf("%10.2f",$_->amount)
1049 #foreach ( @cr_cust_credit ) {
1051 # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1052 # $money_char. sprintf("%10.2f",$_->credited)
1056 #get & print payments
1057 foreach ( $self->cust_bill_pay ) {
1059 #something more elaborate if $_->amount ne ->cust_pay->paid ?
1062 "Payment received ". time2str("%x",$_->cust_pay->_date ),
1063 $money_char. sprintf("%10.2f",$_->amount )
1068 push @buf,['','-----------'];
1069 push @buf,['Balance Due', $money_char.
1070 sprintf("%10.2f", $balance_due ) ];
1072 #create the template
1073 my $templatefile = 'invoice_template';
1074 $templatefile .= "_$template" if $template;
1075 my @invoice_template = $conf->config($templatefile)
1076 or die "cannot load config file $templatefile";
1079 foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
1080 /invoice_lines\((\d+)\)/;
1081 $invoice_lines += $1;
1084 die "no invoice_lines() functions in template?" unless $wasfunc;
1085 my $invoice_template = new Text::Template (
1087 SOURCE => [ map "$_\n", @invoice_template ],
1088 ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1089 $invoice_template->compile()
1090 or die "can't compile template: $Text::Template::ERROR";
1092 #setup template variables
1093 package FS::cust_bill::_template; #!
1094 use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
1096 $invnum = $self->invnum;
1097 $date = $self->_date;
1100 if ( $FS::cust_bill::invoice_lines ) {
1102 int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1104 if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1109 #format address (variable for the template)
1111 @address = ( '', '', '', '', '', '' );
1112 package FS::cust_bill; #!
1113 $FS::cust_bill::_template::address[$l++] =
1114 $cust_main->payname.
1115 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1116 ? " (P.O. #". $cust_main->payinfo. ")"
1120 $FS::cust_bill::_template::address[$l++] = $cust_main->company
1121 if $cust_main->company;
1122 $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1123 $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1124 if $cust_main->address2;
1125 $FS::cust_bill::_template::address[$l++] =
1126 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
1127 $FS::cust_bill::_template::address[$l++] = $cust_main->country
1128 unless $cust_main->country eq 'US';
1130 # #overdue? (variable for the template)
1131 # $FS::cust_bill::_template::overdue = (
1133 # && $today > $self->_date
1134 ## && $self->printed > 1
1135 # && $self->printed > 0
1138 #and subroutine for the template
1140 sub FS::cust_bill::_template::invoice_lines {
1141 my $lines = shift or return @buf;
1143 scalar(@buf) ? shift @buf : [ '', '' ];
1150 $FS::cust_bill::_template::page = 1;
1154 push @collect, split("\n",
1155 $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1157 $FS::cust_bill::_template::page++;
1160 map "$_\n", @collect;
1168 $Id: cust_bill.pm,v 1.41.2.6 2002-10-13 01:05:27 ivan Exp $
1174 print_text formatting (and some logic :/) is in source, but needs to be
1175 slurped in from a file. Also number of lines ($=).
1177 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1178 or something similar so the look can be completely customized?)
1182 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1183 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base