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::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
=head1 SYNOPSIS
-=head1 DESCRIPTIONS
+=head1 DESCRIPTION
These methods are available on FS::cust_main objects.
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.
+
=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;
}
'options' => \%options,
);
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
}
my @cust_bill_pkg = ();
my @cust_bill_pkg_bundle = ();
my $sum = 0;
+ my $discount_show_always = 0;
foreach my $cust_bill_pkg ( @_ ) {
+ $discount_show_always = ($cust_bill_pkg->get('discounts')
+ && scalar(@{$cust_bill_pkg->get('discounts')})
+ && $conf->exists('discount-show-always'));
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));
@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));
(@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 "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';
-
push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
splice( @{ $_->_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";
)
)
)
+ and !$options{recurring_only}
)
{
my $recur = 0;
my $unitrecur = 0;
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 ) <= $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;
'increment_next_bill' => $increment_next_bill,
'discounts' => \@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)
+
$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++;
+ }
+
}
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;
}
+ my $discount_show_always = ($recur == 0 && scalar(@discounts)
+ && $conf->exists('discount-show-always'));
+
if ( $setup != 0 ||
$recur != 0 ||
- !$part_pkg->hidden && $options{has_hidden} ) #include some $0 lines
+ (!$part_pkg->hidden && $options{has_hidden}) || #include some $0 lines
+ $discount_show_always )
{
warn " charges (setup=$setup, recur=$recur); adding line items\n"
'details' => \@details,
'discounts' => \@discounts,
'hidden' => $part_pkg->hidden,
+ 'freq' => $part_pkg->freq,
};
if ( $part_pkg->option('recur_temporality', 1) eq 'preceding' ) {
# 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;
}
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?
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?
#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"
return $total_unapplied_payments;
}
+=back
+
+=head1 FLOW
+
+ bill_and_collect
+
+ cancel_expired_pkgs
+ suspend_adjourned_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
+
+L<FS::cust_main>, L<FS::cust_main::Billing_Realtime>
+
+=cut
+
1;