diff options
Diffstat (limited to 'FS')
-rw-r--r-- | FS/FS/Schema.pm | 4 | ||||
-rw-r--r-- | FS/FS/cust_bill_pkg.pm | 69 | ||||
-rw-r--r-- | FS/FS/cust_main.pm | 196 | ||||
-rw-r--r-- | FS/FS/part_pkg.pm | 97 | ||||
-rw-r--r-- | FS/FS/part_pkg/voip_cdr.pm | 8 |
5 files changed, 309 insertions, 65 deletions
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 3bcd078ab..2043b45ec 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -502,6 +502,7 @@ sub tables_hashref { 'unitrecur', @money_typen, '', '', 'duplicate', 'char', 'NULL', 1, '', '', 'post_total', 'char', 'NULL', 1, '', '', + 'type', 'char', 'NULL', 1, '', '', ], 'primary_key' => 'billpkgnum', 'unique' => [], @@ -514,7 +515,9 @@ sub tables_hashref { 'billpkgnum', 'int', 'NULL', '', '', '', # should not be nullable 'pkgnum', 'int', 'NULL', '', '', '', # deprecated 'invnum', 'int', 'NULL', '', '', '', # deprecated + 'amount', @money_typen, '', '', 'format', 'char', 'NULL', 1, '', '', + 'classnum', 'char', 'NULL', 1, '', '', 'detail', 'varchar', '', $char_d, '', '', ], 'primary_key' => 'detailnum', @@ -1103,6 +1106,7 @@ sub tables_hashref { 'taxoverridenum', 'serial', '', '', '', '', 'pkgpart', 'serial', '', '', '', '', 'taxclassnum', 'serial', '', '', '', '', + 'usage_class', 'varchar', 'NULL', $char_d, '', '', ], 'primary_key' => 'taxoverridenum', 'unique' => [], diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index d3bf65bcb..b481e92a7 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -47,6 +47,8 @@ supported: =item pkgnum - package (see L<FS::cust_pkg>) or 0 for the special virtual sales tax package, or -1 for the virtual line item (itemdesc is used for the line) =item pkgpart_override - optional package definition (see L<FS::part_pkg>) override +=item type - can be set to U for usage; more later + =item setup - setup fee =item recur - recurring fee @@ -137,6 +139,8 @@ sub insert { 'billpkgnum' => $self->billpkgnum, 'format' => (ref($detail) ? $detail->[0] : '' ), 'detail' => (ref($detail) ? $detail->[1] : $detail ), + 'charge' => (ref($detail) ? $detail->[2] : '' ), + 'classnum' => (ref($detail) ? $detail->[3] : '' ), }; $error = $cust_bill_pkg_detail->insert; if ( $error ) { @@ -195,6 +199,7 @@ sub check { || $self->ut_textn('section') || $self->ut_enum('duplicate', [ '', 'Y' ]) || $self->ut_enum('post_total', [ '', 'Y' ]) + || $self->ut_enum('type', [ '', 'U' ]) #only usage for now ; return $error if $error; @@ -458,6 +463,70 @@ sub separate_cdr { $self->pkgnum && $self->section ne $self->part_pkg->categoryname; } +=item usage CLASSNUM + +Returns the amount of the charge associated with usage class CLASSNUM if +CLASSNUM is defined. Otherwise returns the total charge associated with +usage. + +=cut + +sub usage { + my( $self, $classnum ) = @_; + my $sum = 0; + my @values = (); + + if ( $self->get('details') ) { + + @values = + map { $_->[2] } + grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) } + $self->get('details'); + + }else{ + + my $hashref = { 'billpkgnum' => $self->billpkgnum }; + $hashref->{ 'classnum' } = $classnum if defined($classnum); + @values = map { $_->charge } qsearch('cust_bill_pkg_detail', $hashref); + + } + + foreach ( @values ) { + $sum += $_ if $_; + } + $sum; +} + +=item usage_classes + +Returns a list of usage classnums associated with this invoice line's +details. + +=cut + +sub usage_classes { + my( $self ) = @_; + + if ( $self->get('details') ) { + + my %seen = (); + foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) { + $seen{ $detail->[3] } = 1; + } + keys %seen; + + }else{ + + map { $_->classnum } + qsearch({ table => 'cust_bill_pkg_detail', + hashref => { billpkgnum => $self->billpkgnum }, + select => 'DISTINCT classnum', + }); + + } + +} + =back =head1 BUGS diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index cc664f30b..b72079cf1 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -2047,6 +2047,7 @@ Used in conjunction with the I<time> option, this option specifies the date of f sub bill { my( $self, %options ) = @_; return '' if $self->payby eq 'COMP'; + local $DEBUG = 1; warn "$me bill customer ". $self->custnum. "\n" if $DEBUG; @@ -2460,7 +2461,6 @@ sub _make_lines { }; $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart) unless $part_pkg->pkgpart == $real_pkgpart; - push @$cust_bill_pkgs, $cust_bill_pkg; $$total_setup += $setup; $$total_recur += $recur; @@ -2471,7 +2471,14 @@ sub _make_lines { unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) { - $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg); + #some garbage disappears on cust_bill_pkg refactor + my $err_or_cust_bill_pkg = + $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg); + + return $err_or_cust_bill_pkg + unless ( ref($err_or_cust_bill_pkg) ); + + push @$cust_bill_pkgs, @$err_or_cust_bill_pkg; } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP' @@ -2493,7 +2500,6 @@ sub _make_lines { $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart) unless $part_pkg->pkgpart == $real_pkgpart; - push @$appended_cust_bill_pkg, $cust_bill_pkg; unless ($cust_bill_pkg->duplicate) { $$total_setup += $cust_bill_pkg->setup; @@ -2505,7 +2511,14 @@ sub _make_lines { unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) { - $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg); + #some garbage disappears on cust_bill_pkg refactor + my $err_or_cust_bill_pkg = + $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg); + + return $err_or_cust_bill_pkg + unless ( ref($err_or_cust_bill_pkg) ); + + push @$appended_cust_bill_pkg, @$err_or_cust_bill_pkg; } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP' } @@ -2520,39 +2533,35 @@ sub _handle_taxes { my $taxlisthash = shift; my $cust_bill_pkg = shift; - my @taxes = (); - my @taxoverrides = $part_pkg->part_pkg_taxoverride; + my %cust_bill_pkg = (); + my %taxes = (); my $prefix = ( $conf->exists('tax-ship_address') && length($self->ship_last) ) ? 'ship_' : ''; + my @classes; + push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U'; + push @classes, 'setup' if $cust_bill_pkg->setup; + push @classes, 'recur' if $cust_bill_pkg->recur; + if ( $conf->exists('enable_taxproducts') - && (scalar(@taxoverrides) || $part_pkg->taxproductnum ) + && (scalar($part_pkg->part_pkg_taxoverride) || $part_pkg->has_taxproduct) ) { - my @taxclassnums = (); - my $geocode = $self->geocode('cch'); - - if ( scalar( @taxoverrides ) ) { - @taxclassnums = map { $_->taxclassnum } @taxoverrides; - }elsif ( $part_pkg->taxproductnum ) { - @taxclassnums = map { $_->taxclassnum } - $part_pkg->part_pkg_taxrate('cch', $geocode); + foreach my $class (@classes) { + my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $prefix ); + return $err_or_ref unless ref($err_or_ref); + $taxes{$class} = $err_or_ref; } - my $extra_sql = - "AND (". - join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")"; - - @taxes = qsearch({ 'table' => 'tax_rate', - 'hashref' => { 'geocode' => $geocode, }, - 'extra_sql' => $extra_sql, - }) - if scalar(@taxclassnums); - + unless (exists $taxes{''}) { + my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $prefix ); + return $err_or_ref unless ref($err_or_ref); + $taxes{''} = $err_or_ref; + } }else{ @@ -2561,7 +2570,7 @@ sub _handle_taxes { $taxhash{'taxclass'} = $part_pkg->taxclass; - @taxes = qsearch( 'cust_main_county', \%taxhash ); + my @taxes = qsearch( 'cust_main_county', \%taxhash ); unless ( @taxes ) { $taxhash{'taxclass'} = ''; @@ -2574,39 +2583,132 @@ sub _handle_taxes { @taxes = qsearch( 'cust_main_county', \%taxhash ); } - } #if $conf->exists('enable_taxproducts') + $taxes{''} = [ @taxes ]; + $taxes{'setup'} = [ @taxes ]; + $taxes{'recur'} = [ @taxes ]; + $taxes{$_} = [ @taxes ] foreach (@classes); - # maybe eliminate this entirely, along with all the 0% records - unless ( @taxes ) { - my $error; - if ( $conf->exists('enable_taxproducts') ) { - $error = - "fatal: can't find tax rate for zip/taxproduct/pkgpart ". - join('/', ( map $self->get("$prefix$_"), - qw(zip) - ), - $part_pkg->taxproduct_description, - $part_pkg->pkgpart ). "\n"; - } else { - $error = + # maybe eliminate this entirely, along with all the 0% records + unless ( @taxes ) { + return "fatal: can't find tax rate for state/county/country/taxclass ". join('/', ( map $self->get("$prefix$_"), qw(state county country) ), $part_pkg->taxclass ). "\n"; } - return $error; + + } #if $conf->exists('enable_taxproducts') + + # XXX all this goes away with cust_bill_pay refactor + + $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup; + $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur; + + #split setup and recur + if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) { + my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash }; + $cust_bill_pkg->set('details', []); + $cust_bill_pkg->recur(0); + $cust_bill_pkg->type(''); + $cust_bill_pkg{recur} = $cust_bill_pkg_recur; + } + + #split usage from recur + my $usage = $cust_bill_pkg->usage; + if ($usage) { + my $cust_bill_pkg_usage = + new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash }; + $cust_bill_pkg_usage->recur($usage); + $cust_bill_pkg{recur}->recur( $cust_bill_pkg{recur}->recur - $usage ); + $cust_bill_pkg{recur}->type( '' ); + $cust_bill_pkg{''} = $cust_bill_pkg_usage; + } + + #subdivide usage by usage_class + if (exists($cust_bill_pkg{''})) { + foreach my $class (grep {$_} @classes) { + my $usage = $cust_bill_pkg{''}->usage($class); + my $cust_bill_pkg_usage = + new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash }; + $cust_bill_pkg_usage->recur($usage); + $cust_bill_pkg{''}->recur( $cust_bill_pkg{''}->recur - $usage ); + $cust_bill_pkg{$class} = $cust_bill_pkg_usage; + } + $cust_bill_pkg{''}->set('details', []); + delete $cust_bill_pkg{''} unless $cust_bill_pkg{''}->recur; } - foreach my $tax ( @taxes ) { - my $taxname = ref( $tax ). ' '. $tax->taxnum; - if ( exists( $taxlisthash->{ $taxname } ) ) { - push @{ $taxlisthash->{ $taxname } }, $cust_bill_pkg; - }else{ - $taxlisthash->{ $taxname } = [ $tax, $cust_bill_pkg ]; + foreach my $key (keys %cust_bill_pkg) { + my @taxes = @{ $taxes{$key} }; + my $cust_bill_pkg = $cust_bill_pkg{$key}; + + foreach my $tax ( @taxes ) { + my $taxname = ref( $tax ). ' '. $tax->taxnum; + if ( exists( $taxlisthash->{ $taxname } ) ) { + push @{ $taxlisthash->{ $taxname } }, $cust_bill_pkg; + }else{ + $taxlisthash->{ $taxname } = [ $tax, $cust_bill_pkg ]; + } } } + # sort setup,recur,'', and the rest numeric && return + my @result = map { $cust_bill_pkg{$_} } + sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/); + ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a ) + } + keys %cust_bill_pkg; + + \@result; +} + +sub _gather_taxes { + my $self = shift; + my $part_pkg = shift; + my $class = shift; + my $prefix = shift; + + my @taxes = (); + my $geocode = $self->geocode('cch'); + + my @taxclassnums = map { $_->taxclassnum } + $part_pkg->part_pkg_taxoverride($class); + + unless (@taxclassnums) { + @taxclassnums = map { $_->taxclassnum } + $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); + + # maybe eliminate this entirely, along with all the 0% records + unless ( @taxes ) { + return + "fatal: can't find tax rate for zip/taxproduct/pkgpart ". + join('/', ( map $self->get("$prefix$_"), + qw(zip) + ), + $part_pkg->taxproduct_description, + $part_pkg->pkgpart ). "\n"; + } + + warn "Found taxes ". + join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n" + if $DEBUG; + + [ @taxes ]; + } =item collect OPTIONS diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index 3b1cbed16..57145fdd9 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -801,19 +801,84 @@ sub _self_and_linked { ); } -=item part_pkg_taxoverride +=item part_pkg_taxoverride [ CLASS ] Returns all associated FS::part_pkg_taxoverride objects (see -L<FS::part_pkg_taxoverride>). +L<FS::part_pkg_taxoverride>). Limits the returned set to those +of class CLASS if defined. Class may be one of 'setup', 'recur', +the empty string (default), or a usage class number (see L<FS::usage_class>). +When a class is specified, the empty string class (default) is returned +if no more specific values exist. =cut sub part_pkg_taxoverride { my $self = shift; - qsearch('part_pkg_taxoverride', { 'pkgpart' => $self->pkgpart } ); + my $class = shift; + + my $hashref = { 'pkgpart' => $self->pkgpart }; + $hashref->{'usage_class'} = $class if defined($class); + my @overrides = qsearch('part_pkg_taxoverride', $hashref ); + + unless ( scalar(@overrides) || !defined($class) || !$class ){ + $hashref->{'usage_class'} = ''; + @overrides = qsearch('part_pkg_taxoverride', $hashref ); + } + + @overrides; +} + +=item has_taxproduct + +Returns true if this package has any taxproduct associated with it. + +=cut + +sub has_taxproduct { + my $self = shift; + + $self->taxproductnum || + scalar(grep { $_ =~/^usage_taxproductnum_/ } keys %{ {$self->options} } ) + +} + + +=item taxproduct [ CLASS ] + +Returns the associated tax product for this package definition (see +L<FS::part_pkg_taxproduct>). CLASS may be one of 'setup', 'recur' or +the usage classnum (see L<FS::usage_class>). Returns the default +tax product for this record if the more specific CLASS value does +not exist. + +=cut + +sub taxproduct { + my $self = shift; + my $class = shift; + + my $part_pkg_taxproduct; + + my $taxproductnum = $self->taxproductnum; + if ($class) { + my $class_taxproductnum = $self->option("usage_taxproductnum_$class", 1); + $taxproductnum = $class_taxproductnum + if $class_taxproductnum + } + + $part_pkg_taxproduct = + qsearchs( 'part_pkg_taxproduct', { 'taxproductnum' => $taxproductnum } ); + + unless ($part_pkg_taxproduct || $taxproductnum eq $self->taxproductnum ) { + $taxproductnum = $self->taxproductnum; + $part_pkg_taxproduct = + qsearchs( 'part_pkg_taxproduct', { 'taxproductnum' => $taxproductnum } ); + } + + $part_pkg_taxproduct; } -=item taxproduct_description +=item taxproduct_description [ CLASS ] Returns the description of the associated tax product for this package definition (see L<FS::part_pkg_taxproduct>). @@ -822,26 +887,24 @@ definition (see L<FS::part_pkg_taxproduct>). sub taxproduct_description { my $self = shift; - my $part_pkg_taxproduct = - qsearchs( 'part_pkg_taxproduct', - { 'taxproductnum' => $self->taxproductnum } - ); + my $part_pkg_taxproduct = $self->taxproduct(@_); $part_pkg_taxproduct ? $part_pkg_taxproduct->description : ''; } -=item part_pkg_taxrate DATA_PROVIDER, GEOCODE +=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 ). +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>). =cut sub _expand_cch_taxproductnum { my $self = shift; - my $part_pkg_taxproduct = - qsearchs( 'part_pkg_taxproduct', - { 'taxproductnum' => $self->taxproductnum } - ); + my $class = shift; + my $part_pkg_taxproduct = $self->taxproduct($class); + my ($a,$b,$c,$d) = ( $part_pkg_taxproduct ? ( split ':', $part_pkg_taxproduct->taxproduct ) : () @@ -859,7 +922,7 @@ sub _expand_cch_taxproductnum { sub part_pkg_taxrate { my $self = shift; - my ($data_vendor, $geocode) = @_; + my ($data_vendor, $geocode, $class) = @_; my $dbh = dbh; my $extra_sql = 'WHERE part_pkg_taxproduct.data_vendor = '. @@ -873,7 +936,9 @@ sub part_pkg_taxrate { ')'; # much more CCH oddness in m2m -- this is kludgy $extra_sql .= ' AND ('. - join(' OR ', map{ "taxproductnum = $_" } $self->_expand_cch_taxproductnum). + join(' OR ', map{ "taxproductnum = $_" } + $self->_expand_cch_taxproductnum($class) + ). ')'; my $addl_from = 'LEFT JOIN part_pkg_taxproduct USING ( taxproductnum )'; diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm index 991c33cf6..10257056d 100644 --- a/FS/FS/part_pkg/voip_cdr.pm +++ b/FS/FS/part_pkg/voip_cdr.pm @@ -187,6 +187,7 @@ sub calc_usage { my( $rate_region, $regionnum ); my $pretty_destnum; my $charge = ''; + my $classnum = ''; my @call_details = (); if ( $self->option('rating_method') eq 'prefix' || ! $self->option('rating_method') @@ -387,15 +388,18 @@ sub calc_usage { $rate_region->regionname, ); + $classnum = $rate_detail->classnum; + } if ( $charge > 0 ) { + #just use FS::cust_bill_pkg_detail objects? my $call_details; if ( $self->option('rating_method') eq 'upstream_simple' ) { - $call_details = [ 'C', $call_details[0] ]; + $call_details = [ 'C', $call_details[0], $charge, $classnum ]; }else{ $csv->combine(@call_details); - $call_details = [ 'C', $csv->string ]; + $call_details = [ 'C', $csv->string, $charge, $classnum ]; } warn " adding details on charge to invoice: [ ". join(', ', @{$call_details} ). " ]" |