summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
authorjeff <jeff>2008-08-02 04:10:01 +0000
committerjeff <jeff>2008-08-02 04:10:01 +0000
commit6038f6fe5fe3590bcc8063f15ba8ce4cb6a985dc (patch)
tree8aab827e01ca373c0b90b57d970b9e08ec841489 /FS
parent8cbef37a14f86dfb6313284cdc7b131a3072393f (diff)
improve CDR usage presentation
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/Schema.pm7
-rw-r--r--FS/FS/Upgrade.pm3
-rw-r--r--FS/FS/cust_bill.pm1
-rw-r--r--FS/FS/cust_bill_pkg.pm15
-rw-r--r--FS/FS/cust_bill_pkg_detail.pm73
-rw-r--r--FS/FS/cust_main.pm644
-rw-r--r--FS/FS/part_pkg.pm4
-rw-r--r--FS/FS/part_pkg/voip_cdr.pm42
8 files changed, 489 insertions, 300 deletions
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index cc97a46..06816ef 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 6794f4d..b4c79ea 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 c09d547..3aed5a4 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 e92c05d..62c0d58 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 a69998a..8a48888 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 52a3778..e7235ff 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 536cd89..3b1cbed 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<FS::part_pkg_link).
+Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
=cut
@@ -772,6 +772,8 @@ sub bill_part_pkg_link {
=item svc_part_pkg_link
+Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
+
=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 c4e6ab2..a14db2b 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;