use Carp;
use Exporter;
use Scalar::Util qw( blessed );
+use List::Util qw( min );
use Time::Local qw(timelocal);
use Data::Dumper;
use Tie::IxHash;
use FS::cust_main_county;
use FS::cust_location;
use FS::cust_main_exemption;
+use FS::cust_tax_adjustment;
use FS::tax_rate;
use FS::tax_rate_location;
use FS::cust_tax_location;
|| $self->ut_textn('stateid_state')
|| $self->ut_textn('invoice_terms')
|| $self->ut_alphan('geocode')
+ || $self->ut_floatn('cdr_termination_percentage')
;
#barf. need message catalogs. i18n. etc.
grep { ! $_->susp } $self->ncancelled_pkgs;
}
+=item next_bill_date
+
+Returns the next date this customer will be billed, as a UNIX timestamp, or
+undef if no active package has a next bill date.
+
+=cut
+
+sub next_bill_date {
+ my $self = shift;
+ min( map $_->get('bill'), grep $_->get('bill'), $self->unsuspended_pkgs );
+}
+
=item num_cancelled_pkgs
Returns the number of cancelled packages (see L<FS::cust_pkg>) for this
=item ban - can be set true to ban this customer's credit card or ACH information, if present.
+=item nobill - can be set true to skip billing if it might otherwise be done.
+
=back
Always returns a list: an empty list on success or a list of errors.
=cut
+# nb that dates are not specified as valid options to this method
+
sub cancel {
my( $self, %opt ) = @_;
my @pkgs = $self->ncancelled_pkgs;
+ if ( !$opt{nobill} && $conf->exists('bill_usage_on_cancel') ) {
+ $opt{nobill} = 1;
+ my $error = $self->bill( pkg_list => [ @pkgs ], cancel => 1 );
+ warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
+ if $error;
+ }
+
warn "$me cancelling ". scalar($self->ncancelled_pkgs). "/".
scalar(@pkgs). " packages for customer ". $self->custnum. "\n"
if $DEBUG;
=back
+Options are passed to the B<bill> and B<collect> methods verbatim, so all
+options of those methods are also available.
+
=cut
sub bill_and_collect {
$cust_main->bill( pkg_list => [$pkg1, $pkg2] );
+=item not_pkgpart
+
+A hashref of pkgparts to exclude from this billing run.
+
=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.
+=item cancel
+
+This boolean value informs the us that the package is being cancelled. This
+typically might mean not charging the normal recurring fee but only usage
+fees since the last billing. Setup charges may be charged. Not all package
+plans support this feature (they tend to charge 0).
+
=back
=cut
my $time = $options{'time'} || time;
my $invoice_time = $options{'invoice_time'} || $time;
+ $options{'not_pkgpart'} ||= {};
+
#put below somehow?
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
my %taxlisthash;
my @precommit_hooks = ();
- foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
+ $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ]; #param checks?
+ foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
+
+ next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1;
} elsif ( $postal_pkg ) {
foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) {
+ my %postal_options = %options;
+ delete $postal_options{cancel};
my $error =
$self->_make_lines( 'part_pkg' => $part_pkg,
'cust_pkg' => $postal_pkg,
'recur' => \$total_recur,
'tax_matrix' => \%taxlisthash,
'time' => $time,
- 'options' => \%options,
+ 'options' => \%postal_options,
);
if ($error) {
$dbh->rollback if $oldAutoCommit;
}
+ #add tax adjustments
+ warn "adding tax adjustments...\n" if $DEBUG > 2;
+ foreach my $cust_tax_adjustment (
+ qsearch('cust_tax_adjustment', { 'custnum' => $self->custnum,
+ 'billpkgnum' => '',
+ }
+ )
+ ) {
+
+ my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
+ $total_setup = sprintf('%.2f', $total_setup+$tax );
+
+ my $itemdesc = $cust_tax_adjustment->taxname;
+ $itemdesc = '' if $itemdesc eq 'Tax';
+
+ push @cust_bill_pkg, new FS::cust_bill_pkg {
+ 'pkgnum' => 0,
+ 'setup' => $tax,
+ 'recur' => 0,
+ 'sdate' => '',
+ 'edate' => '',
+ 'itemdesc' => $itemdesc,
+ 'itemcomment' => $cust_tax_adjustment->comment,
+ 'cust_tax_adjustment' => $cust_tax_adjustment,
+ #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
+ };
+
+ }
+
my $charged = sprintf('%.2f', $total_setup + $total_recur );
#create the new invoice
my $setup = 0;
my $unitsetup = 0;
- if ( ! $cust_pkg->setup &&
- (
- ( $conf->exists('disable_setup_suspended_pkgs') &&
- ! $cust_pkg->getfield('susp')
- ) || ! $conf->exists('disable_setup_suspended_pkgs')
- )
- || $options{'resetup'}
- ) {
+ 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')
+ )
+ )
+ )
+ )
+ {
warn " bill setup\n" if $DEBUG > 1;
$lineitems++;
#do need it, but it won't get written to the db
#|| $cust_pkg->pkgpart != $real_pkgpart;
+ $cust_pkg->setfield('start_date', '')
+ if $cust_pkg->start_date;
+
}
###
|| ( $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
$lineitems++;
# XXX shared with $recur_prog
- $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
+ $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
+ || $cust_pkg->setup
+ || $time;
#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
+ && !$options{cancel}
);
my %param = ( 'precommit_hooks' => $precommit_hooks,
'increment_next_bill' => $increment_next_bill,
);
- $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details, \%param ) };
- return "$@ running calc_recur for $cust_pkg\n"
+ my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
+ $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
+ return "$@ running $method for $cust_pkg\n"
if ( $@ );
if ( $increment_next_bill ) {
if ( $part_pkg->option('recur_temporality', 1) 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' ) {
$cust_bill_pkg->sdate( $sdate );
$cust_bill_pkg->edate( $cust_pkg->bill );
+ #$cust_bill_pkg->edate( $time ) if $options{cancel};
}
$cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
}
-=item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
+=item charge HASHREF || AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
Creates a one-time charge for this customer. If there is an error, returns
the error, otherwise returns false.
+New-style, with a hashref of options:
+
+ my $error = $cust_main->charge(
+ {
+ 'amount' => 54.32,
+ 'quantity' => 1,
+ 'start_date' => str2time('7/4/2009'),
+ 'pkg' => 'Description',
+ 'comment' => 'Comment',
+ 'additional' => [], #extra invoice detail
+ 'classnum' => 1, #pkg_class
+
+ 'setuptax' => '', # or 'Y' for tax exempt
+
+ #internal taxation
+ 'taxclass' => 'Tax class',
+
+ #vendor taxation
+ 'taxproduct' => 2, #part_pkg_taxproduct
+ 'override' => {}, #XXX describe
+ }
+ );
+
+Old-style:
+
+ my $error = $cust_main->charge( 54.32, 'Description', 'Comment', 'Tax class' );
+
=cut
sub charge {
my $self = shift;
- my ( $amount, $quantity, $pkg, $comment, $classnum, $additional );
+ my ( $amount, $quantity, $start_date, $classnum );
+ my ( $pkg, $comment, $additional );
my ( $setuptax, $taxclass ); #internal taxes
my ( $taxproduct, $override ); #vendor (CCH) taxes
if ( ref( $_[0] ) ) {
$amount = $_[0]->{amount};
$quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
+ $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
$pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
$comment = exists($_[0]->{comment}) ? $_[0]->{comment}
: '$'. sprintf("%.2f",$amount);
$setuptax = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
$taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
$classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
- $additional = $_[0]->{additional};
+ $additional = $_[0]->{additional} || [];
$taxproduct = $_[0]->{taxproductnum};
$override = { '' => $_[0]->{tax_override} };
- }else{
+ } else {
$amount = shift;
$quantity = 1;
+ $start_date = '';
$pkg = @_ ? shift : 'One-time charge';
$comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
$setuptax = '';
}
my $cust_pkg = new FS::cust_pkg ( {
- 'custnum' => $self->custnum,
- 'pkgpart' => $pkgpart,
- 'quantity' => $quantity,
+ 'custnum' => $self->custnum,
+ 'pkgpart' => $pkgpart,
+ 'quantity' => $quantity,
+ 'start_date' => $start_date,
} );
$error = $cust_pkg->insert;
}
+=item unapplied_payments_date_sql START_TIME [ END_TIME ]
+
+Returns an SQL fragment to retreive the total unapplied payments for this
+customer, only considering invoices with date earlier than START_TIME, and
+optionally not later than END_TIME.
+
+Times are specified as SQL fragments or numeric
+UNIX timestamps; see L<perlfunc/"time">). Also see L<Time::Local> and
+L<Date::Parse> for conversion functions. The empty string can be passed
+to disable that time constraint completely.
+
+Available options are:
+
+=cut
+
+sub unapplied_payments_date_sql {
+ my( $class, $start, $end, ) = @_;
+
+ my $unapp_pay = FS::cust_pay->unapplied_sql;
+
+ my $pay_where = $class->_money_table_where( 'cust_pay', $start, $end,
+ 'unapplied_date'=>1 );
+
+ " ( SELECT COALESCE(SUM($unapp_pay), 0) FROM cust_pay $pay_where ) ";
+}
+
=item _money_table_where TABLE START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
Helper method for balance_date_sql; name (and usage) subject to change
$pkgwhere .= "AND (cancel = 0 or cancel is null)"
unless $params->{'cancelled_pkgs'};
+ ##
+ # parse without census tract checkbox
+ ##
+
+ push @where, "(censustract = '' or censustract is null)"
+ if $params->{'no_censustract'};
+
##
# dates
##
}
- #eliminate duplicates
- my %saw = ();
- @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
-
}
+ #eliminate duplicates
+ my %saw = ();
+ @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
+
@cust_main;
}