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
# 2 traces progress of some operations
sub bill_and_collect {
my( $self, %options ) = @_;
+ my $log = FS::Log->new('FS::cust_main::Billing::bill_and_collect');
+ my %logopt = (object => $self);
+ $log->debug('start', %logopt);
+
my $error;
#$options{actual_time} not $options{time} because freeside-daily -d is for
$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} ) );
+ $log->debug('canceling expired packages', %logopt);
+ $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; }
else { warn $error; }
}
- $error = $self->suspend_adjourned_pkgs( day_end( $options{actual_time} ) );
+ $log->debug('suspending adjourned packages', %logopt);
+ $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; }
else { warn $error; }
}
- $error = $self->unsuspend_resumed_pkgs( day_end( $options{actual_time} ) );
+ $log->debug('unsuspending resumed packages', %logopt);
+ $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; }
}
$job->update_statustext('20,billing packages') if $job;
+ $log->debug('billing packages', %logopt);
$error = $self->bill( %options );
if ( $error ) {
$error = "Error billing custnum ". $self->custnum. ": $error";
}
$job->update_statustext('50,applying payments and credits') if $job;
+ $log->debug('applying payments and credits', %logopt);
$error = $self->apply_payments_and_credits;
if ( $error ) {
$error = "Error applying custnum ". $self->custnum. ": $error";
else { warn $error; }
}
- $job->update_statustext('70,running collection events') if $job;
unless ( $conf->exists('cancelled_cust-noevents')
&& ! $self->num_ncancelled_pkgs
) {
+ $job->update_statustext('70,running collection events') if $job;
+ $log->debug('running collection events', %logopt);
$error = $self->collect( %options );
if ( $error ) {
$error = "Error collecting custnum ". $self->custnum. ": $error";
else { warn $error; }
}
}
+
$job->update_statustext('100,finished') if $job;
+ $log->debug('finish', %logopt);
'';
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;
}
A hashref of pkgparts to exclude from this billing run (can also be specified as a comma-separated scalar).
+=item no_prepaid
+
+Do not bill prepaid packages. Used by freeside-daily.
+
=item invoice_time
Used in conjunction with the I<time> option, this option specifies the date of for the generated invoices. Other calculations, such as whether or not to generate the invoice in the first place, are not affected.
return '' if $self->payby eq 'COMP';
local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+ my $log = FS::Log->new('FS::cust_main::Billing::bill');
+ my %logopt = (object => $self);
+ $log->debug('start', %logopt);
warn "$me bill customer ". $self->custnum. "\n"
if $DEBUG;
my $time = $options{'time'} || time;
my $invoice_time = $options{'invoice_time'} || $time;
+ my $cmp_time = ( $conf->exists('next-bill-ignore-time')
+ ? day_end( $time )
+ : $time
+ );
+
$options{'not_pkgpart'} ||= {};
$options{'not_pkgpart'} = { map { $_ => 1 }
split(/\s*,\s*/, $options{'not_pkgpart'})
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ $log->debug('acquiring lock', %logopt);
warn "$me acquiring lock on customer ". $self->custnum. "\n"
if $DEBUG;
$self->select_for_update; #mutex
+ $log->debug('running pre-bill events', %logopt);
warn "$me running pre-bill events for customer ". $self->custnum. "\n"
if $DEBUG;
return $error;
}
+ $log->debug('done running pre-bill events', %logopt);
warn "$me done running pre-bill events for customer ". $self->custnum. "\n"
if $DEBUG;
my @precommit_hooks = ();
$options{'pkg_list'} ||= [ $self->ncancelled_pkgs ]; #param checks?
+
foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
+ my $part_pkg = $cust_pkg->part_pkg;
+
+ next if $options{'no_prepaid'} && $part_pkg->is_prepaid;
+
+ $log->debug('bill package '. $cust_pkg->pkgnum, %logopt);
warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG;
#? to avoid use of uninitialized value errors... ?
$cust_pkg->setfield('bill', '')
unless defined($cust_pkg->bill);
- #my $part_pkg = $cust_pkg->part_pkg;
-
my $real_pkgpart = $cust_pkg->pkgpart;
my %hash = $cust_pkg->hash;
# we could implement this bit as FS::part_pkg::has_hidden, but we already
# suffer from performance issues
$options{has_hidden} = 0;
- my @part_pkg = $cust_pkg->part_pkg->self_and_bill_linked;
+ my @part_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 );
my $next_bill = $cust_pkg->getfield('bill') || 0;
my $error;
# let this run once if this is the last bill upon cancellation
- while ( $next_bill <= $time or $options{cancel} ) {
+ while ( $next_bill <= $cmp_time or $options{cancel} ) {
$error =
$self->_make_lines( 'part_pkg' => $part_pkg,
'cust_pkg' => $cust_pkg,
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')
+ or $object->isa('FS::cust_pkg')
+ or $object->isa('FS::cust_pay_batch') )
+ {
+ # 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} },
+ });
+
+ # If this is a package event, only apply the fee to line items
+ # from that package.
+ if ($object->isa('FS::cust_pkg')) {
+ $cust_bill->set('cust_bill_pkg',
+ [ grep { $_->pkgnum == $object->pkgnum } @cust_bill_pkg ]
+ );
+ }
+
+ } 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 = $part_fee->lineitem($cust_bill) or next;
+ # 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
+ # probably not right to pass cancel => 1 for fees
+ );
+ 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')
)
my $charged = sprintf('%.2f', ${ $total_setup{$pass} } + ${ $total_recur{$pass} } );
- 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 = qsearchs({ 'table' => 'cust_bill',
+ 'hashref' => { custnum=>$self->custnum },
+ 'extra_sql' => 'ORDER BY _date DESC LIMIT 1',
+ });
+ my $previous_balance =
+ $previous_bill
+ ? ( $previous_bill->billing_balance + $previous_bill->charged )
+ : 0;
+ $log->debug('creating the new invoice', %logopt);
warn "creating the new invoice\n" if $DEBUG;
#create the new invoice
my $cust_bill = new FS::cust_bill ( {
=item calculate_taxes LINEITEMREF TAXHASHREF INVOICE_TIME
-This is a weird one. Perhaps it should not even be exposed.
-
Generates tax line items (see L<FS::cust_bill_pkg>) for this customer.
Usually used internally by bill method B<bill>.
# values are arrayrefs of cust_bill_pkg_tax_rate_location hashrefs
my %tax_rate_location = ();
- # keys are taxnums (not internal identifiers!)
+ # keys are taxlisthash keys (internal identifiers!)
# values are arrayrefs of cust_tax_exempt_pkg objects
my %tax_exemption;
foreach my $tax ( keys %$taxlisthash ) {
- # $tax is a tax identifier
+ # $tax is a tax identifier (intersection of a tax definition record
+ # and a cust_bill_pkg record)
my $tax_object = shift @{ $taxlisthash->{$tax} };
# $tax_object is a cust_main_county or tax_rate
- # (with pkgnum and locationnum set)
- # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg objects
+ # (with billpkgnum, pkgnum, locationnum set)
+ # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg component objects
+ # (setup, recurring, usage classes)
warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
warn " ". join('/', @{ $taxlisthash->{$tax} } ). "\n" if $DEBUG > 2;
# taxline calculates the tax on all cust_bill_pkgs in the
# It also calculates exemptions and attaches them to the cust_bill_pkgs
# in the argument.
my $taxables = $taxlisthash->{$tax};
- my $exemptions = $tax_exemption{$tax_object->taxnum} ||= [];
- my $hashref_or_error =
- $tax_object->taxline( $taxables,
+ my $exemptions = $tax_exemption{$tax} ||= [];
+ my $taxline = $tax_object->taxline(
+ $taxables,
'custnum' => $self->custnum,
'invoice_time' => $invoice_time,
'exemptions' => $exemptions,
);
- return $hashref_or_error unless ref($hashref_or_error);
-
- # then collect any new exemptions generated for this tax
- push @$exemptions, @{ $_->cust_tax_exempt_pkg }
- foreach @$taxables;
+ return $taxline unless ref($taxline);
unshift @{ $taxlisthash->{$tax} }, $tax_object;
- my $name = $hashref_or_error->{'name'};
- my $amount = $hashref_or_error->{'amount'};
+ if ( $tax_object->isa('FS::cust_main_county') ) {
+ # then $taxline is a real line item
+ push @{ $taxname{ $taxline->itemdesc } }, $taxline;
- #warn "adding $amount as $name\n";
- $taxname{ $name } ||= [];
- push @{ $taxname{ $name } }, $tax;
+ } else {
+ # leave this as is for now
- $tax_amount{ $tax } += $amount;
+ my $name = $taxline->{'name'};
+ my $amount = $taxline->{'amount'};
- # link records between cust_main_county/tax_rate and cust_location
- $tax_location{ $tax } ||= [];
- $tax_rate_location{ $tax } ||= [];
- if ( ref($tax_object) eq 'FS::cust_main_county' ) {
- push @{ $tax_location{ $tax } },
- {
- 'taxnum' => $tax_object->taxnum,
- 'taxtype' => ref($tax_object),
- 'pkgnum' => $tax_object->get('pkgnum'),
- 'locationnum' => $tax_object->get('locationnum'),
- 'amount' => sprintf('%.2f', $amount ),
- };
- }
- elsif ( ref($tax_object) eq 'FS::tax_rate' ) {
+ #warn "adding $amount as $name\n";
+ $taxname{ $name } ||= [];
+ push @{ $taxname{ $name } }, $tax;
+
+ $tax_amount{ $tax } += $amount;
+
+ # link records between cust_main_county/tax_rate and cust_location
+ $tax_rate_location{ $tax } ||= [];
my $taxratelocationnum =
$tax_object->tax_rate_location->taxratelocationnum;
push @{ $tax_rate_location{ $tax } },
'locationtaxid' => $tax_object->location,
'taxratelocationnum' => $taxratelocationnum,
};
- }
-
- }
-
- #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
- my %packagemap = map { $_->pkgnum => $_ } @$cust_bill_pkg;
- foreach my $tax ( keys %$taxlisthash ) {
- my $taxables = $taxlisthash->{$tax};
- my $tax_object = shift @$taxables; # the rest are line items
- foreach my $cust_bill_pkg ( @$taxables ) {
- next unless ref($cust_bill_pkg) eq 'FS::cust_bill_pkg';
-
- my @cust_tax_exempt_pkg = splice @{ $cust_bill_pkg->cust_tax_exempt_pkg };
-
- next unless @cust_tax_exempt_pkg;
- # get the non-disintegrated version
- my $real_cust_bill_pkg = $packagemap{$cust_bill_pkg->pkgnum}
- or die "can't distribute tax exemptions: no line item for ".
- Dumper($_). " in packagemap ".
- join(',', sort {$a<=>$b} keys %packagemap). "\n";
-
- push @{ $real_cust_bill_pkg->cust_tax_exempt_pkg },
- @cust_tax_exempt_pkg;
- }
- }
+ } #if ref($tax_object)...
+ } #foreach keys %$taxlisthash
#consolidate and create tax line items
warn "consolidating and generating...\n" if $DEBUG > 2;
foreach my $taxname ( keys %taxname ) {
+ my @cust_bill_pkg_tax_location;
+ my @cust_bill_pkg_tax_rate_location;
+ my $tax_cust_bill_pkg = FS::cust_bill_pkg->new({
+ 'pkgnum' => 0,
+ 'recur' => 0,
+ 'sdate' => '',
+ 'edate' => '',
+ 'itemdesc' => $taxname,
+ 'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
+ 'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location,
+ });
+
my $tax_total = 0;
my %seen = ();
- my @cust_bill_pkg_tax_location = ();
- my @cust_bill_pkg_tax_rate_location = ();
warn "adding $taxname\n" if $DEBUG > 1;
foreach my $taxitem ( @{ $taxname{$taxname} } ) {
- next if $seen{$taxitem}++;
- warn "adding $tax_amount{$taxitem}\n" if $DEBUG > 1;
- $tax_total += $tax_amount{$taxitem};
- push @cust_bill_pkg_tax_location,
- map { new FS::cust_bill_pkg_tax_location $_ }
- @{ $tax_location{ $taxitem } };
- push @cust_bill_pkg_tax_rate_location,
- map { new FS::cust_bill_pkg_tax_rate_location $_ }
- @{ $tax_rate_location{ $taxitem } };
+ if ( ref($taxitem) eq 'FS::cust_bill_pkg' ) {
+ # then we need to transfer the amount and the links from the
+ # line item to the new one we're creating.
+ $tax_total += $taxitem->setup;
+ foreach my $link ( @{ $taxitem->get('cust_bill_pkg_tax_location') } ) {
+ $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg);
+ push @cust_bill_pkg_tax_location, $link;
+ }
+ } else {
+ # the tax_rate way
+ next if $seen{$taxitem}++;
+ warn "adding $tax_amount{$taxitem}\n" if $DEBUG > 1;
+ $tax_total += $tax_amount{$taxitem};
+ push @cust_bill_pkg_tax_rate_location,
+ map { new FS::cust_bill_pkg_tax_rate_location $_ }
+ @{ $tax_rate_location{ $taxitem } };
+ }
}
next unless $tax_total;
+ # we should really neverround this up...I guess it's okay if taxline
+ # already returns amounts with 2 decimal places
$tax_total = sprintf('%.2f', $tax_total );
+ $tax_cust_bill_pkg->set('setup', $tax_total);
my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
'disabled' => '',
push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
}
+ $tax_cust_bill_pkg->set('display', \@display);
- push @tax_line_items, new FS::cust_bill_pkg {
- 'pkgnum' => 0,
- 'setup' => $tax_total,
- 'recur' => 0,
- 'sdate' => '',
- 'edate' => '',
- 'itemdesc' => $taxname,
- 'display' => \@display,
- 'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
- 'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location,
- };
-
+ push @tax_line_items, $tax_cust_bill_pkg;
}
\@tax_line_items;
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";
$cust_pkg->pkgpart($part_pkg->pkgpart);
+ my $cmp_time = ( $conf->exists('next-bill-ignore-time')
+ ? day_end( $time )
+ : $time
+ );
+
###
# bill setup
###
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') &&
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)
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)
- || ( $part_pkg->option('suspend_bill', 1) )
- && ! $cust_pkg->option('no_suspend_bill',1)
- )
+ and
+ ( ! $cust_pkg->susp
+ || ( $cust_pkg->susp != $cust_pkg->order_date
+ && ( $cust_pkg->option('suspend_bill',1)
+ || ( $part_pkg->option('suspend_bill', 1)
+ && ! $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')
)
#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,
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 ) {
- my $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
+ my $next_bill;
+
+ if ( my $main_pkg = $cust_pkg->main_pkg ) {
+ # supplemental package
+ # to keep in sync with the main package, simulate billing at
+ # its frequency
+ my $main_pkg_freq = $main_pkg->part_pkg->freq;
+ my $supp_pkg_freq = $part_pkg->freq;
+ my $ratio = $supp_pkg_freq / $main_pkg_freq;
+ if ( $ratio != int($ratio) ) {
+ # the UI should prevent setting up packages like this, but just
+ # in case
+ return "supplemental package period is not an integer multiple of main package period";
+ }
+ $next_bill = $sdate;
+ for (1..$ratio) {
+ $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq );
+ }
+
+ } else {
+ # the normal case
+ $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
return "unparsable frequency: ". $part_pkg->freq
if $next_bill == -1;
+ }
#pro-rating magic - if $recur_prog fiddled $sdate, want to use that
# only for figuring next bill date, nothing else, so, reset $sdate again
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)
$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};
# handle taxes
###
- unless ( $discount_show_always ) {
- my $error =
- $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart, \%options);
- return $error if $error;
- }
+ my $error = $self->_handle_taxes( $taxlisthash, $cust_bill_pkg,
+ cancel => $options{cancel} );
+ return $error if $error;
+
+ $cust_bill_pkg->set_display(
+ part_pkg => $part_pkg,
+ real_pkgpart => $real_pkgpart,
+ );
push @$cust_bill_pkgs, $cust_bill_pkg;
}
-sub _handle_taxes {
+=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<its> 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 $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 $from_pkgnum = shift || $cust_pkg->change_pkgnum;
+ my $from_pkg = FS::cust_pkg->by_key($from_pkgnum);
- local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+ my @transfers;
- my %cust_bill_pkg = ();
- my %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;
- push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
- push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
-
- my $exempt = $conf->exists('cust_class-tax_exempt')
- ? ( $self->cust_class ? $self->cust_class->tax : '' )
- : $self->tax;
- # standardize this just to be sure
- $exempt = ($exempt eq 'Y') ? 'Y' : '';
-
- #if ( $exempt !~ /Y/i && $self->payby ne 'COMP' ) {
- if ( $self->payby ne 'COMP' ) {
-
- if ( $conf->exists('enable_taxproducts')
- && ( scalar($part_pkg->part_pkg_taxoverride)
- || $part_pkg->has_taxproduct
- )
- )
- {
+ # 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);
+ }
- if ( !$exempt ) {
+ 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;
- foreach my $class (@classes) {
- my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $cust_pkg );
- return $err_or_ref unless ref($err_or_ref);
- $taxes{$class} = $err_or_ref;
- }
+ push @transfers, $cust_bill_pkg;
+ } # $prev_balance != 0
- unless (exists $taxes{''}) {
- my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
- return $err_or_ref unless ref($err_or_ref);
- $taxes{''} = $err_or_ref;
- }
+ return @transfers;
+}
- }
+=item handle_taxes TAXLISTHASH CUST_BILL_PKG [ OPTIONS ]
- } else { # cust_main_county tax system
+This is _handle_taxes. It's called once for each cust_bill_pkg generated
+from _make_lines.
- # We fetch taxes even if the customer is completely exempt,
- # because we need to record that fact.
+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], ... ],
+}
- my @loc_keys = qw( district city county state country );
- my $location = $cust_pkg->tax_location;
- my %taxhash = map { $_ => $location->$_ } @loc_keys;
+'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.
- $taxhash{'taxclass'} = $part_pkg->taxclass;
+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.
- warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
+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.
+- cancel: true if this package is being billed on cancellation. This
+ allows tax to be calculated on usage charges only.
- my @taxes = (); # entries are cust_main_county objects
- my %taxhash_elim = %taxhash;
- my @elim = qw( district city county state );
- do {
+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).
- #first try a match with taxclass
- @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
+=cut
- if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
- #then try a match without taxclass
- my %no_taxclass = %taxhash_elim;
- $no_taxclass{ 'taxclass' } = '';
- @taxes = qsearch( 'cust_main_county', \%no_taxclass );
- }
+sub _handle_taxes {
+ my $self = shift;
+ my $taxlisthash = shift;
+ my $cust_bill_pkg = shift;
+ my %options = @_;
+
+ # 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;
+
+ 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_item->part_pkg_taxoverride)
+ || $part_item->has_taxproduct
+ )
+ )
+ {
+
+ # EXTERNAL TAX RATES (via tax_rate)
+ my %cust_bill_pkg = ();
+ my %taxes = ();
- $taxhash_elim{ shift(@elim) } = '';
+ my @classes;
+ push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
+ push @classes, 'setup' if $cust_bill_pkg->setup and !$options{cancel};
+ push @classes, 'recur' if $cust_bill_pkg->recur and !$options{cancel};
- } while ( !scalar(@taxes) && scalar(@elim) );
+ my $exempt = $conf->exists('cust_class-tax_exempt')
+ ? ( $self->cust_class ? $self->cust_class->tax : '' )
+ : $self->tax;
+ # standardize this just to be sure
+ $exempt = ($exempt eq 'Y') ? 'Y' : '';
+
+ if ( !$exempt ) {
- foreach (@taxes) {
- # These could become cust_bill_pkg_tax_location records,
- # or cust_tax_exempt_pkg. We'll decide later.
- $_->set('pkgnum', $cust_pkg->pkgnum);
- $_->set('locationnum', $cust_pkg->tax_locationnum);
+ foreach my $class (@classes) {
+ 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;
}
- $taxes{''} = [ @taxes ];
- $taxes{'setup'} = [ @taxes ];
- $taxes{'recur'} = [ @taxes ];
- $taxes{$_} = [ @taxes ] foreach (@classes);
-
- # # 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 $taxhash{$_}, qw(state county country taxclass) );
- # }
-
- } #if $conf->exists('enable_taxproducts') ...
-
- } # if $self->payby eq 'COMP'
-
- #what's this doing in the middle of _handle_taxes? probably should split
- #this into three parts above in _make_lines
- $cust_bill_pkg->set_display( part_pkg => $part_pkg,
- real_pkgpart => $real_pkgpart,
- );
-
- my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
- foreach my $key (keys %tax_cust_bill_pkg) {
- # $key is "setup", "recur", or a usage class name. ('' is a usage class.)
- # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of
- # the line item.
- # $taxes{$key} is an arrayref of cust_main_county or tax_rate objects that
- # apply to $key-class charges.
- my @taxes = @{ $taxes{$key} || [] };
- my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
-
- my %localtaxlisthash = ();
- foreach my $tax ( @taxes ) {
-
- # this is the tax identifier, not the taxname
- my $taxname = ref( $tax ). ' '. $tax->taxnum;
- $taxname .= ' pkgnum'. $cust_pkg->pkgnum;
- # We need to create a separate $taxlisthash entry for each pkgnum
- # on the invoice, so that cust_bill_pkg_tax_location records will
- # be linked correctly.
-
- # $taxlisthash: keys are "setup", "recur", and usage classes.
- # Values are arrayrefs, first the tax object (cust_main_county
- # or tax_rate) and then any cust_bill_pkg objects that the
- # tax applies to.
- $taxlisthash->{ $taxname } ||= [ $tax ];
- push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
-
- $localtaxlisthash{ $taxname } ||= [ $tax ];
- push @{ $localtaxlisthash{ $taxname } }, $tax_cust_bill_pkg;
+ unless (exists $taxes{''}) {
+ my $err_or_ref = $self->_gather_taxes($part_item, '', $location);
+ return $err_or_ref unless ref($err_or_ref);
+ $taxes{''} = $err_or_ref;
+ }
}
- warn "finding taxed taxes...\n" if $DEBUG > 2;
- foreach my $tax ( keys %localtaxlisthash ) {
- my $tax_object = shift @{ $localtaxlisthash{$tax} };
- warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
- if $DEBUG > 2;
- next unless $tax_object->can('tax_on_tax');
+ my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate; # grrr
+ foreach my $key (keys %tax_cust_bill_pkg) {
+ # $key is "setup", "recur", or a usage class name. ('' is a usage class.)
+ # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of
+ # the line item.
+ # $taxes{$key} is an arrayref of cust_main_county or tax_rate objects that
+ # apply to $key-class charges.
+ my @taxes = @{ $taxes{$key} || [] };
+ my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
+
+ my %localtaxlisthash = ();
+ foreach my $tax ( @taxes ) {
+
+ # this is the tax identifier, not the taxname
+ my $taxname = ref( $tax ). ' '. $tax->taxnum;
+ # $taxlisthash: keys are "setup", "recur", and usage classes.
+ # Values are arrayrefs, first the tax object (cust_main_county
+ # or tax_rate) and then any cust_bill_pkg objects that the
+ # tax applies to.
+ $taxlisthash->{ $taxname } ||= [ $tax ];
+ push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
+
+ $localtaxlisthash{ $taxname } ||= [ $tax ];
+ push @{ $localtaxlisthash{ $taxname } }, $tax_cust_bill_pkg;
- foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
- my $totname = ref( $tot ). ' '. $tot->taxnum;
+ }
- warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
+ warn "finding taxed taxes...\n" if $DEBUG > 2;
+ foreach my $tax ( keys %localtaxlisthash ) {
+ my $tax_object = shift @{ $localtaxlisthash{$tax} };
+ warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
if $DEBUG > 2;
- next unless exists( $localtaxlisthash{ $totname } ); # only increase
- # existing taxes
- warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
- my $hashref_or_error =
- $tax_object->taxline( $localtaxlisthash{$tax},
- 'custnum' => $self->custnum,
- 'invoice_time' => $invoice_time,
- );
- return $hashref_or_error
- unless ref($hashref_or_error);
-
- $taxlisthash->{ $totname } ||= [ $tot ];
- push @{ $taxlisthash->{ $totname } }, $hashref_or_error->{amount};
+ next unless $tax_object->can('tax_on_tax');
+
+ foreach my $tot ( $tax_object->tax_on_tax( $location ) ) {
+ my $totname = ref( $tot ). ' '. $tot->taxnum;
+
+ warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
+ if $DEBUG > 2;
+ next unless exists( $localtaxlisthash{ $totname } ); # only increase
+ # existing 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} );
+ return $hashref_or_error
+ unless ref($hashref_or_error);
+
+ # and append it to the list of taxable items
+ $taxlisthash->{ $totname } ||= [ $tot ];
+ push @{ $taxlisthash->{ $totname } }, $hashref_or_error->{amount};
+ }
}
}
- }
+ } else {
- '';
-}
+ # INTERNAL TAX RATES (cust_main_county)
-sub _gather_taxes {
- my $self = shift;
- my $part_pkg = shift;
- my $class = shift;
- my $cust_pkg = shift;
+ # We fetch taxes even if the customer is completely exempt,
+ # because we need to record that fact.
- local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+ my @loc_keys = qw( district city county state country );
+ my %taxhash = map { $_ => $location->$_ } @loc_keys;
- my $geocode;
- if ( $cust_pkg->locationnum && $conf->exists('tax-pkg_address') ) {
- $geocode = $cust_pkg->cust_location->geocode('cch');
- } else {
- $geocode = $self->geocode('cch');
- }
+ $taxhash{'taxclass'} = $part_item->taxclass;
+
+ warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
- my @taxes = ();
+ my @taxes = (); # entries are cust_main_county objects
+ my %taxhash_elim = %taxhash;
+ my @elim = qw( district city county state );
+ do {
- my @taxclassnums = map { $_->taxclassnum }
- $part_pkg->part_pkg_taxoverride($class);
+ #first try a match with taxclass
+ @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
+
+ if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
+ #then try a match without taxclass
+ my %no_taxclass = %taxhash_elim;
+ $no_taxclass{ 'taxclass' } = '';
+ @taxes = qsearch( 'cust_main_county', \%no_taxclass );
+ }
+
+ $taxhash_elim{ shift(@elim) } = '';
+
+ } while ( !scalar(@taxes) && scalar(@elim) );
+
+ foreach (@taxes) {
+ my $tax_id = 'cust_main_county '.$_->taxnum;
+ $taxlisthash->{$tax_id} ||= [ $_ ];
+ push @{ $taxlisthash->{$tax_id} }, $cust_bill_pkg;
+ }
- 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 ). ")";
+=item _gather_taxes PART_ITEM CLASS CUST_LOCATION
- @taxes = qsearch({ 'table' => 'tax_rate',
- 'hashref' => { 'geocode' => $geocode, },
- 'extra_sql' => $extra_sql,
- })
- if scalar(@taxclassnums);
+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.
- warn "Found taxes ".
- join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n"
- if $DEBUG;
+=cut
- [ @taxes ];
+sub _gather_taxes {
+ my $self = shift;
+ my $part_item = shift;
+ my $class = shift;
+ my $location = shift;
+
+ local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
+ my $geocode = $location->geocode('cch');
+
+ [ $part_item->tax_rates('cch', $geocode, $class) ]
}
#a little false laziness w/due_cust_event (not too bad, really)
- my $join = FS::part_event_condition->join_conditions_sql;
+ # I guess this is always as of now?
+ my $join = FS::part_event_condition->join_conditions_sql('', 'time' => time);
my $order = FS::part_event_condition->order_conditions_sql;
my $mine =
'( '
#???
#my $DEBUG = $opt{'debug'}
+ $opt{'debug'} ||= 0; # silence some warnings
local($DEBUG) = $opt{'debug'}
- if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG;
+ if $opt{'debug'} > $DEBUG;
$DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
warn "$me due_cust_event called with options ".
#some false laziness w/Cron::bill bill_where
- my $join = FS::part_event_condition->join_conditions_sql( $eventtable);
+ my $join = FS::part_event_condition->join_conditions_sql( $eventtable,
+ 'time' => $opt{'time'});
my $where = FS::part_event_condition->where_conditions_sql($eventtable,
'time'=>$opt{'time'},
);
my $pkey = $object->primary_key;
$cross_where = "$eventtable.$pkey = ". $object->$pkey();
- my $join = FS::part_event_condition->join_conditions_sql( $eventtable );
+ my $join = FS::part_event_condition->join_conditions_sql( $eventtable,
+ 'time' => $opt{'time'});
my $extra_sql =
FS::part_event_condition->where_conditions_sql( $eventtable,
'time'=>$opt{'time'}
#return 0 unless
- my @payments = sort { $b->_date <=> $a->_date }
- grep { $_->unapplied > 0 }
- $self->cust_pay;
+ my @payments = $self->unapplied_cust_pay;
- my @invoices = sort { $a->_date <=> $b->_date}
- grep { $_->owed > 0 }
- $self->cust_bill;
+ my @invoices = $self->open_cust_bill;
if ( $conf->exists('pkg-balances') ) {
# limit @payments to those w/ a pkgnum grepped from $self
_handle_taxes
(vendor-only) _gather_taxes
_omit_zero_value_bundles
+ _handle_taxes (for fees)
calculate_taxes
apply_payments_and_credits