X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_main%2FBilling.pm;h=081dd70f79506a74785d22cc9d0e11c885816f56;hb=2c62268f304f1ec6e8baf89043eb1bd1197bb9a6;hp=69fbda42eb34bef066cb912a0f2158e6364ffdd9;hpb=6bd91fa5feb41ea3294bdaee5fe1c1134f19c330;p=freeside.git diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 69fbda42e..081dd70f7 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -116,8 +116,13 @@ sub bill_and_collect { $options{'actual_time'} ||= time; my $job = $options{'job'}; + my $actual_time = ( $conf->exists('next-bill-ignore-time') + ? day_end( $options{actual_time} ) + : $options{actual_time} + ); + $job->update_statustext('0,cleaning expired packages') if $job; - $error = $self->cancel_expired_pkgs( day_end( $options{actual_time} ) ); + $error = $self->cancel_expired_pkgs( $actual_time ); if ( $error ) { $error = "Error expiring custnum ". $self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } @@ -125,7 +130,7 @@ sub bill_and_collect { else { warn $error; } } - $error = $self->suspend_adjourned_pkgs( day_end( $options{actual_time} ) ); + $error = $self->suspend_adjourned_pkgs( $actual_time ); if ( $error ) { $error = "Error adjourning custnum ". $self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } @@ -133,7 +138,7 @@ sub bill_and_collect { else { warn $error; } } - $error = $self->unsuspend_resumed_pkgs( day_end( $options{actual_time} ) ); + $error = $self->unsuspend_resumed_pkgs( $actual_time ); if ( $error ) { $error = "Error resuming custnum ".$self->custnum. ": $error"; if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; } @@ -187,14 +192,30 @@ sub cancel_expired_pkgs { my @errors = (); - foreach my $cust_pkg ( @cancel_pkgs ) { + CUST_PKG: foreach my $cust_pkg ( @cancel_pkgs ) { my $cpr = $cust_pkg->last_cust_pkg_reason('expire'); - my $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum, + my $error; + + if ( $cust_pkg->change_to_pkgnum ) { + + my $new_pkg = FS::cust_pkg->by_key($cust_pkg->change_to_pkgnum); + if ( !$new_pkg ) { + push @errors, 'can\'t change pkgnum '.$cust_pkg->pkgnum.' to pkgnum '. + $cust_pkg->change_to_pkgnum.'; not expiring'; + next CUST_PKG; + } + $error = $cust_pkg->change( 'cust_pkg' => $new_pkg, + 'unprotect_svcs' => 1 ); + $error = '' if ref $error eq 'FS::cust_pkg'; + + } else { # just cancel it + $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum, 'reason_otaker' => $cpr->otaker, 'time' => $time, ) : () ); + } push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error; } @@ -432,6 +453,24 @@ sub bill { my @part_pkg = $cust_pkg->part_pkg->self_and_bill_linked; $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden); + # if this package was changed from another package, + # and it hasn't been billed since then, + # and package balances are enabled, + if ( $cust_pkg->change_pkgnum + and $cust_pkg->change_date >= ($cust_pkg->last_bill || 0) + and $cust_pkg->change_date < $invoice_time + and $conf->exists('pkg-balances') ) + { + # _transfer_balance will also create the appropriate credit + my @transfer_items = $self->_transfer_balance($cust_pkg); + # $part_pkg[0] is the "real" part_pkg + my $pass = ($cust_pkg->no_auto || $part_pkg[0]->no_auto) ? + 'no_auto' : ''; + push @{ $cust_bill_pkg{$pass} }, @transfer_items; + # treating this as recur, just because most charges are recur... + ${$total_recur{$pass}} += $_->recur foreach @transfer_items; + } + foreach my $part_pkg ( @part_pkg ) { $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill ); @@ -915,6 +954,11 @@ sub _make_lines { $cust_pkg->pkgpart($part_pkg->pkgpart); + my $cmp_time = ( $conf->exists('next-bill-ignore-time') + ? day_end( $time ) + : $time + ); + ### # bill setup ### @@ -923,12 +967,14 @@ sub _make_lines { my $unitsetup = 0; my @setup_discounts = (); my %setup_param = ( 'discounts' => \@setup_discounts ); + my $setup_billed_currency = ''; + my $setup_billed_amount = 0; if ( ! $options{recurring_only} and ! $options{cancel} and ( $options{'resetup'} || ( ! $cust_pkg->setup && ( ! $cust_pkg->start_date - || $cust_pkg->start_date <= day_end($time) + || $cust_pkg->start_date <= $cmp_time ) && ( ! $conf->exists('disable_setup_suspended_pkgs') || ( $conf->exists('disable_setup_suspended_pkgs') && @@ -949,7 +995,13 @@ sub _make_lines { return "$@ running calc_setup for $cust_pkg\n" if $@; - $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh + $unitsetup = $cust_pkg->base_setup() + || $setup; #XXX uuh + + if ( $setup_param{'billed_currency'} ) { + $setup_billed_currency = delete $setup_param{'billed_currency'}; + $setup_billed_amount = delete $setup_param{'billed_amount'}; + } } $cust_pkg->setfield('setup', $time) @@ -969,6 +1021,8 @@ sub _make_lines { my $recur = 0; my $unitrecur = 0; my @recur_discounts = (); + my $recur_billed_currency = ''; + my $recur_billed_amount = 0; my $sdate; if ( ! $cust_pkg->start_date and ( ! $cust_pkg->susp || $cust_pkg->option('suspend_bill',1) @@ -976,7 +1030,7 @@ sub _make_lines { && ! $cust_pkg->option('no_suspend_bill',1) ) and - ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= day_end($time) ) + ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $cmp_time ) || ( $part_pkg->plan eq 'voip_cdr' && $part_pkg->option('bill_every_call') ) @@ -1000,7 +1054,7 @@ sub _make_lines { #over two params! lets at least switch to a hashref for the rest... my $increment_next_bill = ( $part_pkg->freq ne '0' - && ( $cust_pkg->getfield('bill') || 0 ) <= day_end($time) + && ( $cust_pkg->getfield('bill') || 0 ) <= $cmp_time && !$options{cancel} ); my %param = ( %setup_param, @@ -1028,7 +1082,12 @@ sub _make_lines { if ( $@ ); #base_cancel??? - $unitrecur = $cust_pkg->part_pkg->base_recur || $recur; #XXX uuh + $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better + + if ( $param{'billed_currency'} ) { + $recur_billed_currency = delete $param{'billed_currency'}; + $recur_billed_amount = delete $param{'billed_amount'}; + } if ( $increment_next_bill ) { @@ -1145,16 +1204,20 @@ sub _make_lines { push @details, @cust_pkg_detail; 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, - 'details' => \@details, - 'discounts' => [ @setup_discounts, @recur_discounts ], - 'hidden' => $part_pkg->hidden, - 'freq' => $part_pkg->freq, + 'pkgnum' => $cust_pkg->pkgnum, + 'setup' => $setup, + 'unitsetup' => $unitsetup, + 'setup_billed_currency' => $setup_billed_currency, + 'setup_billed_amount' => $setup_billed_amount, + 'recur' => $recur, + 'unitrecur' => $unitrecur, + 'recur_billed_currency' => $recur_billed_currency, + 'recur_billed_amount' => $recur_billed_amount, + 'quantity' => $cust_pkg->quantity, + 'details' => \@details, + 'discounts' => [ @setup_discounts, @recur_discounts ], + 'hidden' => $part_pkg->hidden, + 'freq' => $part_pkg->freq, }; if ( $part_pkg->option('prorate_defer_bill',1) @@ -1166,7 +1229,7 @@ sub _make_lines { $cust_bill_pkg->sdate( $hash{last_bill} ); $cust_bill_pkg->edate( $sdate - 86399 ); #60s*60m*24h-1 $cust_bill_pkg->edate( $time ) if $options{cancel}; - } else { #if ( $part_pkg->recur_temporality eq 'upcoming' ) { + } else { #if ( $part_pkg->recur_temporality eq 'upcoming' ) $cust_bill_pkg->sdate( $sdate ); $cust_bill_pkg->edate( $cust_pkg->bill ); #$cust_bill_pkg->edate( $time ) if $options{cancel}; @@ -1210,24 +1273,107 @@ sub _make_lines { } -# 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. -# -# The most important argument is 'taxlisthash'. This is 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], ... ], -# } -# -# 'cust_main_county' can also be 'tax_rate'. The first object in the array -# is always the cust_main_county or tax_rate identified by the key. -# -# 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. +=item _transfer_balance TO_PKG [ FROM_PKGNUM ] + +Takes one argument, a cust_pkg object that is being billed. This will +be called only if the package was created by a package change, and has +not been billed since the package change, and package balance tracking +is enabled. The second argument can be an alternate package number to +transfer the balance from; this should not be used externally. + +Transfers the balance from the previous package (now canceled) to +this package, by crediting one package and creating an invoice item for +the other. Inserts the credit and returns the invoice item (so that it +can be added to an invoice that's being built). + +If the previous package was never billed, and was also created by a package +change, then this will also transfer the balance from I previous +package, and so on, until reaching a package that either has been billed +or was not created by a package change. + +=cut + +my $balance_transfer_reason; + +sub _transfer_balance { + my $self = shift; + my $cust_pkg = shift; + my $from_pkgnum = shift || $cust_pkg->change_pkgnum; + my $from_pkg = FS::cust_pkg->by_key($from_pkgnum); + + my @transfers; + + # if $from_pkg is not the first package in the chain, and it was never + # billed, walk back + if ( $from_pkg->change_pkgnum and scalar($from_pkg->cust_bill_pkg) == 0 ) { + @transfers = $self->_transfer_balance($cust_pkg, $from_pkg->change_pkgnum); + } + + my $prev_balance = $self->balance_pkgnum($from_pkgnum); + if ( $prev_balance != 0 ) { + $balance_transfer_reason ||= FS::reason->new_or_existing( + 'reason' => 'Package balance transfer', + 'type' => 'Internal adjustment', + 'class' => 'R' + ); + + my $credit = FS::cust_credit->new({ + 'custnum' => $self->custnum, + 'amount' => abs($prev_balance), + 'reasonnum' => $balance_transfer_reason->reasonnum, + '_date' => $cust_pkg->change_date, + }); + + my $cust_bill_pkg = FS::cust_bill_pkg->new({ + 'setup' => 0, + 'recur' => abs($prev_balance), + #'sdate' => $from_pkg->last_bill, # not sure about this + #'edate' => $cust_pkg->change_date, + 'itemdesc' => $self->mt('Previous Balance, [_1]', + $from_pkg->part_pkg->pkg), + }); + + if ( $prev_balance > 0 ) { + # credit the old package, charge the new one + $credit->set('pkgnum', $from_pkgnum); + $cust_bill_pkg->set('pkgnum', $cust_pkg->pkgnum); + } else { + # the reverse + $credit->set('pkgnum', $cust_pkg->pkgnum); + $cust_bill_pkg->set('pkgnum', $from_pkgnum); + } + my $error = $credit->insert; + die "error transferring package balance from #".$from_pkgnum. + " to #".$cust_pkg->pkgnum.": $error\n" if $error; + + push @transfers, $cust_bill_pkg; + } # $prev_balance != 0 + + return @transfers; +} + +=item _handle_taxes PART_PKG TAXLISTHASH CUST_BILL_PKG CUST_PKG TIME PKGPART [ 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. + +The most important argument is 'taxlisthash'. This is 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], ... ], +} + +'cust_main_county' can also be 'tax_rate'. The first object in the array +is always the cust_main_county or tax_rate identified by the key. + +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. + +=cut sub _handle_taxes { my $self = shift;