[freeside-commits] branch FREESIDE_3_BRANCH updated. e8fb3448ac1b9dae5410f88cf70eb36cd822c247
Mark Wells
mark at 420.am
Tue Feb 25 23:34:09 PST 2014
The branch, FREESIDE_3_BRANCH has been updated
via e8fb3448ac1b9dae5410f88cf70eb36cd822c247 (commit)
from b6c0ce19ea154eeec1f992a528c5c7e3ef877e81 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit e8fb3448ac1b9dae5410f88cf70eb36cd822c247
Author: Mark Wells <mark at freeside.biz>
Date: Tue Feb 25 23:13:32 2014 -0800
non-package fees, #25899
diff --git a/FS/FS/AccessRight.pm b/FS/FS/AccessRight.pm
index b60db22..117e775 100644
--- a/FS/FS/AccessRight.pm
+++ b/FS/FS/AccessRight.pm
@@ -355,6 +355,9 @@ tie my %rights, 'Tie::IxHash',
'Bulk edit package definitions',
+ 'Edit fee definitions',
+ { rightname=>'Edit global fee definitions', global=>1 },
+
'Edit billing events',
{ rightname=>'Edit global billing events', global=>1 },
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 1c30901..f160d3f 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -352,6 +352,10 @@ if ( -e $addl_handler_use_file ) {
use FS::alarm_station;
use FS::addr_range;
use FS::pbx_extension;
+ use FS::cust_event_fee;
+ use FS::part_fee;
+ use FS::cust_bill_pkg_fee;
+ use FS::part_fee_msgcat;
# Sammath Naur
if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Report/Table.pm b/FS/FS/Report/Table.pm
index 7f59384..17b12ae 100644
--- a/FS/FS/Report/Table.pm
+++ b/FS/FS/Report/Table.pm
@@ -141,7 +141,7 @@ sub payments {
sub credits {
my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
$self->scalar_sql("
- SELECT SUM(amount)
+ SELECT SUM(cust_credit.amount)
FROM cust_credit
LEFT JOIN cust_main USING ( custnum )
WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum).
@@ -390,9 +390,6 @@ unspecified, defaults to all three.
'use_override': for line items generated by an add-on package, use the class
of the add-on rather than the base package.
-'freq': limit to packages with this frequency. Currently uses the part_pkg
-frequency, so term discounted packages may give odd results.
-
'distribute': for non-monthly recurring charges, ignore the invoice
date. Instead, consider the line item's starting/ending dates. Determine
the fraction of the line item duration that falls within the specified
@@ -421,7 +418,8 @@ my $cust_bill_pkg_join = '
LEFT JOIN cust_main USING ( custnum )
LEFT JOIN cust_pkg USING ( pkgnum )
LEFT JOIN part_pkg USING ( pkgpart )
- LEFT JOIN part_pkg AS override ON pkgpart_override = override.pkgpart';
+ LEFT JOIN part_pkg AS override ON pkgpart_override = override.pkgpart
+ LEFT JOIN part_fee USING ( feepart )';
sub cust_bill_pkg_setup {
my $self = shift;
@@ -434,7 +432,7 @@ sub cust_bill_pkg_setup {
$agentnum ||= $opt{'agentnum'};
my @where = (
- 'pkgnum != 0',
+ '(pkgnum != 0 OR feepart IS NOT NULL)',
$self->with_classnum($opt{'classnum'}, $opt{'use_override'}),
$self->with_report_option(%opt),
$self->in_time_period_and_agent($speriod, $eperiod, $agentnum),
@@ -461,7 +459,7 @@ sub cust_bill_pkg_recur {
my $cust_bill_pkg = $opt{'project'} ? 'v_cust_bill_pkg' : 'cust_bill_pkg';
my @where = (
- 'pkgnum != 0',
+ '(pkgnum != 0 OR feepart IS NOT NULL)',
$self->with_classnum($opt{'classnum'}, $opt{'use_override'}),
$self->with_report_option(%opt),
);
@@ -476,13 +474,14 @@ sub cust_bill_pkg_recur {
$item_usage = 'usage'; #already calculated
}
else {
- $item_usage = '( SELECT COALESCE(SUM(amount),0)
+ $item_usage = '( SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
FROM cust_bill_pkg_detail
WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum )';
}
my $recur_fraction = '';
if ( $opt{'distribute'} ) {
+ $where[0] = 'pkgnum != 0'; # specifically exclude fees
push @where, "cust_main.agentnum = $agentnum" if $agentnum;
push @where,
"$cust_bill_pkg.sdate < $eperiod",
@@ -521,7 +520,8 @@ Arguments as for C<cust_bill_pkg>, plus:
sub cust_bill_pkg_detail {
my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
- my @where = ( "cust_bill_pkg.pkgnum != 0" );
+ my @where =
+ ( "(cust_bill_pkg.pkgnum != 0 OR cust_bill_pkg.feepart IS NOT NULL)" );
push @where, 'cust_main.refnum = '. $opt{'refnum'} if $opt{'refnum'};
@@ -536,7 +536,9 @@ sub cust_bill_pkg_detail {
;
if ( $opt{'distribute'} ) {
- # then limit according to the usage time, not the billing date
+ # exclude fees
+ $where[0] = 'cust_bill_pkg.pkgnum != 0';
+ # and limit according to the usage time, not the billing date
push @where, $self->in_time_period_and_agent($speriod, $eperiod, $agentnum,
'cust_bill_pkg_detail.startdate'
);
@@ -547,7 +549,7 @@ sub cust_bill_pkg_detail {
);
}
- my $total_sql = " SELECT SUM(amount) ";
+ my $total_sql = " SELECT SUM(cust_bill_pkg_detail.amount) ";
$total_sql .=
" / CASE COUNT(cust_pkg.*) WHEN 0 THEN 1 ELSE COUNT(cust_pkg.*) END "
@@ -561,6 +563,7 @@ sub cust_bill_pkg_detail {
LEFT JOIN cust_pkg ON cust_bill_pkg.pkgnum = cust_pkg.pkgnum
LEFT JOIN part_pkg USING ( pkgpart )
LEFT JOIN part_pkg AS override ON pkgpart_override = override.pkgpart
+ LEFT JOIN part_fee USING ( feepart )
WHERE ".join( ' AND ', grep $_, @where );
$self->scalar_sql($total_sql);
@@ -683,14 +686,14 @@ sub with_classnum {
@$classnum = grep /^\d+$/, @$classnum;
my $in = 'IN ('. join(',', @$classnum). ')';
- if ( $use_override ) {
- "(
+ my $expr = "
( COALESCE(part_pkg.classnum, 0) $in AND pkgpart_override IS NULL)
- OR ( COALESCE(override.classnum, 0) $in AND pkgpart_override IS NOT NULL )
- )";
- } else {
- "COALESCE(part_pkg.classnum, 0) $in";
+ OR ( COALESCE(part_fee.classnum, 0) $in AND feepart IS NOT NULL )";
+ if ( $use_override ) {
+ $expr .= "
+ OR ( COALESCE(override.classnum, 0) $in AND pkgpart_override IS NOT NULL )";
}
+ "( $expr )";
}
sub with_usageclass {
@@ -834,7 +837,8 @@ sub init_projection {
# sdate/edate overlapping the ROI, for performance
"INSERT INTO v_cust_bill_pkg (
SELECT cust_bill_pkg.*,
- (SELECT COALESCE(SUM(amount),0) FROM cust_bill_pkg_detail
+ (SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
+ FROM cust_bill_pkg_detail
WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum),
cust_bill._date,
cust_pkg.expire
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 46a85c1..1e060c7 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -761,6 +761,19 @@ sub tables_hashref {
],
},
+ 'cust_event_fee' => {
+ 'columns' => [
+ 'eventfeenum', 'serial', '', '', '', '',
+ 'eventnum', 'int', '', '', '', '',
+ 'billpkgnum', 'int', 'NULL', '', '', '',
+ 'feepart', 'int', '', '', '', '',
+ 'nextbill', 'char', 'NULL', 1, '', '',
+ ],
+ 'primary_key' => 'eventfeenum', # I'd rather just use eventnum
+ 'unique' => [ [ 'billpkgnum' ], [ 'eventnum' ] ], # one-to-one link
+ 'index' => [ [ 'feepart' ] ],
+ },
+
'cust_bill_pkg' => {
'columns' => [
'billpkgnum', 'serial', '', '', '', '',
@@ -779,6 +792,7 @@ sub tables_hashref {
'unitsetup', @money_typen, '', '',
'unitrecur', @money_typen, '', '',
'hidden', 'char', 'NULL', 1, '', '',
+ 'feepart', 'int', 'NULL', '', '', '',
],
'primary_key' => 'billpkgnum',
'unique' => [],
@@ -808,7 +822,7 @@ sub tables_hashref {
'cust_bill_pkg_display' => {
'columns' => [
- 'billpkgdisplaynum', 'serial', '', '', '', '',
+ 'billpkgdisplaynum', 'serial', '', '', '', '',
'billpkgnum', 'int', '', '', '', '',
'section', 'varchar', 'NULL', $char_d, '', '',
#'unitsetup', @money_typen, '', '', #override the linked real one?
@@ -822,6 +836,22 @@ sub tables_hashref {
'index' => [ ['billpkgnum'], ],
},
+ 'cust_bill_pkg_fee' => {
+ 'columns' => [
+ 'billpkgfeenum', 'serial', '', '', '', '',
+ 'billpkgnum', 'int', '', '', '', '',
+ 'base_invnum', 'int', '', '', '', '',
+ 'base_billpkgnum', 'int', 'NULL', '', '', '',
+ 'amount', @money_type, '', '',
+ ],
+ 'primary_key' => 'billpkgfeenum',
+ 'unique' => [],
+ 'index' => [ ['billpkgnum'],
+ ['base_invnum'],
+ ['base_billpkgnum'],
+ ],
+ },
+
'cust_bill_pkg_tax_location' => {
'columns' => [
'billpkgtaxlocationnum', 'serial', '', '', '', '',
@@ -1374,11 +1404,6 @@ sub tables_hashref {
'primary_key' => 'creditlimitnum',
'unique' => [],
'index' => [ ['custnum'], ],
- 'foreign_keys' => [
- { columns => [ 'custnum' ],
- table => 'cust_main',
- },
- ],
},
'cust_main_note' => {
@@ -2130,6 +2155,47 @@ sub tables_hashref {
'index' => [],
},
+ 'part_fee' => {
+ 'columns' => [
+ 'feepart', 'serial', '', '', '', '',
+ 'itemdesc', 'varchar', '', $char_d, '', '',
+ 'comment', 'varchar', 'NULL', 2*$char_d, '', '',
+ 'disabled', 'char', 'NULL', 1, '', '',
+ 'classnum', 'int', 'NULL', '', '', '',
+ 'taxclass', 'varchar', 'NULL', $char_d, '', '',
+ 'taxproductnum', 'int', 'NULL', '', '', '',
+ 'pay_weight', 'real', 'NULL', '', '', '',
+ 'credit_weight', 'real', 'NULL', '', '', '',
+ 'agentnum', 'int', 'NULL', '', '', '',
+ 'amount', @money_type, '', '',
+ 'percent', 'decimal', '', '7,4', '', '',
+ 'basis', 'varchar', '', 16, '', '',
+ 'minimum', @money_typen, '', '',
+ 'maximum', @money_typen, '', '',
+ 'limit_credit', 'char', 'NULL', 1, '', '',
+ 'setuprecur', 'char', '', 5, '', '',
+ 'taxable', 'char', 'NULL', 1, '', '',
+ ],
+ 'primary_key' => 'feepart',
+ 'unique' => [],
+ 'index' => [ [ 'disabled' ], [ 'classnum' ], [ 'agentnum' ]
+ ],
+ },
+
+ 'part_fee_msgcat' => {
+ 'columns' => [
+ 'feepartmsgnum', 'serial', '', '', '', '',
+ 'feepart', 'int', '', '', '', '',
+ 'locale', 'varchar', '', 16, '', '',
+ 'itemdesc', 'varchar', '', $char_d, '', '', #longer/no limit?
+ 'comment', 'varchar', 'NULL', 2*$char_d, '', '', #longer/no limit?
+ ],
+ 'primary_key' => 'feepartmsgnum',
+ 'unique' => [ [ 'feepart', 'locale' ] ],
+ 'index' => [],
+ },
+
+
'part_pkg_link' => {
'columns' => [
'pkglinknum', 'serial', '', '', '', '',
diff --git a/FS/FS/TemplateItem_Mixin.pm b/FS/FS/TemplateItem_Mixin.pm
index 569d98c..bf857a9 100644
--- a/FS/FS/TemplateItem_Mixin.pm
+++ b/FS/FS/TemplateItem_Mixin.pm
@@ -62,7 +62,9 @@ sub desc {
if ( $self->pkgnum > 0 ) {
$self->itemdesc || $self->part_pkg->pkg_locale($locale);
- } else {
+ } elsif ( $self->feepart ) {
+ $self->part_fee->itemdesc_locale($locale);
+ } else { # by the process of elimination it must be a tax
my $desc = $self->itemdesc || 'Tax';
$desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
$desc;
diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm
index c4c2d7f..131a236 100644
--- a/FS/FS/Template_Mixin.pm
+++ b/FS/FS/Template_Mixin.pm
@@ -2452,6 +2452,8 @@ sub _items_cust_bill_pkg {
warn "$me _items_cust_bill_pkg cust_bill_pkg is quotation_pkg\n"
if $DEBUG > 1;
+ # quotation_pkgs are never fees, so don't worry about the case where
+ # part_pkg is undefined
if ( $cust_bill_pkg->setup != 0 ) {
my $description = $desc;
@@ -2471,7 +2473,7 @@ sub _items_cust_bill_pkg {
};
}
- } elsif ( $cust_bill_pkg->pkgnum > 0 ) {
+ } elsif ( $cust_bill_pkg->pkgnum > 0 ) { # and it's not a quotation_pkg
warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
if $DEBUG > 1;
@@ -2739,29 +2741,21 @@ sub _items_cust_bill_pkg {
} # recurring or usage with recurring charge
- } else { #pkgnum tax or one-shot line item (??)
+ } else { # taxes and fees
warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
if $DEBUG > 1;
- if ( $cust_bill_pkg->setup != 0 ) {
- push @b, {
- 'description' => $desc,
- 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
- };
- }
- if ( $cust_bill_pkg->recur != 0 ) {
- push @b, {
- 'description' => "$desc (".
- $self->time2str_local('short', $cust_bill_pkg->sdate). ' - '.
- $self->time2str_local('short', $cust_bill_pkg->edate). ')',
- 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
- };
- }
+ # items of this kind should normally not have sdate/edate.
+ push @b, {
+ 'description' => $desc,
+ 'amount' => sprintf('%.2f', $cust_bill_pkg->setup
+ + $cust_bill_pkg->recur)
+ };
- }
+ } # if quotation / package line item / other line item
- }
+ } # foreach $display
$discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
&& $conf->exists('discount-show-always'));
diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm
index dc0ae2d..594c9e6 100644
--- a/FS/FS/cust_bill_pkg.pm
+++ b/FS/FS/cust_bill_pkg.pm
@@ -12,6 +12,7 @@ use FS::cust_bill;
use FS::cust_bill_pkg_detail;
use FS::cust_bill_pkg_display;
use FS::cust_bill_pkg_discount;
+use FS::cust_bill_pkg_fee;
use FS::cust_bill_pay_pkg;
use FS::cust_credit_bill_pkg;
use FS::cust_tax_exempt_pkg;
@@ -25,6 +26,7 @@ use FS::cust_bill_pkg_discount_void;
use FS::cust_bill_pkg_tax_location_void;
use FS::cust_bill_pkg_tax_rate_location_void;
use FS::cust_tax_exempt_pkg_void;
+use FS::part_fee;
$DEBUG = 0;
$me = '[FS::cust_bill_pkg]';
@@ -47,8 +49,8 @@ FS::cust_bill_pkg - Object methods for cust_bill_pkg records
=head1 DESCRIPTION
An FS::cust_bill_pkg object represents an invoice line item.
-FS::cust_bill_pkg inherits from FS::Record. The following fields are currently
-supported:
+FS::cust_bill_pkg inherits from FS::Record. The following fields are
+currently supported:
=over 4
@@ -221,8 +223,7 @@ sub insert {
# XXX if we ever do tax-on-tax for these, this will have to change
# since pkgnum will be zero
$link->set('pkgnum', $taxable_cust_bill_pkg->pkgnum);
- $link->set('locationnum',
- $taxable_cust_bill_pkg->cust_pkg->tax_locationnum);
+ $link->set('locationnum', $taxable_cust_bill_pkg->tax_locationnum);
$link->set('taxable_cust_bill_pkg', '');
}
@@ -257,6 +258,52 @@ sub insert {
}
}
+ my $fee_links = $self->get('cust_bill_pkg_fee');
+ if ( $fee_links ) {
+ foreach my $link ( @$fee_links ) {
+ # very similar to cust_bill_pkg_tax_location, for obvious reasons
+ next if $link->billpkgfeenum; # don't try to double-insert
+
+ my $target = $link->get('cust_bill_pkg'); # the line item of the fee
+ my $base = $link->get('base_cust_bill_pkg'); # line item it was based on
+
+ if ( $target and $target->billpkgnum ) {
+ $link->set('billpkgnum', $target->billpkgnum);
+ # base_invnum => null indicates that the fee is based on its own
+ # invoice
+ $link->set('base_invnum', $target->invnum) unless $link->base_invnum;
+ $link->set('cust_bill_pkg', '');
+ }
+
+ if ( $base and $base->billpkgnum ) {
+ $link->set('base_billpkgnum', $base->billpkgnum);
+ $link->set('base_cust_bill_pkg', '');
+ } elsif ( $base ) {
+ # it's based on a line item that's not yet inserted
+ my $link_array = $base->get('cust_bill_pkg_fee') || [];
+ push @$link_array, $link;
+ $base->set('cust_bill_pkg_fee' => $link_array);
+ next; # don't insert the link yet
+ }
+
+ $error = $link->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error inserting cust_bill_pkg_fee: $error";
+ }
+ } # foreach my $link
+ }
+
+ my $cust_event_fee = $self->get('cust_event_fee');
+ if ( $cust_event_fee ) {
+ $cust_event_fee->set('billpkgnum' => $self->billpkgnum);
+ $error = $cust_event_fee->replace;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error updating cust_event_fee: $error";
+ }
+ }
+
my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
if ( $cust_tax_adjustment ) {
$cust_tax_adjustment->billpkgnum($self->billpkgnum);
@@ -905,6 +952,59 @@ sub credited {
$self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
}
+=item tax_locationnum
+
+Returns the L<FS::cust_location> number that this line item is in for tax
+purposes. For package sales, it's the package tax location; for fees,
+it's the customer's default service location.
+
+=cut
+
+sub tax_locationnum {
+ my $self = shift;
+ if ( $self->pkgnum ) { # normal sales
+ return $self->cust_pkg->tax_locationnum;
+ } elsif ( $self->feepart ) { # fees
+ return $self->cust_bill->cust_main->ship_locationnum;
+ } else { # taxes
+ return '';
+ }
+}
+
+sub tax_location {
+ my $self = shift;
+ FS::cust_location->by_key($self->tax_locationnum);
+}
+
+=item part_X
+
+Returns the L<FS::part_pkg> or L<FS::part_fee> object that defines this
+charge. If called on a tax line, returns nothing.
+
+=cut
+
+sub part_X {
+ my $self = shift;
+ if ( $self->override_pkgpart ) {
+ return FS::part_pkg->by_key($self->override_pkgpart);
+ } elsif ( $self->pkgnum ) {
+ return $self->cust_pkg->part_pkg;
+ } elsif ( $self->feepart ) {
+ return $self->part_fee;
+ } else {
+ return;
+ }
+}
+
+# stubs
+
+sub part_fee {
+ my $self = shift;
+ $self->feepart
+ ? FS::part_fee->by_key($self->feepart)
+ : undef;
+}
+
=back
=head1 CLASS METHODS
@@ -928,9 +1028,10 @@ sub usage_sql { $usage_sql }
# this makes owed_sql, etc. much more concise
sub charged_sql {
my ($class, $start, $end, %opt) = @_;
+ my $setuprecur = $opt{setuprecur} || '';
my $charged =
- $opt{setuprecur} =~ /^s/ ? 'cust_bill_pkg.setup' :
- $opt{setuprecur} =~ /^r/ ? 'cust_bill_pkg.recur' :
+ $setuprecur =~ /^s/ ? 'cust_bill_pkg.setup' :
+ $setuprecur =~ /^r/ ? 'cust_bill_pkg.recur' :
'cust_bill_pkg.setup + cust_bill_pkg.recur';
if ($opt{no_usage} and $charged =~ /recur/) {
@@ -966,10 +1067,9 @@ sub paid_sql {
my ($class, $start, $end, %opt) = @_;
my $s = $start ? "AND cust_pay._date <= $start" : '';
my $e = $end ? "AND cust_pay._date > $end" : '';
- my $setuprecur =
- $opt{setuprecur} =~ /^s/ ? 'setup' :
- $opt{setuprecur} =~ /^r/ ? 'recur' :
- '';
+ my $setuprecur = $opt{setuprecur} || '';
+ $setuprecur = 'setup' if $setuprecur =~ /^s/;
+ $setuprecur = 'recur' if $setuprecur =~ /^r/;
$setuprecur &&= "AND setuprecur = '$setuprecur'";
my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
@@ -995,10 +1095,9 @@ sub credited_sql {
my ($class, $start, $end, %opt) = @_;
my $s = $start ? "AND cust_credit._date <= $start" : '';
my $e = $end ? "AND cust_credit._date > $end" : '';
- my $setuprecur =
- $opt{setuprecur} =~ /^s/ ? 'setup' :
- $opt{setuprecur} =~ /^r/ ? 'recur' :
- '';
+ my $setuprecur = $opt{setuprecur} || '';
+ $setuprecur = 'setup' if $setuprecur =~ /^s/;
+ $setuprecur = 'recur' if $setuprecur =~ /^r/;
$setuprecur &&= "AND setuprecur = '$setuprecur'";
my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
diff --git a/FS/FS/cust_bill_pkg_fee.pm b/FS/FS/cust_bill_pkg_fee.pm
new file mode 100644
index 0000000..b9adfaf
--- /dev/null
+++ b/FS/FS/cust_bill_pkg_fee.pm
@@ -0,0 +1,91 @@
+package FS::cust_bill_pkg_fee;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+
+=head1 NAME
+
+FS::cust_bill_pkg_fee - Object methods for cust_bill_pkg_fee records
+
+=head1 SYNOPSIS
+
+ use FS::cust_bill_pkg_fee;
+
+ $record = new FS::cust_bill_pkg_fee \%hash;
+ $record = new FS::cust_bill_pkg_fee { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_bill_pkg_fee object records the origin of a fee.
+FS::cust_bill_pkg_fee inherits from FS::Record. The following fields
+are currently supported:
+
+=over 4
+
+=item billpkgfeenum - primary key
+
+=item billpkgnum - the billpkgnum of the fee line item
+
+=item base_invnum - the invoice number (L<FS::cust_bill>) that caused
+(this portion of) the fee to be charged.
+
+=item base_billpkgnum - the invoice line item (L<FS::cust_bill_pkg>) that
+caused (this portion of) the fee to be charged. May be null.
+
+=item amount - the fee amount
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=cut
+
+sub table { 'cust_bill_pkg_fee'; }
+
+# seeing as these methods are not defined in this module I object to having
+# perldoc noise for them
+
+=item check
+
+Checks all fields to make sure this is a valid example. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('billpkgfeenum')
+ || $self->ut_number('billpkgnum')
+ || $self->ut_foreign_key('base_invnum', 'cust_bill', 'invnum')
+ || $self->ut_foreign_keyn('base_billpkgnum', 'cust_bill_pkg', 'billpkgnum')
+ || $self->ut_money('amount')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_credit.pm b/FS/FS/cust_credit.pm
index a117f85..adaf17a 100644
--- a/FS/FS/cust_credit.pm
+++ b/FS/FS/cust_credit.pm
@@ -919,14 +919,9 @@ sub credit_lineitems {
# recalculate taxes with new amounts
$taxlisthash{$invnum} ||= {};
- my $part_pkg = $cust_bill_pkg->part_pkg;
- $cust_main->_handle_taxes( $part_pkg,
- $taxlisthash{$invnum},
- $cust_bill_pkg,
- $cust_bill_pkg->cust_pkg,
- $cust_bill_pkg->cust_bill->_date, #invoice time
- $cust_bill_pkg->cust_pkg->pkgpart,
- );
+ my $part_pkg = $cust_bill_pkg->part_pkg
+ if $cust_bill_pkg->pkgpart_override;
+ $cust_main->_handle_taxes( $taxlisthash{$invnum}, $cust_bill_pkg );
}
###
@@ -1022,12 +1017,12 @@ sub credit_lineitems {
# we still have to deal with the possibility that the tax links don't
# cover the whole amount of tax because of an incomplete upgrade...
- if ($amount > 0) {
+ if ($amount > 0.005) {
$cust_credit_bill{$invnum} += $amount;
push @{ $cust_credit_bill_pkg{$invnum} },
new FS::cust_credit_bill_pkg {
'billpkgnum' => $tax_item->billpkgnum,
- 'amount' => $amount,
+ 'amount' => sprintf('%.2f', $amount),
'setuprecur' => 'setup',
};
diff --git a/FS/FS/cust_event_fee.pm b/FS/FS/cust_event_fee.pm
new file mode 100644
index 0000000..29d7c5c
--- /dev/null
+++ b/FS/FS/cust_event_fee.pm
@@ -0,0 +1,174 @@
+package FS::cust_event_fee;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+use FS::cust_event;
+use FS::part_fee;
+
+=head1 NAME
+
+FS::cust_event_fee - Object methods for cust_event_fee records
+
+=head1 SYNOPSIS
+
+ use FS::cust_event_fee;
+
+ $record = new FS::cust_event_fee \%hash;
+ $record = new FS::cust_event_fee { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::cust_event_fee object links a billing event that charged a fee
+(an L<FS::cust_event>) to the resulting invoice line item (an
+L<FS::cust_bill_pkg> object). FS::cust_event_fee inherits from FS::Record.
+The following fields are currently supported:
+
+=over 4
+
+=item eventfeenum - primary key
+
+=item eventnum - key of the cust_event record that required the fee to be
+created. This is a unique column; there's no reason for a single event
+instance to create more than one fee.
+
+=item billpkgnum - key of the cust_bill_pkg record representing the fee
+on an invoice. This is also a unique column but can be NULL to indicate
+a fee that hasn't been billed yet. In that case it will be billed the next
+time billing runs for the customer.
+
+=item feepart - key of the fee definition (L<FS::part_fee>).
+
+=item nextbill - 'Y' if the fee should be charged on the customer's next
+bill, rather than causing a bill to be produced immediately.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new event-fee link. To add the record to the database,
+see L<"insert">.
+
+=cut
+
+sub table { 'cust_event_fee'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid example. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('eventfeenum')
+ || $self->ut_foreign_key('eventnum', 'cust_event', 'eventnum')
+ || $self->ut_foreign_keyn('billpkgnum', 'cust_bill_pkg', 'billpkgnum')
+ || $self->ut_foreign_key('feepart', 'part_fee', 'feepart')
+ || $self->ut_flag('nextbill')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item by_cust CUSTNUM[, PARAMS]
+
+Finds all cust_event_fee records belonging to the customer CUSTNUM. Currently
+fee events can be cust_main or cust_bill events; this will return both.
+
+PARAMS can be additional params to pass to qsearch; this really only works
+for 'hashref' and 'order_by'.
+
+=cut
+
+sub by_cust {
+ my $class = shift;
+ my $custnum = shift or return;
+ my %params = @_;
+ $custnum =~ /^\d+$/ or die "bad custnum $custnum";
+
+ # silliness
+ my $where = ($params{hashref} && keys (%{ $params{hashref} }))
+ ? 'AND'
+ : 'WHERE';
+ qsearch({
+ table => 'cust_event_fee',
+ addl_from => 'JOIN cust_event USING (eventnum) ' .
+ 'JOIN part_event USING (eventpart) ',
+ extra_sql => "$where eventtable = 'cust_main' ".
+ "AND cust_event.tablenum = $custnum",
+ %params
+ }),
+ qsearch({
+ table => 'cust_event_fee',
+ addl_from => 'JOIN cust_event USING (eventnum) ' .
+ 'JOIN part_event USING (eventpart) ' .
+ 'JOIN cust_bill ON (cust_event.tablenum = cust_bill.invnum)',
+ extra_sql => "$where eventtable = 'cust_bill' ".
+ "AND cust_bill.custnum = $custnum",
+ %params
+ })
+}
+
+# stubs
+
+sub cust_event {
+ my $self = shift;
+ FS::cust_event->by_key($self->eventnum);
+}
+
+sub part_fee {
+ my $self = shift;
+ FS::part_fee->by_key($self->feepart);
+}
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::cust_event>, L<FS::part_fee>, L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm
index 1e11b57..5ce94fe 100644
--- a/FS/FS/cust_main/Billing.pm
+++ b/FS/FS/cust_main/Billing.pm
@@ -21,6 +21,7 @@ use FS::cust_bill_pkg_tax_rate_location;
use FS::part_event;
use FS::part_event_condition;
use FS::pkg_category;
+use FS::cust_event_fee;
use FS::Log;
# 1 is mostly method/subroutine entry and options
@@ -532,12 +533,91 @@ sub bill {
my @cust_bill_pkg = _omit_zero_value_bundles(@{ $cust_bill_pkg{$pass} });
- next unless @cust_bill_pkg; #don't create an invoice w/o line items
-
warn "$me billing pass $pass\n"
#.Dumper(\@cust_bill_pkg)."\n"
if $DEBUG > 2;
+ ###
+ # process fees
+ ###
+
+ my @pending_event_fees = FS::cust_event_fee->by_cust($self->custnum,
+ hashref => { 'billpkgnum' => '' }
+ );
+ warn "$me found pending fee events:\n".Dumper(\@pending_event_fees)."\n"
+ if @pending_event_fees and $DEBUG > 1;
+
+ # determine whether to generate an invoice
+ my $generate_bill = scalar(@cust_bill_pkg) > 0;
+
+ foreach my $event_fee (@pending_event_fees) {
+ $generate_bill = 1 unless $event_fee->nextbill;
+ }
+
+ # don't create an invoice with no line items, or where the only line
+ # items are fees that are supposed to be held until the next invoice
+ next if !$generate_bill;
+
+ # calculate fees...
+ my @fee_items;
+ foreach my $event_fee (@pending_event_fees) {
+ my $object = $event_fee->cust_event->cust_X;
+ my $part_fee = $event_fee->part_fee;
+ my $cust_bill;
+ if ( $object->isa('FS::cust_main') ) {
+ # Not the real cust_bill object that will be inserted--in particular
+ # there are no taxes yet. If you want to charge a fee on the total
+ # invoice amount including taxes, you have to put the fee on the next
+ # invoice.
+ $cust_bill = FS::cust_bill->new({
+ 'custnum' => $self->custnum,
+ 'cust_bill_pkg' => \@cust_bill_pkg,
+ 'charged' => ${ $total_setup{$pass} } +
+ ${ $total_recur{$pass} },
+ });
+ } elsif ( $object->isa('FS::cust_bill') ) {
+ # simple case: applying the fee to a previous invoice (late fee,
+ # etc.)
+ $cust_bill = $object;
+ }
+ # if the fee def belongs to a different agent, don't charge the fee.
+ # event conditions should prevent this, but just in case they don't,
+ # skip the fee.
+ if ( $part_fee->agentnum and $part_fee->agentnum != $self->agentnum ) {
+ warn "tried to charge fee#".$part_fee->feepart .
+ " on customer#".$self->custnum." from a different agent.\n";
+ next;
+ }
+ # also skip if it's disabled
+ next if $part_fee->disabled eq 'Y';
+ # calculate the fee
+ my $fee_item = $event_fee->part_fee->lineitem($cust_bill);
+ # link this so that we can clear the marker on inserting the line item
+ $fee_item->set('cust_event_fee', $event_fee);
+ push @fee_items, $fee_item;
+
+ }
+
+ # add fees to the invoice
+ foreach my $fee_item (@fee_items) {
+
+ push @cust_bill_pkg, $fee_item;
+ ${ $total_setup{$pass} } += $fee_item->setup;
+ ${ $total_recur{$pass} } += $fee_item->recur;
+
+ my $part_fee = $fee_item->part_fee;
+ my $fee_location = $self->ship_location; # I think?
+
+ my $error = $self->_handle_taxes(
+ $taxlisthash{$pass},
+ $fee_item,
+ location => $fee_location
+ );
+ return $error if $error;
+
+ }
+
+ # XXX implementation of fees is supposed to make this go away...
if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
!$conf->exists('postal_invoice-recurring_only')
)
@@ -633,14 +713,12 @@ sub bill {
my @cust_bill = $self->cust_bill;
my $balance = $self->balance;
- my $previous_balance = scalar(@cust_bill)
- ? ( $cust_bill[$#cust_bill]->billing_balance || 0 )
- : 0;
-
- $previous_balance += $cust_bill[$#cust_bill]->charged
- if scalar(@cust_bill);
- #my $balance_adjustments =
- # sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged);
+ my $previous_bill = $cust_bill[-1] if @cust_bill;
+ my $previous_balance = 0;
+ if ( $previous_bill ) {
+ $previous_balance = $previous_bill->billing_balance
+ + $previous_bill->charged;
+ }
warn "creating the new invoice\n" if $DEBUG;
#create the new invoice
@@ -935,6 +1013,7 @@ sub _make_lines {
my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
+ my $cust_location = $cust_pkg->tax_location;
my $precommit_hooks = $params{precommit_hooks} or die "no precommit_hooks specified";
my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
my $total_setup = $params{setup} or die "no setup accumulator specified";
@@ -1231,18 +1310,8 @@ sub _make_lines {
# handle taxes
###
- #unless ( $discount_show_always ) { # oh, for god's sake
- my $error = $self->_handle_taxes(
- $part_pkg,
- $taxlisthash,
- $cust_bill_pkg,
- $cust_pkg,
- $options{invoice_time},
- $real_pkgpart,
- \%options # I have serious objections to this
- );
+ my $error = $self->_handle_taxes( $taxlisthash, $cust_bill_pkg );
return $error if $error;
- #}
$cust_bill_pkg->set_display(
part_pkg => $part_pkg,
@@ -1338,15 +1407,13 @@ sub _transfer_balance {
return @transfers;
}
-=item _handle_taxes PART_PKG TAXLISTHASH CUST_BILL_PKG CUST_PKG TIME PKGPART [ OPTIONS ]
+=item handle_taxes TAXLISTHASH CUST_BILL_PKG [ OPTIONS ]
This is _handle_taxes. It's called once for each cust_bill_pkg generated
-from _make_lines, along with the part_pkg, cust_pkg, invoice time, the
-non-overridden pkgpart, a flag indicating whether the package is being
-canceled, and a partridge in a pear tree.
+from _make_lines.
-The most important argument is 'taxlisthash'. This is shared across the
-entire invoice. It looks like this:
+TAXLISTHASH is a hashref shared across the entire invoice. It looks like
+this:
{
'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
@@ -1359,27 +1426,35 @@ That "..." is a list of FS::cust_bill_pkg objects that will be fed to
the 'taxline' method to calculate the amount of the tax. This doesn't
happen until calculate_taxes, though.
+OPTIONS may include:
+- part_item: a part_pkg or part_fee object to be used as the package/fee
+ definition.
+- location: a cust_location to be used as the billing location.
+
+If not supplied, part_item will be inferred from the pkgnum or feepart of the
+cust_bill_pkg, and location from the pkgnum (or, for fees, the invnum and
+the customer's default service location).
+
=cut
sub _handle_taxes {
my $self = shift;
- my $part_pkg = shift;
my $taxlisthash = shift;
my $cust_bill_pkg = shift;
- my $cust_pkg = shift;
- my $invoice_time = shift;
- my $real_pkgpart = shift;
- my $options = shift;
+ my %options = @_;
- local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+ # at this point I realize that we have enough information to infer all this
+ # stuff, instead of passing around giant honking argument lists
+ my $location = $options{location} || $cust_bill_pkg->tax_location;
+ my $part_item = $options{part_item} || $cust_bill_pkg->part_X;
- my $location = $cust_pkg->tax_location;
+ local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
return if ( $self->payby eq 'COMP' ); #dubious
if ( $conf->exists('enable_taxproducts')
- && ( scalar($part_pkg->part_pkg_taxoverride)
- || $part_pkg->has_taxproduct
+ && ( scalar($part_item->part_pkg_taxoverride)
+ || $part_item->has_taxproduct
)
)
{
@@ -1391,9 +1466,8 @@ sub _handle_taxes {
my @classes;
#push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
- # debatable
- push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
- push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
+ push @classes, 'setup' if $cust_bill_pkg->setup;
+ push @classes, 'recur' if $cust_bill_pkg->recur;
my $exempt = $conf->exists('cust_class-tax_exempt')
? ( $self->cust_class ? $self->cust_class->tax : '' )
@@ -1404,13 +1478,13 @@ sub _handle_taxes {
if ( !$exempt ) {
foreach my $class (@classes) {
- my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $cust_pkg );
+ my $err_or_ref = $self->_gather_taxes($part_item, $class, $location);
return $err_or_ref unless ref($err_or_ref);
$taxes{$class} = $err_or_ref;
}
unless (exists $taxes{''}) {
- my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
+ my $err_or_ref = $self->_gather_taxes($part_item, '', $location);
return $err_or_ref unless ref($err_or_ref);
$taxes{''} = $err_or_ref;
}
@@ -1461,10 +1535,7 @@ sub _handle_taxes {
warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
# calculate the tax amount that the tax_on_tax will apply to
my $hashref_or_error =
- $tax_object->taxline( $localtaxlisthash{$tax},
- 'custnum' => $self->custnum,
- 'invoice_time' => $invoice_time,
- );
+ $tax_object->taxline( $localtaxlisthash{$tax} );
return $hashref_or_error
unless ref($hashref_or_error);
@@ -1486,7 +1557,7 @@ sub _handle_taxes {
my @loc_keys = qw( district city county state country );
my %taxhash = map { $_ => $location->$_ } @loc_keys;
- $taxhash{'taxclass'} = $part_pkg->taxclass;
+ $taxhash{'taxclass'} = $part_item->taxclass;
warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
@@ -1519,44 +1590,28 @@ sub _handle_taxes {
'';
}
+=item _gather_taxes PART_ITEM CLASS CUST_LOCATION
+
+Internal method used with vendor-provided tax tables. PART_ITEM is a part_pkg
+or part_fee (which will define the tax eligibility of the product), CLASS is
+'setup', 'recur', null, or a C<usage_class> number, and CUST_LOCATION is the
+location where the service was provided (or billed, depending on
+configuration). Returns an arrayref of L<FS::tax_rate> objects that
+can apply to this line item.
+
+=cut
+
sub _gather_taxes {
my $self = shift;
- my $part_pkg = shift;
+ my $part_item = shift;
my $class = shift;
- my $cust_pkg = shift;
+ my $location = shift;
local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
- my $geocode = $cust_pkg->tax_location->geocode('cch');
-
- my @taxes = ();
-
- my @taxclassnums = map { $_->taxclassnum }
- $part_pkg->part_pkg_taxoverride($class);
-
- unless (@taxclassnums) {
- @taxclassnums = map { $_->taxclassnum }
- grep { $_->taxable eq 'Y' }
- $part_pkg->part_pkg_taxrate('cch', $geocode, $class);
- }
- warn "Found taxclassnum values of ". join(',', @taxclassnums)
- if $DEBUG;
-
- my $extra_sql =
- "AND (".
- join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
-
- @taxes = qsearch({ 'table' => 'tax_rate',
- 'hashref' => { 'geocode' => $geocode, },
- 'extra_sql' => $extra_sql,
- })
- if scalar(@taxclassnums);
-
- warn "Found taxes ".
- join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n"
- if $DEBUG;
+ my $geocode = $location->geocode('cch');
- [ @taxes ];
+ [ $part_item->tax_rates('cch', $geocode, $class) ]
}
@@ -2405,6 +2460,7 @@ sub apply_payments {
_handle_taxes
(vendor-only) _gather_taxes
_omit_zero_value_bundles
+ _handle_taxes (for fees)
calculate_taxes
apply_payments_and_credits
diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm
index 10a007c..e4d9c80 100644
--- a/FS/FS/cust_main_county.pm
+++ b/FS/FS/cust_main_county.pm
@@ -316,6 +316,11 @@ sub taxline {
my $cust_pkg = $cust_bill_pkg->cust_pkg;
my $part_pkg = $cust_bill_pkg->part_pkg;
+ my $part_fee = $cust_bill_pkg->part_fee;
+
+ my $locationnum = $cust_pkg
+ ? $cust_pkg->locationnum
+ : $cust_main->bill_locationnum;
my @new_exemptions;
my $taxable_charged = $cust_bill_pkg->setup + $cust_bill_pkg->recur
@@ -341,8 +346,13 @@ sub taxline {
}
- if ( ($part_pkg->setuptax eq 'Y' or $self->setuptax eq 'Y')
- and $cust_bill_pkg->setup > 0 and $taxable_charged > 0 ) {
+ my $setup_exempt = ( ($part_fee and not $part_fee->taxable)
+ or ($part_pkg and $part_pkg->setuptax)
+ or $self->setuptax );
+
+ if ( $setup_exempt
+ and $cust_bill_pkg->setup > 0
+ and $taxable_charged > 0 ) {
push @new_exemptions, FS::cust_tax_exempt_pkg->new({
amount => $cust_bill_pkg->setup,
@@ -351,8 +361,14 @@ sub taxline {
$taxable_charged -= $cust_bill_pkg->setup;
}
- if ( ($part_pkg->recurtax eq 'Y' or $self->recurtax eq 'Y')
- and $cust_bill_pkg->recur > 0 and $taxable_charged > 0 ) {
+
+ my $recur_exempt = ( ($part_fee and not $part_fee->taxable)
+ or ($part_pkg and $part_pkg->recurtax)
+ or $self->recurtax );
+
+ if ( $recur_exempt
+ and $cust_bill_pkg->recur > 0
+ and $taxable_charged > 0 ) {
push @new_exemptions, FS::cust_tax_exempt_pkg->new({
amount => $cust_bill_pkg->recur,
@@ -494,7 +510,7 @@ sub taxline {
'taxtype' => ref($self),
'cents' => $this_tax_cents,
'pkgnum' => $cust_bill_pkg->pkgnum,
- 'locationnum' => $cust_bill_pkg->cust_pkg->tax_locationnum,
+ 'locationnum' => $locationnum,
'taxable_cust_bill_pkg' => $cust_bill_pkg,
'tax_cust_bill_pkg' => $tax_item,
});
diff --git a/FS/FS/part_event/Action/Mixin/fee.pm b/FS/FS/part_event/Action/Mixin/fee.pm
new file mode 100644
index 0000000..a49782d
--- /dev/null
+++ b/FS/FS/part_event/Action/Mixin/fee.pm
@@ -0,0 +1,65 @@
+package FS::part_event::Action::Mixin::fee;
+
+use strict;
+use base qw( FS::part_event::Action );
+use FS::Record qw( qsearch );
+
+sub event_stage { 'pre-bill'; }
+
+sub option_fields {
+ (
+ 'feepart' => { label => 'Fee definition',
+ type => 'select-table', #select-part_fee XXX
+ table => 'part_fee',
+ hashref => { disabled => '' },
+ name_col => 'itemdesc',
+ value_col => 'feepart',
+ disable_empty => 1,
+ },
+ ),
+
+}
+
+sub default_weight { 10; }
+
+sub hold_until_bill { 1 }
+
+sub do_action {
+ my( $self, $cust_object, $cust_event ) = @_;
+
+ my $feepart = $self->option('feepart')
+ or die "no fee definition selected for event '".$self->event."'\n";
+ my $tablenum = $cust_object->get($cust_object->primary_key);
+
+ # see if there's already a pending fee for this customer/invoice
+ my @existing = qsearch({
+ table => 'cust_event_fee',
+ addl_from => 'JOIN cust_event USING (eventnum)',
+ hashref => { feepart => $feepart,
+ billpkgnum => '' },
+ extra_sql => " AND tablenum = $tablenum",
+ });
+ if (scalar @existing > 0) {
+ warn $self->event." event, object $tablenum: already scheduled\n"
+ if $FS::part_fee::DEBUG;
+ return;
+ }
+
+ # mark the event so that the fee will be charged
+ # the logic for calculating the fee amount is in FS::part_fee
+ # the logic for attaching it to the base invoice/line items is in
+ # FS::cust_bill_pkg
+ my $cust_event_fee = FS::cust_event_fee->new({
+ 'eventnum' => $cust_event->eventnum,
+ 'feepart' => $feepart,
+ 'billpkgnum' => '',
+ 'nextbill' => $self->hold_until_bill ? 'Y' : '',
+ });
+
+ my $error = $cust_event_fee->insert;
+ die $error if $error;
+
+ '';
+}
+
+1;
diff --git a/FS/FS/part_event/Action/cust_bill_fee.pm b/FS/FS/part_event/Action/cust_bill_fee.pm
new file mode 100644
index 0000000..5d962b1
--- /dev/null
+++ b/FS/FS/part_event/Action/cust_bill_fee.pm
@@ -0,0 +1,28 @@
+package FS::part_event::Action::cust_bill_fee;
+
+use strict;
+use base qw( FS::part_event::Action::Mixin::fee );
+
+sub description { 'Charge a fee based on this invoice'; }
+
+sub eventtable_hashref {
+ { 'cust_bill' => 1 };
+}
+
+sub option_fields {
+ (
+ __PACKAGE__->SUPER::option_fields,
+ 'nextbill' => { label => 'Hold fee until the customer\'s next bill',
+ type => 'checkbox',
+ value => 'Y'
+ },
+ )
+}
+
+# it makes sense for this to be optional for previous-invoice fees
+sub hold_until_bill {
+ my $self = shift;
+ $self->option('nextbill');
+}
+
+1;
diff --git a/FS/FS/part_event/Action/cust_fee.pm b/FS/FS/part_event/Action/cust_fee.pm
new file mode 100644
index 0000000..9373091
--- /dev/null
+++ b/FS/FS/part_event/Action/cust_fee.pm
@@ -0,0 +1,18 @@
+package FS::part_event::Action::cust_fee;
+
+use strict;
+use base qw( FS::part_event::Action::Mixin::fee );
+
+sub description { 'Charge a fee based on the customer\'s current invoice'; }
+
+sub eventtable_hashref {
+ { 'cust_main' => 1 };
+}
+
+sub hold_until_bill { 1 }
+
+# Otherwise identical to cust_bill_fee. We only have a separate event
+# because it behaves differently as an invoice event than as a customer
+# event, and needs a different description.
+
+1;
diff --git a/FS/FS/part_event/Action/fee.pm b/FS/FS/part_event/Action/fee.pm
index c2b4673..f1d5891 100644
--- a/FS/FS/part_event/Action/fee.pm
+++ b/FS/FS/part_event/Action/fee.pm
@@ -1,5 +1,7 @@
package FS::part_event::Action::fee;
+# DEPRECATED; will most likely be removed in 4.x
+
use strict;
use base qw( FS::part_event::Action );
@@ -53,11 +55,9 @@ sub _calc_fee {
my $part_pkg = FS::part_pkg->new({
taxclass => $self->option('taxclass')
});
- my $error = $cust_main->_handle_taxes(
- FS::part_pkg->new({ taxclass => ($self->option('taxclass') || '') }),
- $taxlisthash,
- $charge,
- FS::cust_pkg->new({custnum => $cust_main->custnum}),
+ my $error = $cust_main->_handle_taxes( $taxlisthash, $charge,
+ location => $cust_main->ship_location,
+ part_item => $part_pkg,
);
if ( $error ) {
warn "error estimating taxes for breakage charge: custnum ".$cust_main->custnum."\n";
diff --git a/FS/FS/part_fee.pm b/FS/FS/part_fee.pm
new file mode 100644
index 0000000..cd895eb
--- /dev/null
+++ b/FS/FS/part_fee.pm
@@ -0,0 +1,497 @@
+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::pkg_class;
+use FS::part_pkg_taxproduct;
+use FS::agent;
+
+$DEBUG = 0;
+
+=head1 NAME
+
+FS::part_fee - Object methods for part_fee records
+
+=head1 SYNOPSIS
+
+ use FS::part_fee;
+
+ $record = new FS::part_fee \%hash;
+ $record = new FS::part_fee { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_fee object represents the definition of a fee
+
+Fees are like packages, but instead of being ordered and then billed on a
+cycle, they are created by the operation of events and added to a single
+invoice. The fee definition specifies the fee's description, how the amount
+is calculated (a flat fee or a percentage of the customer's balance), and
+how to classify the fee for tax and reporting purposes.
+
+FS::part_fee inherits from FS::Record. The following fields are currently
+supported:
+
+=over 4
+
+=item feepart - primary key
+
+=item comment - a description of the fee for employee use, not shown on
+the invoice
+
+=item disabled - 'Y' if the fee is disabled
+
+=item classnum - the L<FS::pkg_class> that the fee belongs to, for reporting
+
+=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
+default service location.
+
+=item taxclass - the tax class the fee belongs to, as a string, for the
+internal tax system
+
+=item taxproductnum - the tax product family the fee belongs to, for the
+external tax system in use, if any
+
+=item pay_weight - Weight (relative to credit_weight and other package/fee
+definitions) that controls payment application to specific line items.
+
+=item credit_weight - Weight that controls credit application to specific
+line items.
+
+=item agentnum - the agent (L<FS::agent>) who uses this fee definition.
+
+=item amount - the flat fee to charge, as a decimal amount
+
+=item percent - the percentage of the base to charge (out of 100). If both
+this and "amount" are specified, the fee will be the sum of the two.
+
+=item basis - the method for calculating the base: currently one of "charged",
+"owed", or null.
+
+=item minimum - the minimum fee that should be charged
+
+=item maximum - the maximum fee that should be charged
+
+=item limit_credit - 'Y' to set the maximum fee at the customer's credit
+balance, if any.
+
+=item setuprecur - whether the fee should be classified as 'setup' or
+'recur', for reporting purposes.
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new fee definition. To add the record to the database, see
+L<"insert">.
+
+=cut
+
+sub table { 'part_fee'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Delete this record from the database.
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+Checks all fields to make sure this is a valid example. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ $self->set('amount', 0) unless $self->amount;
+ $self->set('percent', 0) unless $self->percent;
+
+ my $error =
+ $self->ut_numbern('feepart')
+ || $self->ut_textn('comment')
+ || $self->ut_flag('disabled')
+ || $self->ut_foreign_keyn('classnum', 'pkg_class', 'classnum')
+ || $self->ut_flag('taxable')
+ || $self->ut_textn('taxclass')
+ || $self->ut_numbern('taxproductnum')
+ || $self->ut_floatn('pay_weight')
+ || $self->ut_floatn('credit_weight')
+ || $self->ut_agentnum_acl('agentnum',
+ [ 'Edit global package definitions' ])
+ || $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('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', '');
+ }
+
+ $self->SUPER::check;
+}
+
+=item explanation
+
+Returns a string describing how this fee is calculated.
+
+=cut
+
+sub explanation {
+ my $self = shift;
+ # XXX customer currency
+ my $money_char = FS::Conf->new->config('money_char') || '$';
+ my $money = $money_char . '%.2f';
+ my $percent = '%.1f%%';
+ my $string;
+ if ( $self->amount > 0 ) {
+ $string = sprintf($money, $self->amount);
+ }
+ if ( $self->percent > 0 ) {
+ if ( $string ) {
+ $string .= " plus ";
+ }
+ $string .= sprintf($percent, $self->percent);
+ $string .= ' of the ';
+ if ( $self->basis eq 'charged' ) {
+ $string .= 'invoice amount';
+ } elsif ( $self->basis('owed') ) {
+ $string .= 'unpaid invoice balance';
+ }
+ }
+ if ( $self->minimum or $self->maximum or $self->limit_credit ) {
+ $string .= "\nbut";
+ if ( $self->minimum ) {
+ $string .= ' at least '.sprintf($money, $self->minimum);
+ }
+ if ( $self->maximum ) {
+ $string .= ' and' if $self->minimum;
+ $string .= ' at most '.sprintf($money, $self->maximum);
+ }
+ if ( $self->limit_credit ) {
+ if ( $self->maximum ) {
+ $string .= ", or the customer's credit balance, whichever is less.";
+ } else {
+ $string .= ' and' if $self->minimum;
+ $string .= " not more than the customer's credit balance";
+ }
+ }
+ }
+ return $string;
+}
+
+=item lineitem INVOICE
+
+Given INVOICE (an L<FS::cust_bill>), returns an L<FS::cust_bill_pkg> object
+representing the invoice line item for the fee, with linked
+L<FS::cust_bill_pkg_fee> 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<FS::cust_bill> 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
+ my @items; # base line items (cust_bill_pkg records)
+ my @item_base; # charged/owed of that item (sequential w/ @items)
+ my @item_fee; # fee amount of that item (sequential w/ @items)
+ my @cust_bill_pkg_fee; # link record
+
+ 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"
+
+ # calculate the fee on an already-inserted past invoice. This may have
+ # payments or credits, so if basis = owed, we need to consider those.
+ 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)
+ $total_base = $cust_bill->charged;
+ @items = @{ $cust_bill->get('cust_bill_pkg') };
+ @item_base = map { $_->setup + $_->recur }
+ @items;
+ }
+
+ $amount += $total_base * $self->percent / 100;
+ }
+
+ if ( $self->minimum ne '' and $amount < $self->minimum ) {
+ warn "Applying mininum fee\n" if $DEBUG;
+ $amount = $self->minimum;
+ }
+
+ my $maximum = $self->maximum;
+ if ( $self->limit_credit ) {
+ my $balance = $cust_bill->cust_main->balance;
+ if ( $balance >= 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;
+ }
+ }
+ if ( $maximum ne '' and $amount > $maximum ) {
+ warn "Applying maximum fee\n" if $DEBUG;
+ $amount = $maximum;
+ }
+
+ # at this point, if the fee is zero, return nothing
+ return if $amount < 0.005;
+ $amount = sprintf('%.2f', $amount);
+
+ my $cust_bill_pkg = FS::cust_bill_pkg->new({
+ feepart => $self->feepart,
+ pkgnum => 0,
+ # no sdate/edate, right?
+ 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 );
+
+ if ( $self->classnum ) {
+ my $pkg_category = $self->pkg_class->pkg_category;
+ $cust_bill_pkg->set('section' => $pkg_category->categoryname)
+ if $pkg_category;
+ }
+
+ # if this is a percentage fee and has line item fractions,
+ # adjust them to be proportional and to add up correctly.
+ if ( @item_base ) {
+ my $cents = $amount * 100;
+ # not necessarily the same as percent
+ my $multiplier = $amount / $total_base;
+ for (my $i = 0; $i < scalar(@items); $i++) {
+ my $fee = sprintf('%.2f', $item_base[$i] * $multiplier);
+ $item_fee[$i] = $fee;
+ $cents -= $fee * 100;
+ }
+ # correct rounding error
+ while ($cents >= 0.5 or $cents < -0.5) {
+ foreach my $fee (@item_fee) {
+ if ( $cents >= 0.5 ) {
+ $fee += 0.01;
+ $cents--;
+ } elsif ( $cents < -0.5 ) {
+ $fee -= 0.01;
+ $cents++;
+ }
+ }
+ }
+ # and add them 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,
+ amount => $item_fee[$i],
+ base_cust_bill_pkg => $items[$i], # late resolve
+ });
+ }
+ }
+ } else { # if !@item_base
+ # 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,
+ amount => $amount,
+ });
+ }
+
+ # cust_bill_pkg::insert will handle this
+ $cust_bill_pkg->set('cust_bill_pkg_fee', \@cust_bill_pkg_fee);
+ # avoid misbehavior by usage() and some other things
+ $cust_bill_pkg->set('details', []);
+
+ return $cust_bill_pkg;
+}
+
+=item itemdesc_locale LOCALE
+
+Returns a customer-viewable description of this fee for the given locale,
+from the part_fee_msgcat table. If the locale is empty or no localized fee
+description exists, returns part_fee.itemdesc.
+
+=cut
+
+sub itemdesc_locale {
+ my ( $self, $locale ) = @_;
+ return $self->itemdesc unless $locale;
+ my $part_fee_msgcat = qsearchs('part_fee_msgcat', {
+ feepart => $self->feepart,
+ locale => $locale,
+ }) or return $self->itemdesc;
+ $part_fee_msgcat->itemdesc;
+}
+
+=item tax_rates DATA_PROVIDER, GEOCODE
+
+Returns the external taxes (L<FS::tax_rate> objects) that apply to this
+fee, in the location specified by GEOCODE.
+
+=cut
+
+sub tax_rates {
+ my $self = shift;
+ my ($vendor, $geocode) = @_;
+ return unless $self->taxproductnum;
+ my $taxproduct = FS::part_pkg_taxproduct->by_key($self->taxproductnum);
+ # cch stuff
+ my @taxclassnums = map { $_->taxclassnum }
+ $taxproduct->part_pkg_taxrate($geocode);
+ return unless @taxclassnums;
+
+ warn "Found taxclassnum values of ". join(',', @taxclassnums) ."\n"
+ if $DEBUG;
+ my $extra_sql = "AND taxclassnum IN (". join(',', @taxclassnums) . ")";
+ my @taxes = qsearch({ 'table' => 'tax_rate',
+ 'hashref' => { 'geocode' => $geocode,
+ 'data_vendor' => $vendor },
+ 'extra_sql' => $extra_sql,
+ });
+ warn "Found taxes ". join(',', map {$_->taxnum} @taxes) ."\n"
+ if $DEBUG;
+
+ return @taxes;
+}
+
+sub part_pkg_taxoverride {} # we don't do overrides here
+
+sub has_taxproduct {
+ my $self = shift;
+ return ($self->taxproductnum ? 1 : 0);
+}
+
+# stubs that will go away under 4.x
+
+sub pkg_class {
+ my $self = shift;
+ $self->classnum
+ ? FS::pkg_class->by_key($self->classnum)
+ : undef;
+}
+
+sub part_pkg_taxproduct {
+ my $self = shift;
+ $self->taxproductnum
+ ? FS::part_pkg_taxproduct->by_key($self->taxproductnum)
+ : undef;
+}
+
+sub agent {
+ my $self = shift;
+ $self->agentnum
+ ? FS::agent->by_key($self->agentnum)
+ : undef;
+}
+
+sub part_fee_msgcat {
+ my $self = shift;
+ qsearch( 'part_fee_msgcat', { feepart => $self->feepart } );
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_fee_msgcat.pm b/FS/FS/part_fee_msgcat.pm
new file mode 100644
index 0000000..e60651e
--- /dev/null
+++ b/FS/FS/part_fee_msgcat.pm
@@ -0,0 +1,127 @@
+package FS::part_fee_msgcat;
+use base qw( FS::Record );
+
+use strict;
+use FS::Locales;
+
+=head1 NAME
+
+FS::part_fee_msgcat - Object methods for part_fee_msgcat records
+
+=head1 SYNOPSIS
+
+ use FS::part_fee_msgcat;
+
+ $record = new FS::part_fee_msgcat \%hash;
+ $record = new FS::part_fee_msgcat { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_fee_msgcat object represents localized labels of a fee
+definition. FS::part_fee_msgcat inherits from FS::Record. The following
+fields are currently supported:
+
+=over 4
+
+=item feepartmsgnum
+
+primary key
+
+=item feepart - Fee definition (L<FS::part_fee>)
+
+=item locale - locale string
+
+=item itemdesc - Localized fee name (customer-viewable)
+
+=item comment - Localized fee comment (non-customer-viewable), optional
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'part_fee_msgcat'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('feepartmsgnum')
+ || $self->ut_foreign_key('feepart', 'part_fee', 'feepart')
+ || $self->ut_enum('locale', [ FS::Locales->locales ] )
+ || $self->ut_text('itemdesc')
+ || $self->ut_textn('comment')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+Exactly duplicates part_pkg_msgcat.pm.
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index c908c91..a679cf3 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -1377,74 +1377,40 @@ sub taxproduct_description {
$part_pkg_taxproduct ? $part_pkg_taxproduct->description : '';
}
-=item part_pkg_taxrate DATA_PROVIDER, GEOCODE, [ CLASS ]
-Returns the package to taxrate m2m records for this package in the location
-specified by GEOCODE (see L<FS::part_pkg_taxrate>) and usage class CLASS.
-CLASS may be one of 'setup', 'recur', or one of the usage classes numbers
-(see L<FS::usage_class>).
+=item tax_rates DATA_PROVIDER, GEOCODE, [ CLASS ]
+
+Returns the tax table entries (L<FS::tax_rate> objects) that apply to this
+package in the location specified by GEOCODE, for usage class CLASS (one of
+'setup', 'recur', null, or a C<usage_class> number).
=cut
-sub _expand_cch_taxproductnum {
- my $self = shift;
- my $class = shift;
- my $part_pkg_taxproduct = $self->taxproduct($class);
-
- my ($a,$b,$c,$d) = ( $part_pkg_taxproduct
- ? ( split ':', $part_pkg_taxproduct->taxproduct )
- : ()
- );
- $a = '' unless $a; $b = '' unless $b; $c = '' unless $c; $d = '' unless $d;
- my $extra_sql = "AND ( taxproduct = '$a:$b:$c:$d'
- OR taxproduct = '$a:$b:$c:'
- OR taxproduct = '$a:$b:".":$d'
- OR taxproduct = '$a:$b:".":' )";
- map { $_->taxproductnum } qsearch( { 'table' => 'part_pkg_taxproduct',
- 'hashref' => { 'data_vendor'=>'cch' },
- 'extra_sql' => $extra_sql,
- } );
-
-}
-
-sub part_pkg_taxrate {
+sub tax_rates {
my $self = shift;
- my ($data_vendor, $geocode, $class) = @_;
-
- my $dbh = dbh;
- my $extra_sql = 'WHERE part_pkg_taxproduct.data_vendor = '.
- dbh->quote($data_vendor);
-
- # CCH oddness in m2m
- $extra_sql .= ' AND ('.
- join(' OR ', map{ 'geocode = '. $dbh->quote(substr($geocode, 0, $_)) }
- qw(10 5 2)
- ).
- ')';
- # much more CCH oddness in m2m -- this is kludgy
- my @tpnums = $self->_expand_cch_taxproductnum($class);
- if (scalar(@tpnums)) {
- $extra_sql .= ' AND ('.
- join(' OR ', map{ "taxproductnum = $_" } @tpnums ).
- ')';
- } else {
- $extra_sql .= ' AND ( 0 = 1 )';
+ my ($vendor, $geocode, $class) = @_;
+ my @taxclassnums = map { $_->taxclassnum }
+ $self->part_pkg_taxoverride($class);
+ if (!@taxclassnums) {
+ my $part_pkg_taxproduct = $self->taxproduct($class);
+ @taxclassnums = map { $_->taxclassnum }
+ grep { $_->taxable eq 'Y' } # why do we need this?
+ $part_pkg_taxproduct->part_pkg_taxrate($geocode);
}
+ return unless @taxclassnums;
+
+ warn "Found taxclassnum values of ". join(',', @taxclassnums) ."\n"
+ if $DEBUG;
+ my $extra_sql = "AND taxclassnum IN (". join(',', @taxclassnums) . ")";
+ my @taxes = qsearch({ 'table' => 'tax_rate',
+ 'hashref' => { 'geocode' => $geocode,
+ 'data_vendor' => $vendor },
+ 'extra_sql' => $extra_sql,
+ });
+ warn "Found taxes ". join(',', map {$_->taxnum} @taxes) ."\n"
+ if $DEBUG;
- my $addl_from = 'LEFT JOIN part_pkg_taxproduct USING ( taxproductnum )';
- my $order_by = 'ORDER BY taxclassnum, length(geocode) desc, length(taxproduct) desc';
- my $select = 'DISTINCT ON(taxclassnum) *, taxproduct';
-
- # should qsearch preface columns with the table to facilitate joins?
- qsearch( { 'table' => 'part_pkg_taxrate',
- 'select' => $select,
- 'hashref' => { # 'data_vendor' => $data_vendor,
- # 'taxproductnum' => $self->taxproductnum,
- },
- 'addl_from' => $addl_from,
- 'extra_sql' => $extra_sql,
- 'order_by' => $order_by,
- } );
+ return @taxes;
}
=item part_pkg_discount
diff --git a/FS/FS/part_pkg_taxproduct.pm b/FS/FS/part_pkg_taxproduct.pm
index 56e63b6..ddea1da 100644
--- a/FS/FS/part_pkg_taxproduct.pm
+++ b/FS/FS/part_pkg_taxproduct.pm
@@ -2,7 +2,7 @@ package FS::part_pkg_taxproduct;
use strict;
use vars qw( @ISA $delete_kludge );
-use FS::Record qw( qsearch );
+use FS::Record qw( qsearch dbh );
@ISA = qw(FS::Record);
$delete_kludge = 0;
@@ -123,12 +123,86 @@ sub check {
$self->SUPER::check;
}
+=item part_pkg_taxrate GEOCODE
+
+Returns the L<FS::part_pkg_taxrate> records (tax definitions) that can apply
+to this tax product category in the location identified by GEOCODE.
+
+=cut
+
+# actually only returns one arbitrary record for each taxclassnum, making
+# it useful only for retrieving the taxclassnums
+
+sub part_pkg_taxrate {
+ my $self = shift;
+ my $data_vendor = $self->data_vendor; # because duh
+ my $geocode = shift;
+
+ my $dbh = dbh;
+
+ # CCH oddness in m2m
+ my $extra_sql .= "AND part_pkg_taxrate.data_vendor = '$data_vendor' ".
+ "AND (".
+ join(' OR ', map{ 'geocode = '. $dbh->quote(substr($geocode, 0, $_)) }
+ qw(10 5 2)
+ ).
+ ')';
+ # much more CCH oddness in m2m -- this is kludgy
+ my $tpnums = join(',',
+ map { $_->taxproductnum }
+ $self->expand_cch_taxproduct
+ );
+ $extra_sql .= "AND taxproductnum IN($tpnums)";
+
+ my $addl_from = 'LEFT JOIN part_pkg_taxproduct USING ( taxproductnum )';
+ my $order_by = 'ORDER BY taxclassnum, length(geocode) desc, length(taxproduct) desc';
+ my $select = 'DISTINCT ON(taxclassnum) *, taxproduct';
+
+ # should qsearch preface columns with the table to facilitate joins?
+ qsearch( { 'table' => 'part_pkg_taxrate',
+ 'select' => $select,
+ 'hashref' => { 'taxable' => 'Y' },
+ 'addl_from' => $addl_from,
+ 'extra_sql' => $extra_sql,
+ 'order_by' => $order_by,
+ } );
+}
+
+=item expand_cch_taxproduct
+
+Returns the full set of part_pkg_taxproduct records that are "implied" by
+this one.
+
+=cut
+
+sub expand_cch_taxproduct {
+ my $self = shift;
+ my $class = shift;
+
+ my ($a,$b,$c,$d) = split ':', $self->taxproduct;
+ $a = '' unless $a; $b = '' unless $b; $c = '' unless $c; $d = '' unless $d;
+ my $taxproducts = join(',',
+ "'${a}:${b}:${c}:${d}'",
+ "'${a}:${b}:${c}:'",
+ "'${a}:${b}::${d}'",
+ "'${a}:${b}::'"
+ );
+ qsearch( {
+ 'table' => 'part_pkg_taxproduct',
+ 'hashref' => { 'data_vendor'=>'cch' },
+ 'extra_sql' => "AND taxproduct IN($taxproducts)",
+ } );
+}
+
+
=back
=cut
=head1 BUGS
+Confusingly named. It has nothing to do with part_pkg.
+
=head1 SEE ALSO
L<FS::Record>, schema.html from the base documentation.
diff --git a/FS/FS/tax_rate.pm b/FS/FS/tax_rate.pm
index e557682..66965cc 100644
--- a/FS/FS/tax_rate.pm
+++ b/FS/FS/tax_rate.pm
@@ -373,7 +373,7 @@ sub passtype_name {
$tax_passtypes{$self->passtype};
}
-=item taxline TAXABLES, [ OPTIONSHASH ]
+=item taxline TAXABLES
Returns a listref of a name and an amount of tax calculated for the list
of packages/amounts referenced by TAXABLES. If an error occurs, a message
@@ -383,13 +383,13 @@ is returned as a scalar.
sub taxline {
my $self = shift;
+ # this used to accept a hash of options but none of them did anything
+ # so it's been removed.
my $taxables;
- my %opt = ();
if (ref($_[0]) eq 'ARRAY') {
$taxables = shift;
- %opt = @_;
}else{
$taxables = [ @_ ];
#exemptions would be broken in this case
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 093edaf..1cc68cc 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -717,3 +717,12 @@ FS/addr_range.pm
t/addr_range.t
FS/pbx_extension.pm
t/pbx_extension.t
+FS/cust_event_fee.pm
+t/cust_event_fee.t
+FS/part_fee.pm
+t/part_fee.t
+FS/cust_bill_pkg_fee.pm
+t/cust_bill_pkg_fee.t
+FS/part_fee_msgcat.pm
+t/part_fee_msgcat.t
+
diff --git a/FS/t/cust_bill_pkg_fee.t b/FS/t/cust_bill_pkg_fee.t
new file mode 100644
index 0000000..c7cf0a0
--- /dev/null
+++ b/FS/t/cust_bill_pkg_fee.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_bill_pkg_fee;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/cust_event_fee.t b/FS/t/cust_event_fee.t
new file mode 100644
index 0000000..882b1df
--- /dev/null
+++ b/FS/t/cust_event_fee.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::cust_event_fee;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_fee.t b/FS/t/part_fee.t
new file mode 100644
index 0000000..b4192a4
--- /dev/null
+++ b/FS/t/part_fee.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_fee;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/part_fee_msgcat.t b/FS/t/part_fee_msgcat.t
new file mode 100644
index 0000000..f7e8ca8
--- /dev/null
+++ b/FS/t/part_fee_msgcat.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_fee_msgcat;
+$loaded=1;
+print "ok 1\n";
diff --git a/httemplate/browse/part_fee.html b/httemplate/browse/part_fee.html
new file mode 100644
index 0000000..482c692
--- /dev/null
+++ b/httemplate/browse/part_fee.html
@@ -0,0 +1,71 @@
+<& elements/browse.html,
+ title => 'Fee definitions',
+ name_singular => 'fee definition',
+ query => $query,
+ count_query => $count_query,
+ header => [ '#',
+ 'Description',
+ 'Comment',
+ 'Class',
+ 'Amount',
+ 'Tax status',
+ ],
+ fields => [ 'feepart',
+ 'itemdesc',
+ 'comment',
+ 'classname',
+ $sub_amount,
+ $sub_tax,
+ ],
+ disableable => 1,
+ disabled_statuspos => 3,
+ agent_pos => 6,
+ agent_virt => 1,
+ agent_null_right=> 'Edit global fee definitions',
+ links => [ '',
+ $link,
+ $link,
+ ],
+ align => 'cllccc',
+ menubar => \@menubar,
+&>
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+my $acl_edit = $curuser->access_right('Edit fee definitions');
+my $acl_edit_global = $curuser->access_right('Edit global fee definitions');
+die "access denied"
+ unless $acl_edit or $acl_edit_global;
+
+my $query = {
+ 'select' => 'part_fee.*,'.
+ '(select classname from pkg_class '.
+ 'where pkg_class.classnum = part_fee.classnum) AS classname',
+ 'table' => 'part_fee',
+};
+my $count_query = "SELECT COUNT(*) FROM part_fee";
+
+my $sub_amount = sub {
+ my $obj = shift;
+ my $string = $obj->explanation;
+ $string =~ s/\n/<br>/sg;
+ $string;
+};
+
+my $sub_tax = sub {
+ my $obj = shift;
+ if ( $obj->taxable ) {
+ return $obj->taxclass || 'taxable';
+ } elsif ( $obj->taxproductnum ) {
+ return join('<br>',
+ split(/\s*:\s*/, $obj->part_pkg_taxproduct->description)
+ );
+ } else {
+ return 'exempt';
+ }
+};
+
+my $link = [ $p.'edit/part_fee.html?', 'feepart' ];
+
+my @menubar = ( 'Add a new fee definition',
+ $p.'edit/part_fee.html' );
+</%init>
diff --git a/httemplate/edit/credit-cust_bill_pkg.html b/httemplate/edit/credit-cust_bill_pkg.html
index a5ecb69..40faddc 100644
--- a/httemplate/edit/credit-cust_bill_pkg.html
+++ b/httemplate/edit/credit-cust_bill_pkg.html
@@ -269,7 +269,8 @@ my @cust_bill_pkg = qsearch({
'select' => 'cust_bill_pkg.*',
'table' => 'cust_bill_pkg',
'addl_from' => 'LEFT JOIN cust_bill USING (invnum)',
- 'extra_sql' => "WHERE custnum = $custnum AND pkgnum != 0",
+ 'extra_sql' => "WHERE custnum = $custnum ".
+ "AND (pkgnum != 0 or feepart IS NOT NULL)",
'order_by' => 'ORDER BY invnum ASC, billpkgnum ASC',
});
diff --git a/httemplate/edit/part_fee.html b/httemplate/edit/part_fee.html
new file mode 100644
index 0000000..dada233
--- /dev/null
+++ b/httemplate/edit/part_fee.html
@@ -0,0 +1,141 @@
+<& elements/edit.html,
+ 'name_singular' => 'fee definition',
+ 'table' => 'part_fee',
+ 'labels' => {
+ 'feepart' => 'Fee definition',
+ 'itemdesc' => 'Description',
+ 'comment' => 'Comment (customer-hidden)',
+ 'classnum' => 'Package class',
+ 'taxable' => 'This fee is taxable',
+ 'disabled' => 'Disable this fee',
+ 'taxclass' => 'Tax class name',
+ 'taxproductnum' => 'Tax product',
+ 'pay_weight' => 'Payment weight',
+ 'credit_weight' => 'Credit weight',
+ 'agentnum' => 'Agent',
+ 'amount' => 'Flat fee amount',
+ 'percent' => 'Percentage of invoice amount',
+ 'basis' => 'Based on',
+ 'setuprecur' => 'Report this fee as',
+ 'minimum' => 'Minimum fee',
+ 'maximum' => 'Maximum fee',
+ 'limit_credit' => 'Limit to customer credit balance',
+ %locale_labels
+ },
+ 'fields' => \@fields,
+ 'edit_callback' => $edit_callback,
+ 'error_callback' => $error_callback,
+&>
+<%init>
+my $curuser = $FS::CurrentUser::CurrentUser;
+my $acl_edit = $curuser->access_right('Edit fee definitions');
+my $acl_edit_global = $curuser->access_right('Edit global fee definitions');
+die "access denied"
+ unless $acl_edit or $acl_edit_global;
+
+my $conf = FS::Conf->new;
+my @tax_fields;
+if ( $conf->exists('enable_taxproducts') ) {
+ @tax_fields = (
+ { field => 'taxproductnum', type => 'select-taxproduct' }
+ );
+} else {
+ @tax_fields = (
+ { field => 'taxable', type => 'checkbox', value => 'Y' },
+ );
+ push (
+ { field => 'taxclass', type => 'select-taxclass' },
+ ) if $conf->exists('enable_taxclasses');
+}
+
+my $default_locale = $conf->config('locale') || 'en_US';
+my @locales = grep {$_ ne $default_locale} $conf->config('available-locales');
+# duplicates edit/part_pkg.cgi, yuck
+my $n = 0;
+my (@locale_fields, %locale_labels);
+foreach (@locales) {
+ push @locale_fields,
+ { field => 'feepartmsgnum'. $n, type => 'hidden' },
+ { field => 'feepartmsgnum'. $n. '_locale', type => 'hidden' },
+ { field => 'feepartmsgnum'. $n. '_itemdesc', type => 'text', size => 40 },
+ ;
+ $locale_labels{ 'feepartmsgnum'.$n.'_itemdesc' } =
+ 'Description—' . FS::Locales->description($_);
+ $n++;
+}
+
+my @fields = (
+
+ { field => 'itemdesc', type => 'text', size => 40, },
+ @locale_fields,
+
+ { field => 'comment', type => 'text', size => 40, },
+
+ { field => 'agentnum',
+ type => 'select-agent',
+ disable_empty => !$acl_edit_global,
+ empty_label => '(global)',
+ },
+
+ { field => 'classnum',
+ type => 'select-pkg_class',
+ },
+
+ { field => 'disabled',
+ type => 'checkbox',
+ value => 'Y',
+ },
+
+ { field => 'setuprecur',
+ type => 'select',
+ options => [ 'setup', 'recur' ],
+ labels => { 'setup' => 'a setup fee',
+ 'recur' => 'a recurring charge' },
+ },
+
+ { type => 'justtitle', value => 'Fee calculation' },
+ { field => 'amount', type => 'money', },
+ { field => 'percent', type => 'percentage', },
+
+ { field => 'basis',
+ type => 'select',
+ options => [ 'charged', 'owed' ],
+ labels => { 'charged' => 'amount charged',
+ 'owed' => 'balance due', },
+ },
+
+ { field => 'minimum', type => 'money', },
+ { field => 'maximum', type => 'money', },
+ { field => 'limit_credit',
+ type => 'checkbox',
+ value => 'Y' },
+
+ { type => 'justtitle', value => 'Taxation' },
+
+ @tax_fields,
+);
+
+my $edit_callback = sub {
+ my ($cgi, $obj, $fields, $opt) = @_;
+ my %existing_locales;
+ if ( $obj->feepart ) {
+ %existing_locales = map { $_->locale => $_ } $obj->part_fee_msgcat;
+ }
+ my $n = 0;
+ foreach (@locales) {
+ $obj->set('feepartmsgnum'.$n.'_locale', $_);
+ # load the existing itemdescs
+ if ( my $msgcat = $existing_locales{$_} ) {
+ $obj->set('feepartmsgnum'.$n, $msgcat->feepartmsgnum);
+ $obj->set('feepartmsgnum'.$n.'_itemdesc', $msgcat->itemdesc);
+ }
+ # then override that with the CGI param if there is one
+ if ( my $itemdesc = $cgi->param('feepartmsgnum'.$n.'_itemdesc') ) {
+ $obj->set('feepartmsgnum'.$n.'_itemdesc', $itemdesc);
+ }
+ $n++;
+ }
+};
+
+my $error_callback = $edit_callback;
+</%init>
diff --git a/httemplate/edit/process/part_fee.html b/httemplate/edit/process/part_fee.html
new file mode 100644
index 0000000..25656e9
--- /dev/null
+++ b/httemplate/edit/process/part_fee.html
@@ -0,0 +1,20 @@
+<& elements/process.html,
+ 'debug' => 1,
+ 'table' => 'part_fee',
+ 'agent_virt' => 1,
+ 'agent_null_right' => 'Edit global fee definitions',
+ 'viewall_dir' => 'browse',
+ 'process_o2m' => {
+ 'table' => 'part_fee_msgcat',
+ 'fields' => [ 'locale', 'itemdesc' ],
+ },
+&>
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+my $acl_edit = $curuser->access_right('Edit fee definitions');
+my $acl_edit_global = $curuser->access_right('Edit global fee definitions');
+die "access denied"
+ unless $acl_edit or $acl_edit_global;
+
+</%init>
diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html
index cfc262e..9f1e105 100644
--- a/httemplate/elements/menu.html
+++ b/httemplate/elements/menu.html
@@ -572,6 +572,10 @@ if ( $curuser->access_right('Configuration') ) {
$config_pkg{'Package report classes'} = [ $fsurl.'browse/part_pkg_report_option.html', 'Package classes define optional groups of packages for reporting only.' ];
#eo package grouping sub-menu
+ if ( $curuser->access_right([ 'Edit fee definitions',
+ 'Edit global fee definitions' ]) ) {
+ $config_pkg{'Fees'} = [ $fsurl.'browse/part_fee.html', '' ];
+ }
$config_pkg{'Discounts'} = [ $fsurl.'browse/discount.html', '' ];
$config_pkg{'Discount classes'} = [ $fsurl.'browse/discount_class.html', '' ];
$config_pkg{'Cancel/Suspend Reasons'} = [ \%config_pkg_reason, '' ];
diff --git a/httemplate/misc/xmlhttp-calculate_taxes.html b/httemplate/misc/xmlhttp-calculate_taxes.html
index ed7bd01..2bb1f4c 100644
--- a/httemplate/misc/xmlhttp-calculate_taxes.html
+++ b/httemplate/misc/xmlhttp-calculate_taxes.html
@@ -62,14 +62,7 @@ if ( $sub eq 'calculate_taxes' ) {
my $taxlisthash = {};
foreach my $cust_bill_pkg (values %cust_bill_pkg) {
- my $part_pkg = $cust_bill_pkg->part_pkg;
- $cust_main->_handle_taxes( $part_pkg,
- $taxlisthash,
- $cust_bill_pkg,
- $cust_bill_pkg->cust_pkg,
- $cust_bill_pkg->cust_bill->_date,
- $cust_bill_pkg->cust_pkg->pkgpart,
- );
+ $cust_main->_handle_taxes( $taxlisthash, $cust_bill_pkg );
}
my $listref_or_error =
$cust_main->calculate_taxes( [ values %cust_bill_pkg ], $taxlisthash, [ values %cust_bill_pkg ]->[0]->cust_bill->_date );
diff --git a/httemplate/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html b/httemplate/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html
index c0db3e2..4558682 100644
--- a/httemplate/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html
+++ b/httemplate/misc/xmlhttp-cust_bill_pkg-calculate_taxes.html
@@ -62,15 +62,7 @@ if ( $sub eq 'calculate_taxes' ) {
push @cust_bill_pkg, $cust_bill_pkg;
- my $part_pkg = $cust_bill_pkg->part_pkg;
- $cust_main->_handle_taxes( $part_pkg,
- $taxlisthash,
- $cust_bill_pkg,
- $cust_bill_pkg->cust_pkg,
- $cust_bill_pkg->cust_bill->_date,
- $cust_bill_pkg->cust_pkg->pkgpart,
- );
-
+ $cust_main->_handle_taxes( $taxlisthash, $cust_bill_pkg );
}
if ( @cust_bill_pkg ) {
@@ -89,7 +81,10 @@ if ( $sub eq 'calculate_taxes' ) {
foreach my $taxline ( @$listref_or_error ) {
my $amount = $taxline->setup;
my $desc = $taxline->desc;
- foreach my $location ( @{$taxline->cust_bill_pkg_tax_location}, @{$taxline->cust_bill_pkg_tax_rate_location} ) {
+ foreach my $location (
+ @{$taxline->get('cust_bill_pkg_tax_location')},
+ @{$taxline->get('cust_bill_pkg_tax_rate_location')} )
+ {
my $taxlocnum = $location->locationnum || '';
my $taxratelocnum = $location->taxratelocationnum || '';
$location->cust_bill_pkg_desc($taxline->desc); #ugh @ that kludge
diff --git a/httemplate/search/cust_bill_pkg.cgi b/httemplate/search/cust_bill_pkg.cgi
index 0b61471..ccc0046 100644
--- a/httemplate/search/cust_bill_pkg.cgi
+++ b/httemplate/search/cust_bill_pkg.cgi
@@ -130,9 +130,9 @@ Filtering parameters:
- use_override: Apply "classnum" and "taxclass" filtering based on the
override (bundle) pkgpart, rather than always using the true pkgpart.
-- nottax: Limit to items that are not taxes (pkgnum > 0).
+- nottax: Limit to items that are not taxes (pkgnum > 0 or feepart > 0).
-- istax: Limit to items that are taxes (pkgnum == 0).
+- istax: Limit to items that are taxes (pkgnum == 0 and feepart = null).
- taxnum: Limit to items whose tax definition matches this taxnum.
With "nottax" that means items that are subject to that tax;
@@ -281,7 +281,8 @@ if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
# we want the package and its definition if available
my $join_pkg =
' LEFT JOIN cust_pkg USING (pkgnum)
- LEFT JOIN part_pkg USING (pkgpart)';
+ LEFT JOIN part_pkg USING (pkgpart)
+ LEFT JOIN part_fee USING (feepart)';
my $part_pkg = 'part_pkg';
# "Separate sub-packages from parents"
@@ -295,12 +296,16 @@ if ( $use_override ) {
$part_pkg = 'override';
}
push @select, "$part_pkg.pkgpart", "$part_pkg.pkg";
-push @select, "$part_pkg.taxclass" if $conf->exists('enable_taxclasses');
+push @select, "COALESCE($part_pkg.taxclass, part_fee.taxclass) AS taxclass"
+ if $conf->exists('enable_taxclasses');
# the non-tax case
if ( $cgi->param('nottax') ) {
- push @where, 'cust_bill_pkg.pkgnum > 0';
+ push @select, "part_fee.itemdesc";
+
+ push @where,
+ '(cust_bill_pkg.pkgnum > 0 OR cust_bill_pkg.feepart IS NOT NULL)';
my @tax_where; # will go into a subquery
my @exempt_where; # will also go into a subquery
@@ -311,7 +316,7 @@ if ( $cgi->param('nottax') ) {
# N: classnum
if ( grep { $_ eq 'classnum' } $cgi->param ) {
my @classnums = grep /^\d+$/, $cgi->param('classnum');
- push @where, "COALESCE($part_pkg.classnum, 0) IN ( ".
+ push @where, "COALESCE(part_fee.classnum, $part_pkg.classnum, 0) IN ( ".
join(',', @classnums ).
' )'
if @classnums;
@@ -336,7 +341,7 @@ if ( $cgi->param('nottax') ) {
# effective taxclass, not the real one
push @tax_where, 'cust_main_county.taxclass IS NULL'
} elsif ( $cgi->param('taxclass') ) {
- push @tax_where, "$part_pkg.taxclass IN (" .
+ push @tax_where, "COALESCE(part_fee.taxclass, $part_pkg.taxclass) IN (" .
join(', ', map {dbh->quote($_)} $cgi->param('taxclass') ).
')';
}
@@ -678,7 +683,7 @@ if ( $cgi->param('salesnum') =~ /^(\d+)$/ ) {
'paid' => ($cgi->param('paid') ? 1 : 0),
'classnum' => scalar($cgi->param('classnum'))
);
- $join_pkg .= " JOIN sales_pkg_class ON ( COALESCE(sales_pkg_class.classnum, 0) = COALESCE( part_pkg.classnum, 0) )";
+ $join_pkg .= " JOIN sales_pkg_class ON ( COALESCE(sales_pkg_class.classnum, 0) = COALESCE( part_fee.classnum, part_pkg.classnum, 0) )";
my $extra_sql = $subsearch->{extra_sql};
$extra_sql =~ s/^WHERE//;
-----------------------------------------------------------------------
Summary of changes:
FS/FS/AccessRight.pm | 3 +
FS/FS/Mason.pm | 4 +
FS/FS/Report/Table.pm | 40 +-
FS/FS/Schema.pm | 78 +++-
FS/FS/TemplateItem_Mixin.pm | 4 +-
FS/FS/Template_Mixin.pm | 30 +-
FS/FS/cust_bill_pkg.pm | 127 +++++-
FS/FS/cust_bill_pkg_fee.pm | 91 ++++
FS/FS/cust_credit.pm | 15 +-
FS/FS/cust_event_fee.pm | 174 +++++++
FS/FS/cust_main/Billing.pm | 210 ++++++---
FS/FS/cust_main_county.pm | 26 +-
FS/FS/part_event/Action/Mixin/fee.pm | 65 +++
FS/FS/part_event/Action/cust_bill_fee.pm | 28 ++
FS/FS/part_event/Action/cust_fee.pm | 18 +
FS/FS/part_event/Action/fee.pm | 10 +-
FS/FS/part_fee.pm | 497 ++++++++++++++++++++
FS/FS/part_fee_msgcat.pm | 127 +++++
FS/FS/part_pkg.pm | 88 +---
FS/FS/part_pkg_taxproduct.pm | 76 +++-
FS/FS/tax_rate.pm | 6 +-
FS/MANIFEST | 9 +
FS/t/cust_bill_pkg_fee.t | 5 +
FS/t/cust_event_fee.t | 5 +
FS/t/part_fee.t | 5 +
FS/t/part_fee_msgcat.t | 5 +
httemplate/browse/part_fee.html | 71 +++
httemplate/edit/credit-cust_bill_pkg.html | 3 +-
httemplate/edit/part_fee.html | 141 ++++++
httemplate/edit/process/part_fee.html | 20 +
httemplate/elements/menu.html | 4 +
httemplate/misc/xmlhttp-calculate_taxes.html | 9 +-
.../xmlhttp-cust_bill_pkg-calculate_taxes.html | 15 +-
httemplate/search/cust_bill_pkg.cgi | 21 +-
34 files changed, 1784 insertions(+), 246 deletions(-)
create mode 100644 FS/FS/cust_bill_pkg_fee.pm
create mode 100644 FS/FS/cust_event_fee.pm
create mode 100644 FS/FS/part_event/Action/Mixin/fee.pm
create mode 100644 FS/FS/part_event/Action/cust_bill_fee.pm
create mode 100644 FS/FS/part_event/Action/cust_fee.pm
create mode 100644 FS/FS/part_fee.pm
create mode 100644 FS/FS/part_fee_msgcat.pm
create mode 100644 FS/t/cust_bill_pkg_fee.t
create mode 100644 FS/t/cust_event_fee.t
create mode 100644 FS/t/part_fee.t
create mode 100644 FS/t/part_fee_msgcat.t
create mode 100644 httemplate/browse/part_fee.html
create mode 100644 httemplate/edit/part_fee.html
create mode 100644 httemplate/edit/process/part_fee.html
More information about the freeside-commits
mailing list