From 6038f6fe5fe3590bcc8063f15ba8ce4cb6a985dc Mon Sep 17 00:00:00 2001 From: jeff Date: Sat, 2 Aug 2008 04:10:01 +0000 Subject: [PATCH] improve CDR usage presentation --- FS/FS/Schema.pm | 7 +- FS/FS/Upgrade.pm | 3 + FS/FS/cust_bill.pm | 1 + FS/FS/cust_bill_pkg.pm | 15 +- FS/FS/cust_bill_pkg_detail.pm | 73 ++++- FS/FS/cust_main.pm | 644 ++++++++++++++++++++++++------------------ FS/FS/part_pkg.pm | 4 +- FS/FS/part_pkg/voip_cdr.pm | 42 ++- 8 files changed, 489 insertions(+), 300 deletions(-) diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index cc97a4635..06816effe 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -508,14 +508,15 @@ sub tables_hashref { 'cust_bill_pkg_detail' => { 'columns' => [ 'detailnum', 'serial', '', '', '', '', - 'pkgnum', 'int', '', '', '', '', - 'invnum', 'int', '', '', '', '', + 'billpkgnum', 'int', 'NULL', '', '', '', # should not be nullable + 'pkgnum', 'int', 'NULL', '', '', '', # deprecated + 'invnum', 'int', 'NULL', '', '', '', # deprecated 'format', 'char', 'NULL', 1, '', '', 'detail', 'varchar', '', $char_d, '', '', ], 'primary_key' => 'detailnum', 'unique' => [], - 'index' => [ [ 'pkgnum', 'invnum' ] ], + 'index' => [ [ 'billpkgnum' ], [ 'pkgnum', 'invnum' ] ], }, 'cust_credit' => { diff --git a/FS/FS/Upgrade.pm b/FS/FS/Upgrade.pm index 6794f4d9f..b4c79ea50 100644 --- a/FS/FS/Upgrade.pm +++ b/FS/FS/Upgrade.pm @@ -103,6 +103,9 @@ sub upgrade_data { #remove bad pending records 'cust_pay_pending' => [], + #replace invnum and pkgnum with billpkgnum + 'cust_bill_pkg_detail' => [], + ; \%hash; diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index c09d547cd..3aed5a44a 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -2687,6 +2687,7 @@ sub _items_cust_bill_pkg { $cust_pkg->h_labels_short($self->_date); #$cust_bill_pkg->edate, #$cust_bill_pkg->sdate), + @d = () if $cust_bill_pkg->itemdesc; push @d, $cust_bill_pkg->details(%details_opt); push @b, { diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index e92c05dc7..62c0d583a 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -55,7 +55,7 @@ supported: =item edate - ending date of recurring fee -=item itemdesc - Line item description (currentlty used only when pkgnum is 0 or -1) +=item itemdesc - Line item description (overrides normal package description) =item quantity - If not set, defaults to 1 @@ -116,10 +116,9 @@ sub insert { foreach my $detail ( @{$self->get('details')} ) { my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail { - 'pkgnum' => $self->pkgnum, - 'invnum' => $self->invnum, - 'format' => (ref($detail) ? $detail->[0] : '' ), - 'detail' => (ref($detail) ? $detail->[1] : $detail ), + 'billpkgnum' => $self->billpkgnum, + 'format' => (ref($detail) ? $detail->[0] : '' ), + 'detail' => (ref($detail) ? $detail->[1] : $detail ), }; $error = $cust_bill_pkg_detail->insert; if ( $error ) { @@ -292,9 +291,7 @@ sub details { ) } qsearch ({ 'table' => 'cust_bill_pkg_detail', - 'hashref' => { 'pkgnum' => $self->pkgnum, - 'invnum' => $self->invnum, - }, + 'hashref' => { 'billpkgnum' => $self->billpkgnum }, 'order_by' => 'ORDER BY detailnum', }); #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum }); @@ -313,7 +310,7 @@ sub desc { my $self = shift; if ( $self->pkgnum > 0 ) { - $self->part_pkg->pkg; + $self->itemdesc || $self->part_pkg->pkg; } else { $self->itemdesc || 'Tax'; } diff --git a/FS/FS/cust_bill_pkg_detail.pm b/FS/FS/cust_bill_pkg_detail.pm index a69998a42..8a4888894 100644 --- a/FS/FS/cust_bill_pkg_detail.pm +++ b/FS/FS/cust_bill_pkg_detail.pm @@ -1,10 +1,13 @@ package FS::cust_bill_pkg_detail; use strict; -use vars qw( @ISA ); -use FS::Record qw( qsearch qsearchs ); +use vars qw( @ISA $me $DEBUG ); +use FS::Record qw( qsearch qsearchs dbdef ); +use FS::cust_bill_pkg; @ISA = qw(FS::Record); +$me = '[ FS::cust_bill_pkg_detail ]'; +$DEBUG = 0; =head1 NAME @@ -35,9 +38,7 @@ inherits from FS::Record. The following fields are currently supported: =item detailnum - primary key -=item pkgnum - - -=item invnum - +=item billpkgnum - link to cust_bill_pkg =item detail - detail description @@ -102,8 +103,7 @@ sub check { my $self = shift; $self->ut_numbern('detailnum') - || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum') - || $self->ut_foreign_key('invnum', 'cust_bill', 'invnum') + || $self->ut_foreign_key('billpkgnum', 'cust_bill_pkg', 'billpkgnum') || $self->ut_enum('format', [ '', 'C' ] ) || $self->ut_text('detail') || $self->SUPER::check @@ -111,6 +111,65 @@ sub check { } +# _upgrade_data +# +# Used by FS::Upgrade to migrate to a new database. + +sub _upgrade_data { # class method + + my ($class, %opts) = @_; + + warn "$me upgrading $class\n" if $DEBUG; + + if ( defined( dbdef->table($class->table)->column('billpkgnum') ) && + defined( dbdef->table($class->table)->column('invnum') ) && + defined( dbdef->table($class->table)->column('pkgnum') ) + ) { + + warn "$me Checking for unmigrated invoice line item details\n" if $DEBUG; + + my @cbpd = qsearch({ 'table' => $class->table, + 'hashref' => {}, + 'extra_sql' => 'WHERE invnum IS NOT NULL AND '. + 'pkgnum IS NOT NULL', + }); + + if (scalar(@cbpd)) { + warn "$me Found unmigrated invoice line item details\n" if $DEBUG; + + foreach my $cbpd ( @cbpd ) { + my $detailnum = $cbpd->detailnum; + warn "$me Contemplating detail $detailnum\n" if $DEBUG > 1; + my $cust_bill_pkg = + qsearchs({ 'table' => 'cust_bill_pkg', + 'hashref' => { 'invnum' => $cbpd->invnum, + 'pkgnum' => $cbpd->pkgnum, + }, + 'order_by' => 'ORDER BY billpkgnum LIMIT 1', + }); + if ($cust_bill_pkg) { + $cbpd->billpkgnum($cust_bill_pkg->billpkgnum); + $cbpd->invnum(''); + $cbpd->pkgnum(''); + my $error = $cbpd->replace; + + warn "*** WARNING: error replacing line item detail ". + "(cust_bill_pkg_detail) $detailnum: $error ***\n" + if $error; + } else { + warn "Found orphaned line item detail $detailnum during upgrade.\n"; + } + + } # foreach $cbpd + + } # if @cbpd + + } # if billpkgnum, invnum, and pkgnum columns defined + + ''; + +} + =back =head1 BUGS diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 52a3778f6..e7235ffcc 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -2061,6 +2061,7 @@ sub bill { $self->select_for_update; #mutex my @cust_bill_pkg = (); + my @appended_cust_bill_pkg = (); ### # find the packages which are due for billing, find out how much they are @@ -2092,296 +2093,62 @@ sub bill { my $old_cust_pkg = new FS::cust_pkg \%hash; foreach my $part_pkg ( $cust_pkg->part_pkg->self_and_bill_linked ) { - - $cust_pkg->pkgpart($part_pkg->pkgpart); - $cust_pkg->set($_, $hash{$_}) foreach qw( setup last_bill bill ); - - my @details = (); - - my $lineitems = 0; - - ### - # bill setup - ### - - my $setup = 0; - my $unitsetup = 0; - if ( ! $cust_pkg->setup && - ( - ( $conf->exists('disable_setup_suspended_pkgs') && - ! $cust_pkg->getfield('susp') - ) || ! $conf->exists('disable_setup_suspended_pkgs') - ) - || $options{'resetup'} - ) { - - warn " bill setup\n" if $DEBUG > 1; - $lineitems++; - - $setup = eval { $cust_pkg->calc_setup( $time, \@details ) }; - if ( $@ ) { - $dbh->rollback if $oldAutoCommit; - return "$@ running calc_setup for $cust_pkg\n"; - } - - $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh - - $cust_pkg->setfield('setup', $time) - unless $cust_pkg->setup; - #do need it, but it won't get written to the db - #|| $cust_pkg->pkgpart != $real_pkgpart; - - } - - ### - # bill recurring fee - ### - - #XXX unit stuff here too - my $recur = 0; - my $unitrecur = 0; - my $sdate; - if ( $part_pkg->getfield('freq') ne '0' && - ! $cust_pkg->getfield('susp') && - ( $cust_pkg->getfield('bill') || 0 ) <= $time - ) { - - # XXX should this be a package event? probably. events are called - # at collection time at the moment, though... - $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG) - if $part_pkg->can('reset_usage'); - #don't want to reset usage just cause we want a line item?? - #&& $part_pkg->pkgpart == $real_pkgpart; - - warn " bill recur\n" if $DEBUG > 1; - $lineitems++; - - # XXX shared with $recur_prog - $sdate = $cust_pkg->bill || $cust_pkg->setup || $time; - - #over two params! lets at least switch to a hashref for the rest... - my %param = ( 'precommit_hooks' => \@precommit_hooks, ); - - $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details, \%param ) }; - if ( $@ ) { - $dbh->rollback if $oldAutoCommit; - return "$@ running calc_recur for $cust_pkg\n"; - } - - - #change this bit to use Date::Manip? CAREFUL with timezones (see - # mailing list archive) - my ($sec,$min,$hour,$mday,$mon,$year) = - (localtime($sdate) )[0,1,2,3,4,5]; - - #pro-rating magic - if $recur_prog fiddles $sdate, want to use that - # only for figuring next bill date, nothing else, so, reset $sdate again - # here - $sdate = $cust_pkg->bill || $cust_pkg->setup || $time; - $cust_pkg->last_bill($sdate); - - if ( $part_pkg->freq =~ /^\d+$/ ) { - $mon += $part_pkg->freq; - until ( $mon < 12 ) { $mon -= 12; $year++; } - } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) { - my $weeks = $1; - $mday += $weeks * 7; - } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) { - my $days = $1; - $mday += $days; - } elsif ( $part_pkg->freq =~ /^(\d+)h$/ ) { - my $hours = $1; - $hour += $hours; - } else { - $dbh->rollback if $oldAutoCommit; - return "unparsable frequency: ". $part_pkg->freq; - } - $cust_pkg->setfield('bill', - timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year)); - + my $error = + $self->_make_lines( 'part_pkg' => $part_pkg, + 'cust_pkg' => $cust_pkg, + 'precommit_hooks' => \@precommit_hooks, + 'line_items' => \@cust_bill_pkg, + 'appended_line_items' => \@appended_cust_bill_pkg, + 'setup' => \$total_setup, + 'recur' => \$total_recur, + 'tax_matrix' => \%taxlisthash, + 'time' => $time, + 'options' => \%options, + ); + if ($error) { + $dbh->rollback if $oldAutoCommit; + return $error; } - warn "\$setup is undefined" unless defined($setup); - warn "\$recur is undefined" unless defined($recur); - warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill); - - ### - # If there's line items, create em cust_bill_pkg records - # If $cust_pkg has been modified, update it (if we're a real pkgpart) - ### - - if ( $lineitems ) { - - if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) { - # hmm.. and if just the options are modified in some weird price plan? - - warn " package ". $cust_pkg->pkgnum. " modified; updating\n" - if $DEBUG >1; - - my $error = $cust_pkg->replace( $old_cust_pkg, - 'options' => { $cust_pkg->options }, - ); - if ( $error ) { #just in case - $dbh->rollback if $oldAutoCommit; - return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"; - } - } - - $setup = sprintf( "%.2f", $setup ); - $recur = sprintf( "%.2f", $recur ); - if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) { - $dbh->rollback if $oldAutoCommit; - return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum; - } - if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) { - $dbh->rollback if $oldAutoCommit; - return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum; - } - - if ( $setup != 0 || $recur != 0 ) { - - unless ($postal_charge) { - $postal_charge = 1; # try only once - my $postal_pkg = $self->charge_postal_fee(); - if ( $postal_pkg && !ref( $postal_pkg ) ) { - $dbh->rollback if $oldAutoCommit; - return "can't charge postal invoice fee for customer ". - $self->custnum. ": $postal_pkg"; - } - push @cust_pkgs, $postal_pkg if $postal_pkg; - } - - warn " charges (setup=$setup, recur=$recur); adding line items\n" - if $DEBUG > 1; - my $cust_bill_pkg = new FS::cust_bill_pkg { - 'pkgnum' => $cust_pkg->pkgnum, - 'setup' => $setup, - 'unitsetup' => $unitsetup, - 'recur' => $recur, - 'unitrecur' => $unitrecur, - 'quantity' => $cust_pkg->quantity, - 'sdate' => $sdate, - 'edate' => $cust_pkg->bill, - 'details' => \@details, - }; - $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart) - unless $part_pkg->pkgpart == $real_pkgpart; - push @cust_bill_pkg, $cust_bill_pkg; - - $total_setup += $setup; - $total_recur += $recur; - - ### - # handle taxes - ### - - unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) { - - my @taxes = (); - my @taxoverrides = $part_pkg->part_pkg_taxoverride; - - my $prefix = - ( $conf->exists('tax-ship_address') && length($self->ship_last) ) - ? 'ship_' - : ''; - - if ( $conf->exists('enable_taxproducts') - && (scalar(@taxoverrides) || $part_pkg->taxproductnum ) - ) - { - - 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); - } - - my $extra_sql = - "AND (". - join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")"; - - @taxes = qsearch({ 'table' => 'tax_rate', - 'hashref' => { 'geocode' => $geocode, }, - 'extra_sql' => $extra_sql, - }) - if scalar(@taxclassnums); - - - }else{ - - my %taxhash = map { $_ => $self->get("$prefix$_") } - qw( state county country ); - - $taxhash{'taxclass'} = $part_pkg->taxclass; - - @taxes = qsearch( 'cust_main_county', \%taxhash ); - - unless ( @taxes ) { - $taxhash{'taxclass'} = ''; - @taxes = qsearch( 'cust_main_county', \%taxhash ); - } - - #one more try at a whole-country tax rate - unless ( @taxes ) { - $taxhash{$_} = '' foreach qw( state county ); - @taxes = qsearch( 'cust_main_county', \%taxhash ); - } - - } #if $conf->exists('enable_taxproducts') - - # maybe eliminate this entirely, along with all the 0% records - unless ( @taxes ) { - $dbh->rollback if $oldAutoCommit; - 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 = - "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; - } - - 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 ]; - } - } - - - } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP' - - } #if $setup != 0 || $recur != 0 - - } #if $cust_pkg->modified - } #foreach my $part_pkg } #foreach my $cust_pkg + push @cust_bill_pkg, @appended_cust_bill_pkg; + unless ( @cust_bill_pkg ) { #don't create an invoice w/o line items #but do commit any package date cycling that happened $dbh->commit or die $dbh->errstr if $oldAutoCommit; return ''; } + my $postal_pkg = $self->charge_postal_fee(); + if ( $postal_pkg && !ref( $postal_pkg ) ) { + $dbh->rollback if $oldAutoCommit; + return "can't charge postal invoice fee for customer ". + $self->custnum. ": $postal_pkg"; + } + if ( $postal_pkg ) { + foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) { + my $error = + $self->_make_lines( 'part_pkg' => $part_pkg, + 'cust_pkg' => $postal_pkg, + 'precommit_hooks' => \@precommit_hooks, + 'line_items' => \@cust_bill_pkg, + 'appended_line_items' => \@appended_cust_bill_pkg, + 'setup' => \$total_setup, + 'recur' => \$total_recur, + 'tax_matrix' => \%taxlisthash, + 'time' => $time, + 'options' => \%options, + ); + if ($error) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + } + warn "having a look at the taxes we found...\n" if $DEBUG > 2; foreach my $tax ( keys %taxlisthash ) { my $tax_object = shift @{ $taxlisthash{$tax} }; @@ -2512,6 +2279,327 @@ sub bill { ''; #no error } + +sub _make_lines { + my ($self, %params) = @_; + + 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 $precommit_hooks = $params{precommit_hooks} or die "no package specified"; + my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified"; + my $appended_cust_bill_pkg = $params{appended_line_items} + or die "no appended line buffer specified"; + my $total_setup = $params{setup} or die "no setup accumulator specified"; + my $total_recur = $params{recur} or die "no recur accumulator specified"; + my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified"; + my $time = $params{'time'} or die "no time specified"; + my (%options) = %{$params{options}}; #hmmm only for 'resetup' + + my $dbh = dbh; + my $real_pkgpart = $cust_pkg->pkgpart; + my %hash = $cust_pkg->hash; + my $old_cust_pkg = new FS::cust_pkg \%hash; + + $cust_pkg->pkgpart($part_pkg->pkgpart); + $cust_pkg->set($_, $hash{$_}) foreach qw( setup last_bill bill ); + + my @details = (); + + my $lineitems = 0; + + ### + # bill setup + ### + + my $setup = 0; + my $unitsetup = 0; + if ( ! $cust_pkg->setup && + ( + ( $conf->exists('disable_setup_suspended_pkgs') && + ! $cust_pkg->getfield('susp') + ) || ! $conf->exists('disable_setup_suspended_pkgs') + ) + || $options{'resetup'} + ) { + + warn " bill setup\n" if $DEBUG > 1; + $lineitems++; + + $setup = eval { $cust_pkg->calc_setup( $time, \@details ) }; + return "$@ running calc_setup for $cust_pkg\n" + if $@; + + $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh + + $cust_pkg->setfield('setup', $time) + unless $cust_pkg->setup; + #do need it, but it won't get written to the db + #|| $cust_pkg->pkgpart != $real_pkgpart; + + } + + ### + # bill recurring fee + ### + + #XXX unit stuff here too + my $recur = 0; + my $unitrecur = 0; + my $sdate; + if ( $part_pkg->getfield('freq') ne '0' && + ! $cust_pkg->getfield('susp') && + ( $cust_pkg->getfield('bill') || 0 ) <= $time + ) { + + # XXX should this be a package event? probably. events are called + # at collection time at the moment, though... + $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG) + if $part_pkg->can('reset_usage'); + #don't want to reset usage just cause we want a line item?? + #&& $part_pkg->pkgpart == $real_pkgpart; + + warn " bill recur\n" if $DEBUG > 1; + $lineitems++; + + # XXX shared with $recur_prog + $sdate = $cust_pkg->bill || $cust_pkg->setup || $time; + + #over two params! lets at least switch to a hashref for the rest... + my %param = ( 'precommit_hooks' => $precommit_hooks, ); + + $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details, \%param ) }; + return "$@ running calc_recur for $cust_pkg\n" + if ( $@ ); + + + #change this bit to use Date::Manip? CAREFUL with timezones (see + # mailing list archive) + my ($sec,$min,$hour,$mday,$mon,$year) = + (localtime($sdate) )[0,1,2,3,4,5]; + + #pro-rating magic - if $recur_prog fiddles $sdate, want to use that + # only for figuring next bill date, nothing else, so, reset $sdate again + # here + $sdate = $cust_pkg->bill || $cust_pkg->setup || $time; + $cust_pkg->last_bill($sdate); + + if ( $part_pkg->freq =~ /^\d+$/ ) { + $mon += $part_pkg->freq; + until ( $mon < 12 ) { $mon -= 12; $year++; } + } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) { + my $weeks = $1; + $mday += $weeks * 7; + } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) { + my $days = $1; + $mday += $days; + } elsif ( $part_pkg->freq =~ /^(\d+)h$/ ) { + my $hours = $1; + $hour += $hours; + } else { + return "unparsable frequency: ". $part_pkg->freq; + } + $cust_pkg->setfield('bill', + timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year)); + + } + + warn "\$setup is undefined" unless defined($setup); + warn "\$recur is undefined" unless defined($recur); + warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill); + + ### + # If there's line items, create em cust_bill_pkg records + # If $cust_pkg has been modified, update it (if we're a real pkgpart) + ### + + if ( $lineitems ) { + + if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) { + # hmm.. and if just the options are modified in some weird price plan? + + warn " package ". $cust_pkg->pkgnum. " modified; updating\n" + if $DEBUG >1; + + my $error = $cust_pkg->replace( $old_cust_pkg, + 'options' => { $cust_pkg->options }, + ); + return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error" + if $error; #just in case + } + + $setup = sprintf( "%.2f", $setup ); + $recur = sprintf( "%.2f", $recur ); + if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) { + return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum; + } + if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) { + return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum; + } + + if ( $setup != 0 || $recur != 0 ) { + + warn " charges (setup=$setup, recur=$recur); adding line items\n" + if $DEBUG > 1; + my $cust_bill_pkg = new FS::cust_bill_pkg { + 'pkgnum' => $cust_pkg->pkgnum, + 'setup' => $setup, + 'unitsetup' => $unitsetup, + 'recur' => $recur, + 'unitrecur' => $unitrecur, + 'quantity' => $cust_pkg->quantity, + 'sdate' => $sdate, + 'edate' => $cust_pkg->bill, + 'details' => \@details, + }; + $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; + + ### + # handle taxes + ### + + unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) { + + $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg); + + } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP' + + } #if $setup != 0 || $recur != 0 + + } #if $line_items + + if ( $part_pkg->can('append_cust_bill_pkgs') ) { + my %param = ( 'precommit_hooks' => $precommit_hooks, ); + my ($more_cust_bill_pkgs) = + eval { $part_pkg->append_cust_bill_pkgs( $cust_pkg, \$sdate, \%param ) }; + + return "$@ running append_cust_bill_pkgs for $cust_pkg\n" + if ( $@ ); + return "$more_cust_bill_pkgs" + unless ( ref($more_cust_bill_pkgs) ); + + foreach my $cust_bill_pkg ( @{$more_cust_bill_pkgs} ) { + + $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart) + unless $part_pkg->pkgpart == $real_pkgpart; + push @$appended_cust_bill_pkg, $cust_bill_pkg; + + $$total_setup += $cust_bill_pkg->setup; + $$total_recur += $cust_bill_pkg->recur; + + ### + # handle taxes + ### + + unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) { + + $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg); + + } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP' + } + } + +} + +sub _handle_taxes { + my $self = shift; + my $part_pkg = shift; + my $taxlisthash = shift; + my $cust_bill_pkg = shift; + + my @taxes = (); + my @taxoverrides = $part_pkg->part_pkg_taxoverride; + + my $prefix = + ( $conf->exists('tax-ship_address') && length($self->ship_last) ) + ? 'ship_' + : ''; + + if ( $conf->exists('enable_taxproducts') + && (scalar(@taxoverrides) || $part_pkg->taxproductnum ) + ) + { + + 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); + } + + my $extra_sql = + "AND (". + join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")"; + + @taxes = qsearch({ 'table' => 'tax_rate', + 'hashref' => { 'geocode' => $geocode, }, + 'extra_sql' => $extra_sql, + }) + if scalar(@taxclassnums); + + + }else{ + + my %taxhash = map { $_ => $self->get("$prefix$_") } + qw( state county country ); + + $taxhash{'taxclass'} = $part_pkg->taxclass; + + @taxes = qsearch( 'cust_main_county', \%taxhash ); + + unless ( @taxes ) { + $taxhash{'taxclass'} = ''; + @taxes = qsearch( 'cust_main_county', \%taxhash ); + } + + #one more try at a whole-country tax rate + unless ( @taxes ) { + $taxhash{$_} = '' foreach qw( state county ); + @taxes = qsearch( 'cust_main_county', \%taxhash ); + } + + } #if $conf->exists('enable_taxproducts') + + # 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 = + "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; + } + + 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 ]; + } + } + +} + =item collect OPTIONS (Attempt to) collect money for this customer's outstanding invoices (see diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index 536cd8940..3b1cbed16 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -762,7 +762,7 @@ sub option { =item bill_part_pkg_link -Returns the associated part_pkg_link records (see L). =cut @@ -772,6 +772,8 @@ sub bill_part_pkg_link { =item svc_part_pkg_link +Returns the associated part_pkg_link records (see L). + =cut sub svc_part_pkg_link { diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm index c4e6ab2d8..a14db2b2c 100644 --- a/FS/FS/part_pkg/voip_cdr.pm +++ b/FS/FS/part_pkg/voip_cdr.pm @@ -85,6 +85,10 @@ tie my %rating_method, 'Tie::IxHash', 'select_options' => { FS::cdr::invoice_formats() }, }, + 'separate_usage' => { 'name' => 'Separate usage charges from recurring charges', + 'type' => 'checkbox', + }, + #XXX also have option for an external db # 'cdr_location' => { 'name' => 'CDR database location' # 'type' => 'select', @@ -116,6 +120,7 @@ tie my %rating_method, 'Tie::IxHash', disable_src domestic_prefix international_prefix use_amaflags use_disposition output_format + separate_usage ) ], 'weight' => 40, @@ -126,8 +131,16 @@ sub calc_setup { $self->option('setup_fee'); } -#false laziness w/voip_sqlradacct... resolve it if that one ever gets used again sub calc_recur { + my $self = shift; + my $charges = 0; + $charges = $self->calc_usage(@_) + unless $self->option('separate_usage', 'Hush!'); + $self->option('recur_fee') + $charges; +} + +#false laziness w/voip_sqlradacct calc_recur resolve it if that one ever gets used again +sub calc_usage { my($self, $cust_pkg, $sdate, $details, $param ) = @_; my $last_bill = $cust_pkg->last_bill; @@ -425,7 +438,7 @@ sub calc_recur { } #if ( $spool_cdr && length($downstream_cdr) ) - $self->option('recur_fee') + $charges; + $charges; } @@ -445,5 +458,30 @@ sub calc_units { scalar(grep { $_->part_svc->svcdb eq 'svc_phone' } $cust_pkg->cust_svc); } +sub append_cust_bill_pkgs { + my $self = shift; + my($cust_pkg, $sdate, $details, $param ) = @_; + return [] + unless $self->option('separate_usage', 'Hush!'); + + my @details = (); + my $charges = $self->calc_usage($cust_pkg, $sdate, \@details, $param); + + my $cust_bill_pkg = new FS::cust_bill_pkg { + 'pkgnum' => $cust_pkg->pkgnum, + 'setup' => 0, + 'unitsetup' => 0, + 'recur' => sprintf( "%.2f", $charges), # hmmm + 'unitrecur' => 0, + 'quantity' => $cust_pkg->quantity, + 'sdate' => $$sdate, + 'edate' => $cust_pkg->bill, # already fiddled + 'itemdesc' => 'Call details', # configurable? + 'details' => \@details, + }; + + return [ $cust_bill_pkg ]; +} + 1; -- 2.11.0