use List::Util qw( min );
use FS::UID qw( dbh );
use FS::Record qw( qsearch qsearchs dbdef );
+use FS::Misc::DateTime qw( day_end );
use FS::cust_bill;
use FS::cust_bill_pkg;
use FS::cust_bill_pkg_display;
use FS::cust_bill_pay;
use FS::cust_credit_bill;
-use FS::cust_pkg;
use FS::cust_tax_adjustment;
use FS::tax_rate;
use FS::tax_rate_location;
use FS::cust_bill_pkg_tax_rate_location;
use FS::part_event;
use FS::part_event_condition;
+use FS::pkg_category;
# 1 is mostly method/subroutine entry and options
# 2 traces progress of some operations
my $job = $options{'job'};
$job->update_statustext('0,cleaning expired packages') if $job;
- $error = $self->cancel_expired_pkgs( $options{actual_time} );
+ $error = $self->cancel_expired_pkgs( day_end( $options{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( $options{actual_time} );
+ $error = $self->suspend_adjourned_pkgs( day_end( $options{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} ) );
+ if ( $error ) {
+ $error = "Error resuming custnum ".$self->custnum. ": $error";
+ if ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
+ elsif ( $options{fatal} ) { die $error; }
+ else { warn $error; }
+ }
+
$job->update_statustext('20,billing packages') if $job;
$error = $self->bill( %options );
if ( $error ) {
sub cancel_expired_pkgs {
my ( $self, $time, %options ) = @_;
-
+
my @cancel_pkgs = $self->ncancelled_pkgs( {
'extra_sql' => " AND expire IS NOT NULL AND expire > 0 AND expire <= $time "
} );
push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
}
- scalar(@errors) ? join(' / ', @errors) : '';
+ join(' / ', @errors);
}
sub suspend_adjourned_pkgs {
my ( $self, $time, %options ) = @_;
-
+
my @susp_pkgs = $self->ncancelled_pkgs( {
'extra_sql' =>
" AND ( susp IS NULL OR susp = 0 )
push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
}
- scalar(@errors) ? join(' / ', @errors) : '';
+ join(' / ', @errors);
+
+}
+
+sub unsuspend_resumed_pkgs {
+ my ( $self, $time, %options ) = @_;
+
+ my @unsusp_pkgs = $self->ncancelled_pkgs( {
+ 'extra_sql' => " AND resume IS NOT NULL AND resume > 0 AND resume <= $time "
+ } );
+
+ my @errors = ();
+
+ foreach my $cust_pkg ( @unsusp_pkgs ) {
+ my $error = $cust_pkg->unsuspend( 'time' => $time );
+ push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
+ }
+
+ join(' / ', @errors);
}
=item freq_override
If set, then override the normal frequency and look for a part_pkg_discount
-to take at that frequency.
+to take at that frequency. This is appropriate only when the normal
+frequency for all packages is monthly, and is an error otherwise. Use
+C<pkg_list> to limit the set of packages included in billing.
=item time
sub bill {
my( $self, %options ) = @_;
+
return '' if $self->payby eq 'COMP';
+
+ local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
warn "$me bill customer ". $self->custnum. "\n"
if $DEBUG;
next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
- warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1;
+ warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG;
#? to avoid use of uninitialized value errors... ?
$cust_pkg->setfield('bill', '')
my $pass = ($cust_pkg->no_auto || $part_pkg->no_auto) ? 'no_auto' : '';
- my $error =
- $self->_make_lines( 'part_pkg' => $part_pkg,
- 'cust_pkg' => $cust_pkg,
- 'precommit_hooks' => \@precommit_hooks,
- 'line_items' => $cust_bill_pkg{$pass},
- 'setup' => $total_setup{$pass},
- 'recur' => $total_recur{$pass},
- 'tax_matrix' => $taxlisthash{$pass},
- 'time' => $time,
- 'real_pkgpart' => $real_pkgpart,
- 'options' => \%options,
- );
+ my $next_bill = $cust_pkg->getfield('bill') || 0;
+ my $error;
+ while ( $next_bill <= $time ) {
+ $error =
+ $self->_make_lines( 'part_pkg' => $part_pkg,
+ 'cust_pkg' => $cust_pkg,
+ 'precommit_hooks' => \@precommit_hooks,
+ 'line_items' => $cust_bill_pkg{$pass},
+ 'setup' => $total_setup{$pass},
+ 'recur' => $total_recur{$pass},
+ 'tax_matrix' => $taxlisthash{$pass},
+ 'time' => $time,
+ 'real_pkgpart' => $real_pkgpart,
+ 'options' => \%options,
+ );
+
+ # Stop if anything goes wrong
+ last if $error;
+
+ # or if we're not incrementing the bill date.
+ last if ($cust_pkg->getfield('bill') || 0) == $next_bill;
+
+ $next_bill = $cust_pkg->getfield('bill') || 0;
+
+ #stop if -o was passed to freeside-daily
+ last if $options{'one_recur'};
+ }
if ($error) {
$dbh->rollback if $oldAutoCommit && !$options{no_commit};
return $error;
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;
+
if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
!$conf->exists('postal_invoice-recurring_only')
)
#create the new invoice
my $cust_bill = new FS::cust_bill ( {
'custnum' => $self->custnum,
- '_date' => ( $invoice_time ),
+ '_date' => $invoice_time,
'charged' => $charged,
'billing_balance' => $balance,
'previous_balance' => $previous_balance,
#discard bundled packages of 0 value
sub _omit_zero_value_bundles {
+ my @in = @_;
my @cust_bill_pkg = ();
my @cust_bill_pkg_bundle = ();
my $sum = 0;
+ my $discount_show_always = 0;
+
+ foreach my $cust_bill_pkg ( @in ) {
+
+ $discount_show_always = ($cust_bill_pkg->get('discounts')
+ && scalar(@{$cust_bill_pkg->get('discounts')})
+ && $conf->exists('discount-show-always'));
+
+ warn " pkgnum ". $cust_bill_pkg->pkgnum. " sum $sum, ".
+ "setup_show_zero ". $cust_bill_pkg->setup_show_zero.
+ "recur_show_zero ". $cust_bill_pkg->recur_show_zero. "\n"
+ if $DEBUG > 0;
- foreach my $cust_bill_pkg ( @_ ) {
if (scalar(@cust_bill_pkg_bundle) && !$cust_bill_pkg->pkgpart_override) {
- push @cust_bill_pkg, @cust_bill_pkg_bundle if $sum > 0;
+ push @cust_bill_pkg, @cust_bill_pkg_bundle
+ if $sum > 0
+ || ($sum == 0 && ( $discount_show_always
+ || grep {$_->recur_show_zero || $_->setup_show_zero}
+ @cust_bill_pkg_bundle
+ )
+ );
@cust_bill_pkg_bundle = ();
$sum = 0;
}
+
$sum += $cust_bill_pkg->setup + $cust_bill_pkg->recur;
push @cust_bill_pkg_bundle, $cust_bill_pkg;
+
}
- push @cust_bill_pkg, @cust_bill_pkg_bundle if $sum > 0;
+
+ push @cust_bill_pkg, @cust_bill_pkg_bundle
+ if $sum > 0
+ || ($sum == 0 && ( $discount_show_always
+ || grep {$_->recur_show_zero || $_->setup_show_zero}
+ @cust_bill_pkg_bundle
+ )
+ );
+
+ warn " _omit_zero_value_bundles: ". scalar(@in).
+ '->'. scalar(@cust_bill_pkg). "\n" #. Dumper(@cust_bill_pkg). "\n"
+ if $DEBUG > 2;
(@cust_bill_pkg);
sub calculate_taxes {
my ($self, $cust_bill_pkg, $taxlisthash, $invoice_time) = @_;
- my @tax_line_items = ();
+ local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
- warn "having a look at the taxes we found...\n" if $DEBUG > 2;
+ warn "$me calculate_taxes\n"
+ #.Dumper($self, $cust_bill_pkg, $taxlisthash, $invoice_time). "\n"
+ if $DEBUG > 2;
+
+ my @tax_line_items = ();
# keys are tax names (as printed on invoices / itemdesc )
# values are listrefs of taxlisthash keys (internal identifiers)
foreach my $tax ( keys %$taxlisthash ) {
foreach ( @{ $taxlisthash->{$tax} }[1 ... scalar(@{ $taxlisthash->{$tax} })] ) {
next unless ref($_) eq 'FS::cust_bill_pkg';
+
+ my @cust_tax_exempt_pkg = splice( @{ $_->_cust_tax_exempt_pkg } );
- push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
- splice( @{ $_->_cust_tax_exempt_pkg } );
+ next unless @cust_tax_exempt_pkg; #just avoiding the prob when irrelevant?
+ die "can't distribute tax exemptions: no line item for ". Dumper($_).
+ " in packagemap ". join(',', sort {$a<=>$b} keys %packagemap). "\n"
+ unless $packagemap{$_->pkgnum};
+
+ push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
+ @cust_tax_exempt_pkg;
}
}
sub _make_lines {
my ($self, %params) = @_;
+ local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
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 $time = $params{'time'} or die "no time specified";
my (%options) = %{$params{options}};
+ if ( $part_pkg->freq ne '1' and ($options{'freq_override'} || 0) > 0 ) {
+ # this should never happen
+ die 'freq_override billing attempted on non-monthly package '.
+ $cust_pkg->pkgnum;
+ }
+
my $dbh = dbh;
my $real_pkgpart = $params{real_pkgpart};
my %hash = $cust_pkg->hash;
my $old_cust_pkg = new FS::cust_pkg \%hash;
my @details = ();
- my @discounts = ();
my $lineitems = 0;
$cust_pkg->pkgpart($part_pkg->pkgpart);
my $setup = 0;
my $unitsetup = 0;
- if ( $options{'resetup'}
- || ( ! $cust_pkg->setup
- && ( ! $cust_pkg->start_date
- || $cust_pkg->start_date <= $time
- )
- && ( ! $conf->exists('disable_setup_suspended_pkgs')
- || ( $conf->exists('disable_setup_suspended_pkgs') &&
- ! $cust_pkg->getfield('susp')
- )
- )
- )
- and !$options{recurring_only}
- )
+ my @setup_discounts = ();
+ my %setup_param = ( 'discounts' => \@setup_discounts );
+ if ( ! $options{recurring_only}
+ and ! $options{cancel}
+ and ( $options{'resetup'}
+ || ( ! $cust_pkg->setup
+ && ( ! $cust_pkg->start_date
+ || $cust_pkg->start_date <= day_end($time)
+ )
+ && ( ! $conf->exists('disable_setup_suspended_pkgs')
+ || ( $conf->exists('disable_setup_suspended_pkgs') &&
+ ! $cust_pkg->getfield('susp')
+ )
+ )
+ )
+ )
+ )
{
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 $@;
+ unless ( $cust_pkg->waive_setup ) {
+ $lineitems++;
+
+ $setup = eval { $cust_pkg->calc_setup( $time, \@details, \%setup_param ) };
+ return "$@ running calc_setup for $cust_pkg\n"
+ if $@;
- $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
+ $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
+ }
$cust_pkg->setfield('setup', $time)
unless $cust_pkg->setup;
#XXX unit stuff here too
my $recur = 0;
my $unitrecur = 0;
+ my @recur_discounts = ();
my $sdate;
- if ( ! $cust_pkg->get('susp')
- and ! $cust_pkg->get('start_date')
- and ( $part_pkg->getfield('freq') ne '0'
- && ( $cust_pkg->getfield('bill') || 0 ) <= $time
- )
- || ( $part_pkg->plan eq 'voip_cdr'
- && $part_pkg->option('bill_every_call')
- )
- || ( $options{cancel} )
+ if ( ! $cust_pkg->start_date
+ and ( ! $cust_pkg->susp || $part_pkg->option('suspend_bill', 1) )
+ and
+ ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= day_end($time) )
+ || ( $part_pkg->plan eq 'voip_cdr'
+ && $part_pkg->option('bill_every_call')
+ )
+ || $options{cancel}
) {
# XXX should this be a package event? probably. events are called
#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 ) <= $time
+ && ( $cust_pkg->getfield('bill') || 0 ) <= day_end($time)
&& !$options{cancel}
);
- my %param = ( 'precommit_hooks' => $precommit_hooks,
+ my %param = ( %setup_param,
+ 'precommit_hooks' => $precommit_hooks,
'increment_next_bill' => $increment_next_bill,
- 'discounts' => \@discounts,
+ 'discounts' => \@recur_discounts,
'real_pkgpart' => $real_pkgpart,
'freq_override' => $options{freq_override} || '',
+ 'setup_fee' => 0,
);
my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
# There may be some part_pkg for which this is wrong. Only those
# which can_discount are supported.
+ # (the UI should prevent adding discounts to these at the moment)
+ warn "calling $method on cust_pkg ". $cust_pkg->pkgnum.
+ " for pkgpart ". $cust_pkg->pkgpart.
+ " with params ". join(' / ', map "$_=>$param{$_}", keys %param). "\n"
+ if $DEBUG > 2;
+
$recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
return "$@ running $method for $cust_pkg\n"
if ( $@ );
}
+ if ( $param{'setup_fee'} ) {
+ # Add an additional setup fee at the billing stage.
+ # Used for prorate_defer_bill.
+ $setup += $param{'setup_fee'};
+ $unitsetup += $param{'setup_fee'};
+ $lineitems++;
+ }
+
+ if ( defined $param{'discount_left_setup'} ) {
+ foreach my $discount_setup ( values %{$param{'discount_left_setup'}} ) {
+ $setup -= $discount_setup;
+ }
+ }
+
}
warn "\$setup is undefined" unless defined($setup);
# If $cust_pkg has been modified, update it (if we're a real pkgpart)
###
- if ( $lineitems || $options{has_hidden} ) {
+ if ( $lineitems ) {
if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
# hmm.. and if just the options are modified in some weird price plan?
if $DEBUG >1;
my $error = $cust_pkg->replace( $old_cust_pkg,
+ 'depend_jobnum'=>$options{depend_jobnum},
'options' => { $cust_pkg->options },
)
unless $options{no_commit};
return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
}
- if ( $setup != 0 ||
- $recur != 0 ||
- !$part_pkg->hidden && $options{has_hidden} ) #include some $0 lines
+ my $discount_show_always = $conf->exists('discount-show-always')
+ && ( ($setup == 0 && scalar(@setup_discounts))
+ || ($recur == 0 && scalar(@recur_discounts))
+ );
+
+ if ( $setup != 0
+ || $recur != 0
+ || (!$part_pkg->hidden && $options{has_hidden}) #include some $0 lines
+ || $discount_show_always
+ || ($setup == 0 && $cust_pkg->_X_show_zero('setup'))
+ || ($recur == 0 && $cust_pkg->_X_show_zero('recur'))
+ )
{
warn " charges (setup=$setup, recur=$recur); adding line items\n"
'unitrecur' => $unitrecur,
'quantity' => $cust_pkg->quantity,
'details' => \@details,
- 'discounts' => \@discounts,
+ 'discounts' => [ @setup_discounts, @recur_discounts ],
'hidden' => $part_pkg->hidden,
'freq' => $part_pkg->freq,
};
- if ( $part_pkg->option('recur_temporality', 1) eq 'preceding' ) {
+ if ( $part_pkg->recur_temporality eq 'preceding' ) {
$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->option('recur_temporality', 1) 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
###
- my $error =
- $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart, \%options);
- return $error if $error;
+ 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;
+ }
push @$cust_bill_pkgs, $cust_bill_pkg;
my $real_pkgpart = shift;
my $options = shift;
+ local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
my %cust_bill_pkg = ();
my %taxes = ();
)
{
- if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
- return "fatal: Can't (yet) use tax-pkg_address with taxproducts";
- }
-
foreach my $class (@classes) {
- my $err_or_ref = $self->_gather_taxes( $part_pkg, $class );
+ 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;
}
unless (exists $taxes{''}) {
- my $err_or_ref = $self->_gather_taxes( $part_pkg, '' );
+ my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
return $err_or_ref unless ref($err_or_ref);
$taxes{''} = $err_or_ref;
}
} else {
- my @loc_keys = qw( city county state country );
+ my @loc_keys = qw( district city county state country );
my %taxhash;
if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
my $cust_location = $cust_pkg->cust_location;
my @taxes = ();
my %taxhash_elim = %taxhash;
- my @elim = qw( city county state );
+ my @elim = qw( district city county state );
do {
#first try a match with taxclass
} #if $conf->exists('enable_taxproducts') ...
}
-
- my @display = ();
- my $separate = $conf->exists('separate_usage');
- my $temp_pkg = new FS::cust_pkg { pkgpart => $real_pkgpart };
- my $usage_mandate = $temp_pkg->part_pkg->option('usage_mandate', 'Hush!');
- my $section = $temp_pkg->part_pkg->categoryname;
- if ( $separate || $section || $usage_mandate ) {
-
- my %hash = ( 'section' => $section );
-
- $section = $temp_pkg->part_pkg->option('usage_section', 'Hush!');
- my $summary = $temp_pkg->part_pkg->option('summarize_usage', 'Hush!');
- if ( $separate ) {
- push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
- push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
- } else {
- push @display, new FS::cust_bill_pkg_display
- { type => '',
- %hash,
- ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
- };
- }
- if ($separate && $section && $summary) {
- push @display, new FS::cust_bill_pkg_display { type => 'U',
- summary => 'Y',
- %hash,
- };
- }
- if ($usage_mandate || $section && $summary) {
- $hash{post_total} = 'Y';
- }
-
- if ($separate || $usage_mandate) {
- $hash{section} = $section if ($separate || $usage_mandate);
- push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
- }
-
- }
- $cust_bill_pkg->set('display', \@display);
+ #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) {
my $self = shift;
my $part_pkg = shift;
my $class = shift;
+ my $cust_pkg = shift;
+
+ local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
+ my $geocode;
+ if ( $cust_pkg->locationnum && $conf->exists('tax-pkg_address') ) {
+ $geocode = $cust_pkg->cust_location->geocode('cch');
+ } else {
+ $geocode = $self->geocode('cch');
+ }
my @taxes = ();
- my $geocode = $self->geocode('cch');
my @taxclassnums = map { $_->taxclassnum }
$part_pkg->part_pkg_taxoverride($class);
sub collect {
my( $self, %options ) = @_;
+
+ local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
my $invoice_time = $options{'invoice_time'} || time;
#put below somehow?
sub do_cust_event {
my( $self, %options ) = @_;
+
+ local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
+
my $time = $options{'time'} || time;
#put below somehow?
if $DEBUG > 1;
#if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
- if ( my $error = $cust_event->do_event() ) {
+ if ( my $error = $cust_event->do_event( 'time' => $time ) ) {
#XXX wtf is this? figure out a proper dealio with return value
#from do_event
return $error;
Set to true to return the objects, but not actually insert them into the
database.
-=item discount_terms
-
-Returns a list of lengths for term discounts
-
-=cut
-
-sub _discount_pkgs_and_bill {
-my $self = shift;
-
- my @cust_bill = $self->cust_bill;
- my $cust_bill = pop @cust_bill;
- return () unless $cust_bill && $cust_bill->owed;
-
- my @where = ();
- push @where, "cust_bill_pkg.invnum = ". $cust_bill->invnum;
- push @where, "cust_bill_pkg.pkgpart_override IS NULL";
- push @where, "part_pkg.freq = '1'";
- push @where, "(cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0)";
- push @where, "(cust_pkg.susp IS NULL OR cust_pkg.susp = 0)";
- push @where, "0<(SELECT count(*) FROM part_pkg_discount
- WHERE part_pkg.pkgpart = part_pkg_discount.pkgpart)";
- push @where,
- "0=(SELECT count(*) FROM cust_bill_pkg_discount
- WHERE cust_bill_pkg.billpkgnum = cust_bill_pkg_discount.billpkgnum)";
-
- my $extra_sql = 'WHERE '. join(' AND ', @where);
-
- my @cust_pkg =
- qsearch({
- 'table' => 'cust_pkg',
- 'select' => "DISTINCT cust_pkg.*",
- 'addl_from' => 'JOIN cust_bill_pkg USING(pkgnum) '.
- 'JOIN part_pkg USING(pkgpart)',
- 'hashref' => {},
- 'extra_sql' => $extra_sql,
- });
-
- ($cust_bill, @cust_pkg);
-}
-
-sub _discountable_pkgs_at_term {
- my ($term, @pkgs) = @_;
- my $part_pkg = new FS::part_pkg { freq => $term - 1 };
- grep { ( !$_->adjourn || $_->adjourn > $part_pkg->add_freq($_->bill) ) &&
- ( !$_->expire || $_->expire > $part_pkg->add_freq($_->bill) )
- }
- @pkgs;
-}
-
-=item discount_terms
-
-Returns a list of lengths for term discounts
-
-=cut
-
-sub discount_terms {
-my $self = shift;
-
- my %terms = ();
-
- my @discount_pkgs = $self->_discount_pkgs_and_bill;
- shift @discount_pkgs; #discard bill;
-
- map { $terms{$_->months} = 1 }
- grep { $_->months && $_->months > 1 }
- map { $_->discount }
- map { $_->part_pkg->part_pkg_discount }
- @discount_pkgs;
-
- return sort { $a <=> $b } keys %terms;
-
-}
-
-=back
-
-=item discount_term_values MONTHS
-
-Returns a list with credit, dollar amount saved, and total bill acheived
-by prepaying the most recent invoice for MONTHS.
-
-=cut
-
-sub discount_term_values {
- my $self = shift;
- my $term = shift;
- warn "$me discount_term_values called with $term\n" if $DEBUG;
-
- my %result = ();
-
- my @packages = $self->_discount_pkgs_and_bill;
- my $cust_bill = shift(@packages);
- @packages = _discountable_pkgs_at_term( $term, @packages );
- return () unless scalar(@packages);
-
- $_->bill($_->last_bill) foreach @packages;
- my @final = map { new FS::cust_pkg { $_->hash } } @packages;
-
- my %options = (
- 'recurring_only' => 1,
- 'no_usage_reset' => 1,
- 'no_commit' => 1,
- );
-
- my %params = (
- 'return_bill' => [],
- 'pkg_list' => \@packages,
- 'time' => $cust_bill->_date,
- );
-
- my $error = $self->bill(%options, %params);
- die $error if $error; # XXX think about this a bit more
-
- my $credit = 0;
- $credit += $_->charged foreach @{$params{return_bill}};
- $credit = sprintf('%.2f', $credit);
- warn "$me discount_term_values $term credit: $credit\n" if $DEBUG;
-
- %params = (
- 'return_bill' => [],
- 'pkg_list' => \@packages,
- 'time' => $packages[0]->part_pkg->add_freq($cust_bill->_date)
- );
-
- $error = $self->bill(%options, %params);
- die $error if $error; # XXX think about this a bit more
-
- my $next = 0;
- $next += $_->charged foreach @{$params{return_bill}};
- warn "$me discount_term_values $term next: $next\n" if $DEBUG;
-
- %params = (
- 'return_bill' => [],
- 'pkg_list' => \@final,
- 'time' => $cust_bill->_date,
- 'freq_override' => $term,
- );
-
- $error = $self->bill(%options, %params);
- die $error if $error; # XXX think about this a bit more
-
- my $final = $self->balance - $credit;
- $final += $_->charged foreach @{$params{return_bill}};
- $final = sprintf('%.2f', $final);
- warn "$me discount_term_values $term final: $final\n" if $DEBUG;
-
- my $savings = sprintf('%.2f', $self->balance + ($term - 1) * $next - $final);
-
- ( $credit, $savings, $final );
-
-}
-
-sub discount_terms_hash {
- my $self = shift;
-
- my %result = ();
- my @terms = $self->discount_terms;
- foreach my $term (@terms) {
- my @result = $self->discount_term_values($term);
- $result{$term} = [ @result ] if scalar(@result);
- }
-
- return %result;
-
-}
-
=back
=cut
#my $DEBUG = $opt{'debug'}
local($DEBUG) = $opt{'debug'}
if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG;
+ $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
warn "$me due_cust_event called with options ".
join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
my $amount = min( $payment->unapplied, $owed );
- my $cust_bill_pay = new FS::cust_bill_pay ( {
+ my $cbp = {
'paynum' => $payment->paynum,
'invnum' => $cust_bill->invnum,
'amount' => $amount,
- } );
+ };
+ $cbp->{_date} = $payment->_date
+ if $options{'manual'} && $options{'backdate_application'};
+ my $cust_bill_pay = new FS::cust_bill_pay($cbp);
$cust_bill_pay->pkgnum( $payment->pkgnum )
if $conf->exists('pkg-balances') && $payment->pkgnum;
my $error = $cust_bill_pay->insert(%options);
return $total_unapplied_payments;
}
+=back
+
+=head1 FLOW
+
+ bill_and_collect
+
+ cancel_expired_pkgs
+ suspend_adjourned_pkgs
+ unsuspend_resumed_pkgs
+
+ bill
+ (do_cust_event pre-bill)
+ _make_lines
+ _handle_taxes
+ (vendor-only) _gather_taxes
+ _omit_zero_value_bundles
+ calculate_taxes
+
+ apply_payments_and_credits
+ collect
+ do_cust_event
+ due_cust_event
+
=head1 BUGS
=head1 SEE ALSO