X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fpart_fee.pm;h=1d4682c1eed4866034666e724c8559054f4bfc19;hp=67da245d87e2f59269378e125240fba6ec6f5ed4;hb=389b6f1116c3309c2ee57a6c295ed1a793503095;hpb=30e2dfd524a3f52445cbca6bc2cd1962dce7eb04 diff --git a/FS/FS/part_fee.pm b/FS/FS/part_fee.pm index 67da245d8..1d4682c1e 100644 --- a/FS/FS/part_fee.pm +++ b/FS/FS/part_fee.pm @@ -2,10 +2,11 @@ package FS::part_fee; use strict; use base qw( FS::o2m_Common FS::Record ); -use vars qw( $DEBUG ); use FS::Record qw( qsearch qsearchs ); +use FS::cust_bill_pkg_display; -$DEBUG = 1; +our $DEBUG = 0; +our $default_class; =head1 NAME @@ -49,6 +50,9 @@ the invoice =item disabled - 'Y' if the fee is disabled =item classnum - the L that the fee belongs to, for reporting +and placement on multisection invoices. Unlike packages, fees I be +assigned to a class; they will default to class named "Fees", which belongs +to the same invoice section that normally contains taxes. =item taxable - 'Y' if this fee should be considered a taxable sale. Currently, taxable fees will be treated like they exist at the customer's @@ -126,6 +130,16 @@ and replace methods. sub check { my $self = shift; + $self->set('amount', 0) unless $self->amount; + $self->set('percent', 0) unless $self->percent; + + $default_class ||= qsearchs('pkg_class', { classname => 'Fees' }) + or die "default package fee class not found; run freeside-upgrade to continue.\n"; + + if (!$self->get('classnum')) { + $self->set('classnum', $default_class->classnum); + } + my $error = $self->ut_numbern('feepart') || $self->ut_textn('comment') @@ -138,28 +152,25 @@ sub check { || $self->ut_floatn('credit_weight') || $self->ut_agentnum_acl('agentnum', [ 'Edit global package definitions' ]) - || $self->ut_moneyn('amount') - || $self->ut_floatn('percent') + || $self->ut_money('amount') + || $self->ut_float('percent') || $self->ut_moneyn('minimum') || $self->ut_moneyn('maximum') || $self->ut_flag('limit_credit') - || $self->ut_enum('basis', [ '', 'charged', 'owed' ]) + || $self->ut_enum('basis', [ 'charged', 'owed', 'usage' ]) || $self->ut_enum('setuprecur', [ 'setup', 'recur' ]) ; return $error if $error; - return "For a percentage fee, the basis must be set" - if $self->get('percent') > 0 and $self->get('basis') eq ''; - - if ( ! $self->get('percent') and ! $self->get('limit_credit') ) { - # then it makes no sense to apply minimum/maximum - $self->set('minimum', ''); - $self->set('maximum', ''); - } if ( $self->get('limit_credit') ) { $self->set('maximum', ''); } + if ( $self->get('basis') eq 'usage' ) { + # to avoid confusion, don't also allow charging a percentage + $self->set('percent', 0); + } + $self->SUPER::check; } @@ -175,7 +186,7 @@ sub explanation { my $money_char = FS::Conf->new->config('money_char') || '$'; my $money = $money_char . '%.2f'; my $percent = '%.1f%%'; - my $string; + my $string = ''; if ( $self->amount > 0 ) { $string = sprintf($money, $self->amount); } @@ -190,7 +201,14 @@ sub explanation { } elsif ( $self->basis('owed') ) { $string .= 'unpaid invoice balance'; } + } elsif ( $self->basis eq 'usage' ) { + if ( $string ) { + $string .= " plus \n"; + } + # append per-class descriptions + $string .= join("\n", map { $_->explanation } $self->part_fee_usage); } + if ( $self->minimum or $self->maximum or $self->limit_credit ) { $string .= "\nbut"; if ( $self->minimum ) { @@ -219,11 +237,17 @@ representing the invoice line item for the fee, with linked L record(s) allocating the fee to the invoice or its line items, as appropriate. +If the fee is going to be charged on the upcoming invoice (credit card +processing fees, postal invoice fees), INVOICE should be an uninserted +L object where the 'cust_bill_pkg' property is an arrayref +of the non-fee line items that will appear on the invoice. + =cut sub lineitem { my $self = shift; my $cust_bill = shift; + my $cust_main = $cust_bill->cust_main; my $amount = 0 + $self->get('amount'); my $total_base; # sum of base line items @@ -235,37 +259,72 @@ sub lineitem { warn "Calculating fee: ".$self->itemdesc." on ". ($cust_bill->invnum ? "invoice #".$cust_bill->invnum : "current invoice"). "\n" if $DEBUG; - if ( $self->percent > 0 and $self->basis ne '' ) { - warn $self->percent . "% of amount ".$self->basis.")\n" - if $DEBUG; - - # $total_base: the total charged/owed on the invoice - # %item_base: billpkgnum => fraction of base amount - if ( $cust_bill->invnum ) { - my $basis = $self->basis; - $total_base = $cust_bill->$basis; # "charged", "owed" + my $basis = $self->basis; + + # $total_base: the total charged/owed on the invoice + # %item_base: billpkgnum => fraction of base amount + if ( $cust_bill->invnum ) { - # calculate the fee on an already-inserted past invoice. This may have - # payments or credits, so if basis = owed, we need to consider those. + # calculate the fee on an already-inserted past invoice. This may have + # payments or credits, so if basis = owed, we need to consider those. + @items = $cust_bill->cust_bill_pkg; + if ( $basis ne 'usage' ) { + + $total_base = $cust_bill->$basis; # "charged", "owed" my $basis_sql = $basis.'_sql'; my $sql = 'SELECT ' . FS::cust_bill_pkg->$basis_sql . ' FROM cust_bill_pkg WHERE billpkgnum = ?'; - @items = $cust_bill->cust_bill_pkg; @item_base = map { FS::Record->scalar_sql($sql, $_->billpkgnum) } @items; - } else { - # the fee applies to _this_ invoice. It has no payments or credits, so - # "charged" and "owed" basis are both just the invoice amount, and - # the line item amounts (setup + recur) + + $amount += $total_base * $self->percent / 100; + } + } else { + # the fee applies to _this_ invoice. It has no payments or credits, so + # "charged" and "owed" basis are both just the invoice amount, and + # the line item amounts (setup + recur) + @items = @{ $cust_bill->get('cust_bill_pkg') }; + if ( $basis ne 'usage' ) { $total_base = $cust_bill->charged; - @items = @{ $cust_bill->get('cust_bill_pkg') }; @item_base = map { $_->setup + $_->recur } @items; - } - $amount += $total_base * $self->percent / 100; + $amount += $total_base * $self->percent / 100; + } } + if ( $basis eq 'usage' ) { + + my %part_fee_usage = map { $_->classnum => $_ } $self->part_fee_usage; + + foreach my $item (@items) { # cust_bill_pkg objects + my $usage_fee = 0; + $item->regularize_details; + my $details; + if ( $item->billpkgnum ) { + $details = [ + qsearch('cust_bill_pkg_detail', { billpkgnum => $item->billpkgnum }) + ]; + } else { + $details = $item->get('details') || []; + } + foreach my $d (@$details) { + # if there's a usage fee defined for this class... + next if $d->amount eq '' # not a real usage detail + or $d->amount == 0 # zero charge, probably shouldn't charge fee + ; + my $p = $part_fee_usage{$d->classnum} or next; + $usage_fee += ($d->amount * $p->percent / 100) + + $p->amount; + # we'd create detail records here if we were doing that + } + # bypass @item_base entirely + push @item_fee, $usage_fee; + $amount += $usage_fee; + } + + } # if $basis eq 'usage' + if ( $self->minimum ne '' and $amount < $self->minimum ) { warn "Applying mininum fee\n" if $DEBUG; $amount = $self->minimum; @@ -273,9 +332,10 @@ sub lineitem { my $maximum = $self->maximum; if ( $self->limit_credit ) { - my $balance = $cust_bill->cust_main; + my $balance = $cust_bill->cust_main->balance; if ( $balance >= 0 ) { - $maximum = 0; + warn "Credit balance is zero, so fee is zero" if $DEBUG; + return; # don't bother doing estimated tax, etc. } elsif ( -1 * $balance < $maximum ) { $maximum = -1 * $balance; } @@ -296,17 +356,54 @@ sub lineitem { setup => 0, recur => 0, }); + + if ( $maximum and $self->taxable ) { + warn "Estimating taxes on fee.\n" if $DEBUG; + # then we need to estimate tax to respect the maximum + # XXX currently doesn't work with external (tax_rate) taxes + # or batch taxes, obviously + my $taxlisthash = {}; + my $error = $cust_main->_handle_taxes( + $taxlisthash, + $cust_bill_pkg, + location => $cust_main->ship_location + ); + my $total_rate = 0; + # $taxlisthash: tax identifier => [ cust_main_county, cust_bill_pkg... ] + my @taxes = map { $_->[0] } values %$taxlisthash; + foreach (@taxes) { + $total_rate += $_->tax; + } + if ($total_rate > 0) { + my $max_cents = $maximum * 100; + my $charge_cents = sprintf('%0.f', $max_cents * 100/(100 + $total_rate)); + # the actual maximum that we can charge... + $maximum = sprintf('%.2f', $charge_cents / 100.00); + $amount = $maximum if $amount > $maximum; + } + } # if $maximum and $self->taxable + + # set the amount that we'll charge $cust_bill_pkg->set( $self->setuprecur, $amount ); - + + # create display record + my $categoryname = ''; if ( $self->classnum ) { my $pkg_category = $self->pkg_class->pkg_category; - $cust_bill_pkg->set('section' => $pkg_category->categoryname) - if $pkg_category; + $categoryname = $pkg_category->categoryname if $pkg_category; } + my $displaytype = ($self->setuprecur eq 'setup') ? 'S' : 'R'; + my $display = FS::cust_bill_pkg_display->new({ + type => $displaytype, + section => $categoryname, + # post_total? summary? who the hell knows? + }); + $cust_bill_pkg->set('display', [ $display ]); # if this is a percentage fee and has line item fractions, # adjust them to be proportional and to add up correctly. - if ( @item_base ) { + # don't try this if we're charging on a zero-amount set of line items. + if ( scalar(@item_base) > 0 and $total_base > 0 ) { my $cents = $amount * 100; # not necessarily the same as percent my $multiplier = $amount / $total_base; @@ -327,25 +424,25 @@ sub lineitem { } } } - # and add them to the cust_bill_pkg + } + if ( @item_fee ) { + # add allocation records to the cust_bill_pkg for (my $i = 0; $i < scalar(@items); $i++) { if ( $item_fee[$i] > 0 ) { push @cust_bill_pkg_fee, FS::cust_bill_pkg_fee->new({ cust_bill_pkg => $cust_bill_pkg, - base_invnum => $cust_bill->invnum, + base_invnum => $cust_bill->invnum, # may be null amount => $item_fee[$i], base_cust_bill_pkg => $items[$i], # late resolve }); } } - } else { # if !@item_base + } else { # if !@item_fee # then this isn't a proportional fee, so it just applies to the # entire invoice. - # (if it's the current invoice, $cust_bill->invnum is null and that - # will be fixed later) push @cust_bill_pkg_fee, FS::cust_bill_pkg_fee->new({ cust_bill_pkg => $cust_bill_pkg, - base_invnum => $cust_bill->invnum, + base_invnum => $cust_bill->invnum, # may be null amount => $amount, }); } @@ -407,6 +504,19 @@ sub tax_rates { return @taxes; } +=item categoryname + +Returns the package category name, or the empty string if there is no package +category. + +=cut + +sub categoryname { + my $self = shift; + my $pkg_class = $self->pkg_class; + $pkg_class ? $pkg_class->categoryname : ''; +} + sub part_pkg_taxoverride {} # we don't do overrides here sub has_taxproduct { @@ -414,6 +524,11 @@ sub has_taxproduct { return ($self->taxproductnum ? 1 : 0); } +sub taxproduct { # compat w/ part_pkg + my $self = shift; + $self->part_pkg_taxproduct; +} + =back =head1 BUGS