X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=a22f44b2407d0cb5011ff4847b2496120dc6b1cd;hb=0ebeec96313dd7edfca340f01f8fbbbac1f4aa1d;hp=8480ceadc3f5c854efe1fafb5195505bcfa5d2ec;hpb=15f65a0c56cbce6951d9cb4f71119725a2009f79;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 8480ceadc..a22f44b24 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1,41 +1,29 @@ package FS::cust_bill; use strict; -use vars qw( @ISA $conf $invoice_template $money_char ); +use vars qw( @ISA $conf $money_char ); use vars qw( $invoice_lines @buf ); #yuck use Date::Format; use Text::Template; +use FS::UID qw( datasrc ); use FS::Record qw( qsearch qsearchs ); +use FS::Misc qw( send_email ); use FS::cust_main; use FS::cust_bill_pkg; use FS::cust_credit; use FS::cust_pay; use FS::cust_pkg; +use FS::cust_credit_bill; +use FS::cust_pay_batch; +use FS::cust_bill_event; @ISA = qw( FS::Record ); #ask FS::UID to run this stuff for us later -$FS::UID::callback{'FS::cust_bill'} = sub { - +FS::UID->install_callback( sub { $conf = new FS::Conf; - $money_char = $conf->config('money_char') || '$'; - - my @invoice_template = $conf->config('invoice_template') - or die "cannot load config file invoice_template"; - $invoice_lines = 0; - foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy - /invoice_lines\((\d+)\)/; - $invoice_lines += $1; - } - die "no invoice_lines() functions in template?" unless $invoice_lines; - $invoice_template = new Text::Template ( - TYPE => 'ARRAY', - SOURCE => [ map "$_\n", @invoice_template ], - ) or die "can't create new Text::Template object: $Text::Template::ERROR"; - $invoice_template->compile() - or die "can't compile template: $Text::Template::ERROR"; -}; +} ); =head1 NAME @@ -64,6 +52,8 @@ FS::cust_bill - Object methods for cust_bill records @cust_pay_objects = $cust_bill->cust_pay; + $tax_amount = $record->tax; + @lines = $cust_bill->print_text; @lines = $cust_bill->print_text $time; @@ -85,8 +75,9 @@ L and L for conversion functions. =item charged - amount of this invoice -=item printed - how many times this invoice has been printed automatically -(see L). +=item printed - deprecated + +=item closed - books closed flag, empty or `Y' =back @@ -117,7 +108,9 @@ no record you ever posted this invoice (which is bad, no?) =cut sub delete { - return "Can't remove invoice!" + my $self = shift; + return "Can't delete closed invoice" if $self->closed =~ /^Y/i; + $self->SUPER::delete(@_); } =item replace OLD_RECORD @@ -157,6 +150,7 @@ sub check { || $self->ut_numbern('_date') || $self->ut_money('charged') || $self->ut_numbern('printed') + || $self->ut_enum('closed', [ '', 'Y' ]) ; return $error if $error; @@ -199,52 +193,461 @@ sub cust_bill_pkg { qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } ); } +=item cust_bill_event + +Returns the completed invoice events (see L) for this +invoice. + +=cut + +sub cust_bill_event { + my $self = shift; + qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } ); +} + + +=item cust_main + +Returns the customer (see L) for this invoice. + +=cut + +sub cust_main { + my $self = shift; + qsearchs( 'cust_main', { 'custnum' => $self->custnum } ); +} + =item cust_credit -Returns a list consisting of the total previous credited (see -L) for this customer, followed by the previous outstanding -credits (FS::cust_credit objects). +Depreciated. See the cust_credited method. + + #Returns a list consisting of the total previous credited (see + #L) and unapplied for this customer, followed by the previous + #outstanding credits (FS::cust_credit objects). =cut sub cust_credit { - my $self = shift; - my $total = 0; - my @cust_credit = sort { $a->_date <=> $b->_date } - grep { $_->credited != 0 && $_->_date < $self->_date } - qsearch('cust_credit', { 'custnum' => $self->custnum } ) - ; - foreach (@cust_credit) { $total += $_->credited; } - $total, @cust_credit; + use Carp; + croak "FS::cust_bill->cust_credit depreciated; see ". + "FS::cust_bill->cust_credit_bill"; + #my $self = shift; + #my $total = 0; + #my @cust_credit = sort { $a->_date <=> $b->_date } + # grep { $_->credited != 0 && $_->_date < $self->_date } + # qsearch('cust_credit', { 'custnum' => $self->custnum } ) + #; + #foreach (@cust_credit) { $total += $_->credited; } + #$total, @cust_credit; } =item cust_pay -Returns all payments (see L) for this invoice. +Depreciated. See the cust_bill_pay method. + +#Returns all payments (see L) for this invoice. =cut sub cust_pay { + use Carp; + croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay"; + #my $self = shift; + #sort { $a->_date <=> $b->_date } + # qsearch( 'cust_pay', { 'invnum' => $self->invnum } ) + #; +} + +=item cust_bill_pay + +Returns all payment applications (see L) for this invoice. + +=cut + +sub cust_bill_pay { my $self = shift; sort { $a->_date <=> $b->_date } - qsearch( 'cust_pay', { 'invnum' => $self->invnum } ) + qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } ); +} + +=item cust_credited + +Returns all applied credits (see L) for this invoice. + +=cut + +sub cust_credited { + my $self = shift; + sort { $a->_date <=> $b->_date } + qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } ) ; } +=item tax + +Returns the tax amount (see L) for this invoice. + +=cut + +sub tax { + my $self = shift; + my $total = 0; + my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum , + 'pkgnum' => 0 } ); + foreach (@taxlines) { $total += $_->setup; } + $total; +} + =item owed Returns the amount owed (still outstanding) on this invoice, which is charged -minus all payments (see L). +minus all payment applications (see L) and credit +applications (see L). =cut sub owed { my $self = shift; my $balance = $self->charged; - $balance -= $_->paid foreach ( $self->cust_pay ); + $balance -= $_->amount foreach ( $self->cust_bill_pay ); + $balance -= $_->amount foreach ( $self->cust_credited ); + $balance = sprintf( "%.2f", $balance); + $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp $balance; } +=item send + +Sends this invoice to the destinations configured for this customer: send +emails or print. See L. + +=cut + +sub send { + my($self,$template) = @_; + my @print_text = $self->print_text('', $template); + my @invoicing_list = $self->cust_main->invoicing_list; + + if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email + + #better to notify this person than silence + @invoicing_list = ($conf->config('invoice_from')) unless @invoicing_list; + + my $error = send_email( + 'from' => $conf->config('invoice_from'), + 'to' => [ grep { $_ ne 'POST' } @invoicing_list ], + 'subject' => 'Invoice', + 'body' => \@print_text, + ); + return "can't send invoice: $error" if $error; + + } + + if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal + my $lpr = $conf->config('lpr'); + open(LPR, "|$lpr") + or return "Can't open pipe to $lpr: $!"; + print LPR @print_text; + close LPR + or return $! ? "Error closing $lpr: $!" + : "Exit status $? from $lpr"; + } + + ''; + +} + +=item send_csv OPTIONS + +Sends invoice as a CSV data-file to a remote host with the specified protocol. + +Options are: + +protocol - currently only "ftp" +server +username +password +dir + +The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number +and YYMMDDHHMMSS is a timestamp. + +The fields of the CSV file is as follows: + +record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate + +=over 4 + +=item record type - B is either C or C + +If B is C, this is a primary invoice record. The +last five fields (B through B) are irrelevant, and all other +fields are filled in. + +If B is C, this is a line item record. Only the +first two fields (B and B) and the last five fields +(B through B) are filled in. + +=item invnum - invoice number + +=item custnum - customer number + +=item _date - invoice date + +=item charged - total invoice amount + +=item first - customer first name + +=item last - customer first name + +=item company - company name + +=item address1 - address line 1 + +=item address2 - address line 1 + +=item city + +=item state + +=item zip + +=item country + +=item pkg - line item description + +=item setup - line item setup fee (one or both of B and B will be defined) + +=item recur - line item recurring fee (one or both of B and B will be defined) + +=item sdate - start date for recurring fee + +=item edate - end date for recurring fee + +=back + +=cut + +sub send_csv { + my($self, %opt) = @_; + + #part one: create file + + my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill"; + mkdir $spooldir, 0700 unless -d $spooldir; + + my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time); + + open(CSV, ">$file") or die "can't open $file: $!"; + + eval "use Text::CSV_XS"; + die $@ if $@; + + my $csv = Text::CSV_XS->new({'always_quote'=>1}); + + my $cust_main = $self->cust_main; + + $csv->combine( + 'cust_bill', + $self->invnum, + $self->custnum, + time2str("%x", $self->_date), + sprintf("%.2f", $self->charged), + ( map { $cust_main->getfield($_) } + qw( first last company address1 address2 city state zip country ) ), + map { '' } (1..5), + ) or die "can't create csv"; + print CSV $csv->string. "\n"; + + #new charges (false laziness w/print_text) + foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { + + my($pkg, $setup, $recur, $sdate, $edate); + if ( $cust_bill_pkg->pkgnum ) { + + ($pkg, $setup, $recur, $sdate, $edate) = ( + $cust_bill_pkg->cust_pkg->part_pkg->pkg, + ( $cust_bill_pkg->setup != 0 + ? sprintf("%.2f", $cust_bill_pkg->setup ) + : '' ), + ( $cust_bill_pkg->recur != 0 + ? sprintf("%.2f", $cust_bill_pkg->recur ) + : '' ), + time2str("%x", $cust_bill_pkg->sdate), + time2str("%x", $cust_bill_pkg->edate), + ); + + } else { #pkgnum tax + next unless $cust_bill_pkg->setup != 0; + my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc') + ? ( $cust_bill_pkg->itemdesc || 'Tax' ) + : 'Tax'; + ($pkg, $setup, $recur, $sdate, $edate) = + ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' ); + } + + $csv->combine( + 'cust_bill_pkg', + $self->invnum, + ( map { '' } (1..11) ), + ($pkg, $setup, $recur, $sdate, $edate) + ) or die "can't create csv"; + print CSV $csv->string. "\n"; + + } + + close CSV or die "can't close CSV: $!"; + + #part two: upload it + + my $net; + if ( $opt{protocol} eq 'ftp' ) { + eval "use Net::FTP;"; + die $@ if $@; + $net = Net::FTP->new($opt{server}) or die @$; + } else { + die "unknown protocol: $opt{protocol}"; + } + + $net->login( $opt{username}, $opt{password} ) + or die "can't FTP to $opt{username}\@$opt{server}: login error: $@"; + + $net->binary or die "can't set binary mode"; + + $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}"; + + $net->put($file) or die "can't put $file: $!"; + + $net->quit; + + unlink $file; + +} + +=item comp + +Pays this invoice with a compliemntary payment. If there is an error, +returns the error, otherwise returns false. + +=cut + +sub comp { + my $self = shift; + my $cust_pay = new FS::cust_pay ( { + 'invnum' => $self->invnum, + 'paid' => $self->owed, + '_date' => '', + 'payby' => 'COMP', + 'payinfo' => $self->cust_main->payinfo, + 'paybatch' => '', + } ); + $cust_pay->insert; +} + +=item realtime_card + +Attempts to pay this invoice with a credit card payment via a +Business::OnlinePayment realtime gateway. See +http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment +for supported processors. + +=cut + +sub realtime_card { + my $self = shift; + $self->realtime_bop( 'CC', @_ ); +} + +=item realtime_ach + +Attempts to pay this invoice with an electronic check (ACH) payment via a +Business::OnlinePayment realtime gateway. See +http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment +for supported processors. + +=cut + +sub realtime_ach { + my $self = shift; + $self->realtime_bop( 'ECHECK', @_ ); +} + +=item realtime_lec + +Attempts to pay this invoice with phone bill (LEC) payment via a +Business::OnlinePayment realtime gateway. See +http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment +for supported processors. + +=cut + +sub realtime_lec { + my $self = shift; + $self->realtime_bop( 'LEC', @_ ); +} + +sub realtime_bop { + my( $self, $method ) = @_; + + my $cust_main = $self->cust_main; + my $amount = $self->owed; + + my $description = 'Internet Services'; + if ( $conf->exists('business-onlinepayment-description') ) { + my $dtempl = $conf->config('business-onlinepayment-description'); + + my $agent_obj = $cust_main->agent + or die "can't retreive agent for $cust_main (agentnum ". + $cust_main->agentnum. ")"; + my $agent = $agent_obj->agent; + my $pkgs = join(', ', + map { $_->cust_pkg->part_pkg->pkg } + grep { $_->pkgnum } $self->cust_bill_pkg + ); + $description = eval qq("$dtempl"); + } + + $cust_main->realtime_bop($method, $amount, + 'description' => $description, + 'invnum' => $self->invnum, + ); + +} + +=item batch_card + +Adds a payment for this invoice to the pending credit card batch (see +L). + +=cut + +sub batch_card { + my $self = shift; + my $cust_main = $self->cust_main; + + my $cust_pay_batch = new FS::cust_pay_batch ( { + 'invnum' => $self->getfield('invnum'), + 'custnum' => $cust_main->getfield('custnum'), + 'last' => $cust_main->getfield('last'), + 'first' => $cust_main->getfield('first'), + 'address1' => $cust_main->getfield('address1'), + 'address2' => $cust_main->getfield('address2'), + 'city' => $cust_main->getfield('city'), + 'state' => $cust_main->getfield('state'), + 'zip' => $cust_main->getfield('zip'), + 'country' => $cust_main->getfield('country'), + 'trancode' => 77, + 'cardnum' => $cust_main->getfield('payinfo'), + 'exp' => $cust_main->getfield('paydate'), + 'payname' => $cust_main->getfield('payname'), + 'amount' => $self->owed, + } ); + my $error = $cust_pay_batch->insert; + die $error if $error; + + ''; +} + =item print_text [TIME]; Returns an text invoice, as a list of lines. @@ -258,18 +661,17 @@ L and L for conversion functions. sub print_text { - my( $self, $today ) = ( shift, shift ); + my( $self, $today, $template ) = @_; $today ||= time; # my $invnum = $self->invnum; my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } ); $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') ) - unless $cust_main->payname; + unless $cust_main->payname && $cust_main->payby ne 'CHEK'; my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance - my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits - my $balance_due = $self->owed + $pr_total - $cr_total; - - # +# my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits + #my $balance_due = $self->owed + $pr_total - $cr_total; + my $balance_due = $self->owed + $pr_total; #my @collect = (); #my($description,$amount); @@ -291,33 +693,50 @@ sub print_text { } #new charges - foreach ( $self->cust_bill_pkg ) { + foreach my $cust_bill_pkg ( + ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first + ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes + ) { - if ( $_->pkgnum ) { + if ( $cust_bill_pkg->pkgnum ) { - my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } ); - my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart}); - my($pkg)=$part_pkg->pkg; + my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } ); + my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } ); + my $pkg = $part_pkg->pkg; - if ( $_->setup != 0 ) { - push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ]; + if ( $cust_bill_pkg->setup != 0 ) { + push @buf, [ "$pkg Setup", + $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ]; push @buf, map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels; } - if ( $_->recur != 0 ) { + if ( $cust_bill_pkg->recur != 0 ) { push @buf, [ - "$pkg (" . time2str("%x",$_->sdate) . " - " . - time2str("%x",$_->edate) . ")", - $money_char. sprintf("%10.2f",$_->recur) + "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " . + time2str("%x", $cust_bill_pkg->edate) . ")", + $money_char. sprintf("%10.2f", $cust_bill_pkg->recur) ]; push @buf, map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels; } - } else { #pkgnum Tax - push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ] - if $_->setup != 0; + push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details; + + } else { #pkgnum tax or one-shot line item + my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc') + ? ( $cust_bill_pkg->itemdesc || 'Tax' ) + : 'Tax'; + if ( $cust_bill_pkg->setup != 0 ) { + push @buf, [ $itemdesc, + $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ]; + } + if ( $cust_bill_pkg->recur != 0 ) { + push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - " + . time2str("%x", $cust_bill_pkg->edate). ")", + $money_char. sprintf("%10.2f", $cust_bill_pkg->recur) + ]; + } } } @@ -332,40 +751,79 @@ sub print_text { push @buf,['','']; #credits - foreach ( @cr_cust_credit ) { + foreach ( $self->cust_credited ) { + + #something more elaborate if $_->amount ne $_->cust_credit->credited ? + + my $reason = substr($_->cust_credit->reason,0,32); + $reason .= '...' if length($reason) < length($_->cust_credit->reason); + $reason = " ($reason) " if $reason; push @buf,[ - "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")", - $money_char. sprintf("%10.2f",$_->credited) + "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")". + $reason, + $money_char. sprintf("%10.2f",$_->amount) ]; } + #foreach ( @cr_cust_credit ) { + # push @buf,[ + # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")", + # $money_char. sprintf("%10.2f",$_->credited) + # ]; + #} #get & print payments - foreach ( $self->cust_pay ) { + foreach ( $self->cust_bill_pay ) { + + #something more elaborate if $_->amount ne ->cust_pay->paid ? + push @buf,[ - "Payment received ". time2str("%x",$_->_date ), - $money_char. sprintf("%10.2f",$_->paid ) + "Payment received ". time2str("%x",$_->cust_pay->_date ), + $money_char. sprintf("%10.2f",$_->amount ) ]; } #balance due push @buf,['','-----------']; push @buf,['Balance Due', $money_char. - sprintf("%10.2f",$self->owed + $pr_total - $cr_total ) ]; + sprintf("%10.2f", $balance_due ) ]; + + #create the template + my $templatefile = 'invoice_template'; + $templatefile .= "_$template" if $template; + my @invoice_template = $conf->config($templatefile) + or die "cannot load config file $templatefile"; + $invoice_lines = 0; + my $wasfunc = 0; + foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy + /invoice_lines\((\d*)\)/; + $invoice_lines += $1 || scalar(@buf); + $wasfunc=1; + } + die "no invoice_lines() functions in template?" unless $wasfunc; + my $invoice_template = new Text::Template ( + TYPE => 'ARRAY', + SOURCE => [ map "$_\n", @invoice_template ], + ) or die "can't create new Text::Template object: $Text::Template::ERROR"; + $invoice_template->compile() + or die "can't compile template: $Text::Template::ERROR"; #setup template variables - package FS::cust_bill::_template; #! - use vars qw( $invnum $date $page $total_pages @address $overdue @buf ); + use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent ); $invnum = $self->invnum; $date = $self->_date; $page = 1; - - $total_pages = - int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines ); - $total_pages++ - if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines; - + $agent = $self->cust_main->agent->agent; + + if ( $FS::cust_bill::invoice_lines ) { + $total_pages = + int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines ); + $total_pages++ + if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines; + } else { + $total_pages = 1; + } #format address (variable for the template) my $l = 0; @@ -388,24 +846,24 @@ sub print_text { $FS::cust_bill::_template::address[$l++] = $cust_main->country unless $cust_main->country eq 'US'; - #overdue? (variable for the template) - $FS::cust_bill::_template::overdue = ( - $balance_due > 0 - && $today > $self->_date -# && $self->printed > 1 - && $self->printed > 0 - ); + # #overdue? (variable for the template) + # $FS::cust_bill::_template::overdue = ( + # $balance_due > 0 + # && $today > $self->_date + ## && $self->printed > 1 + # && $self->printed > 0 + # ); #and subroutine for the template - sub FS::cust_bill::_template::invoice_lines { - my $lines = shift; + my $lines = shift || scalar(@buf); map { scalar(@buf) ? shift @buf : [ '', '' ]; } ( 1 .. $lines ); } - + + #and fill it in $FS::cust_bill::_template::page = 1; my $lines; my @collect; @@ -422,10 +880,6 @@ sub print_text { =back -=head1 VERSION - -$Id: cust_bill.pm,v 1.7 2001-04-09 23:05:15 ivan Exp $ - =head1 BUGS The delete method. @@ -438,8 +892,9 @@ or something similar so the look can be completely customized?) =head1 SEE ALSO -L, L, L, L, -L, schema.html from the base documentation. +L, L, L, L, +L, L, schema.html from the base +documentation. =cut