use strict;
use vars qw( $conf $DEBUG $me );
use Carp;
+use Data::Dumper;
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);
}
If set true, re-charges setup fees.
+=item recurring_only
+
+If set true then only bill recurring charges, not setup, usage, one time
+charges, etc.
+
+=item freq_override
+
+If set, then override the normal frequency and look for a part_pkg_discount
+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
Bills the customer as if it were that time. Specified as a UNIX timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion functions. For example:
fees since the last billing. Setup charges may be charged. Not all package
plans support this feature (they tend to charge 0).
+=item no_usage_reset
+
+Prevent the resetting of usage limits during this call.
+
+=item no_commit
+
+Do not save the generated bill in the database. Useful with return_bill
+
+=item return_bill
+
+A list reference on which the generated bill(s) will be returned.
+
=item invoice_terms
Optional terms to be printed on this invoice. Otherwise, customer-specific
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;
'time' => $invoice_time,
'check_freq' => $options{'check_freq'},
'stage' => 'pre-bill',
- );
+ )
+ unless $options{no_commit};
if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
+ $dbh->rollback if $oldAutoCommit && !$options{no_commit};
return $error;
}
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;
+ $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')
)
my $postal_pkg = $self->charge_postal_fee();
if ( $postal_pkg && !ref( $postal_pkg ) ) {
- $dbh->rollback if $oldAutoCommit;
+ $dbh->rollback if $oldAutoCommit && !$options{no_commit};
return "can't charge postal invoice fee for customer ".
$self->custnum. ": $postal_pkg";
'options' => \%postal_options,
);
if ($error) {
- $dbh->rollback if $oldAutoCommit;
+ $dbh->rollback if $oldAutoCommit && !$options{no_commit};
return $error;
}
}
$self->calculate_taxes( \@cust_bill_pkg, $taxlisthash{$pass}, $invoice_time);
unless ( ref( $listref_or_error ) ) {
- $dbh->rollback if $oldAutoCommit;
+ $dbh->rollback if $oldAutoCommit && !$options{no_commit};
return $listref_or_error;
}
#my $balance_adjustments =
# sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged);
+ warn "creating the new invoice\n" if $DEBUG;
#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,
'invoice_terms' => $options{'invoice_terms'},
+ 'cust_bill_pkg' => \@cust_bill_pkg,
} );
- $error = $cust_bill->insert;
+ $error = $cust_bill->insert unless $options{no_commit};
if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
+ $dbh->rollback if $oldAutoCommit && !$options{no_commit};
return "can't create invoice for customer #". $self->custnum. ": $error";
}
-
- foreach my $cust_bill_pkg ( @cust_bill_pkg ) {
- $cust_bill_pkg->invnum($cust_bill->invnum);
- my $error = $cust_bill_pkg->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't create invoice line item: $error";
- }
- }
+ push @{$options{return_bill}}, $cust_bill if $options{return_bill};
} #foreach my $pass ( keys %cust_bill_pkg )
foreach my $hook ( @precommit_hooks ) {
eval {
&{$hook}; #($self) ?
- };
+ } unless $options{no_commit};
if ( $@ ) {
- $dbh->rollback if $oldAutoCommit;
+ $dbh->rollback if $oldAutoCommit && !$options{no_commit};
return "$@ running precommit hook $hook\n";
}
}
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit && !$options{no_commit};
+
''; #no error
}
#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);
=back
=cut
+
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 "$me calculate_taxes\n"
+ #.Dumper($self, $cust_bill_pkg, $taxlisthash, $invoice_time). "\n"
+ if $DEBUG > 2;
- warn "having a look at the taxes we found...\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')
- )
- )
- )
- )
+ 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++;
- $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
+ $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
+ }
$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
# at collection time at the moment, though...
$part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
- if $part_pkg->can('reset_usage');
+ if $part_pkg->can('reset_usage') && !$options{'no_usage_reset'};
#don't want to reset usage just cause we want a line item??
#&& $part_pkg->pkgpart == $real_pkgpart;
#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 ( $increment_next_bill ) {
- my $next_bill = $part_pkg->add_freq($sdate);
+ my $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
return "unparsable frequency: ". $part_pkg->freq
if $next_bill == -1;
}
+ 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 "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
if $error; #just in case
}
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?
).
' ) ';
- my @cust_event = qsearchs({
+ my @cust_event = qsearch({
'table' => 'cust_event',
'select' => 'cust_event.*',
'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
Debugging level. Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
+=back
=cut
# =item payby
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;
#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