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.
unless ! $self->referral_custnum
|| qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
+ if ( $self->censustract ne '' ) {
+ $self->censustract =~ /^\s*(\d{9})\.?(\d{2})\s*$/
+ or return "Illegal census tract: ". $self->censustract;
+
+ $self->censustract("$1.$2");
+ }
+
if ( $self->ss eq '' ) {
$self->ss('');
} else {
$self->payname($1);
}
- foreach my $flag (qw( tax spool_cdr squelch_cdr archived )) {
+ foreach my $flag (qw( tax spool_cdr squelch_cdr archived email_csv_cdr )) {
$self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
$self->$flag($1);
}
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;
'recur' => \$total_recur,
'tax_matrix' => \%taxlisthash,
'time' => $time,
+ 'real_pkgpart' => $real_pkgpart,
'options' => \%options,
);
if ($error) {
} elsif ( $postal_pkg ) {
+ my $real_pkgpart = $postal_pkg->pkgpart;
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,
+ 'real_pkgpart' => $real_pkgpart,
+ '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 (%options) = %{$params{options}};
my $dbh = dbh;
- my $real_pkgpart = $cust_pkg->pkgpart;
+ my $real_pkgpart = $params{real_pkgpart};
my %hash = $cust_pkg->hash;
my $old_cust_pkg = new FS::cust_pkg \%hash;
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;
+
}
###
my $recur = 0;
my $unitrecur = 0;
my $sdate;
- if ( ! $cust_pkg->getfield('susp') and
- ( $part_pkg->getfield('freq') ne '0' &&
- ( $cust_pkg->getfield('bill') || 0 ) <= $time
+ 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} )
) {
# 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 ) {
'unitrecur' => $unitrecur,
'quantity' => $cust_pkg->quantity,
'details' => \@details,
+ 'hidden' => $part_pkg->hidden,
};
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)
###
my $error =
- $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time});
+ $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart);
return $error if $error;
push @$cust_bill_pkgs, $cust_bill_pkg;
my $cust_bill_pkg = shift;
my $cust_pkg = shift;
my $invoice_time = shift;
+ my $real_pkgpart = shift;
my %cust_bill_pkg = ();
my %taxes = ();
@taxes = qsearch( 'cust_main_county', \%taxhash_elim );
}
+ @taxes = grep { ! $_->taxname or ! $self->tax_exemption($_->taxname) }
+ @taxes
+ if $self->cust_main_exemption; #just to be safe
+
if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
foreach (@taxes) {
$_->set('pkgnum', $cust_pkg->pkgnum );
$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) );
- }
+ # # 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') ...
}
my @display = ();
- if ( $conf->exists('separate_usage') ) {
+ if ( $conf->exists('separate_usage') || $cust_bill_pkg->hidden ) {
+
+ my $temp_pkg = new FS::cust_pkg { pkgpart => $real_pkgpart };
+ my %hash = $cust_bill_pkg->hidden # maybe for all bill linked?
+ ? ( 'section' => $temp_pkg->part_pkg->categoryname )
+ : ();
+
my $section = $cust_pkg->part_pkg->option('usage_section', 'Hush!');
my $summary = $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
- push @display, new FS::cust_bill_pkg_display { type => 'S' };
- push @display, new FS::cust_bill_pkg_display { type => 'R' };
- push @display, new FS::cust_bill_pkg_display { type => 'U',
- section => $section
- };
+ push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
+ push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
+
if ($section && $summary) {
- $display[2]->post_total('Y');
push @display, new FS::cust_bill_pkg_display { type => 'U',
summary => 'Y',
- }
+ %hash,
+ };
+ $hash{post_total} = 'Y';
}
+
+ $hash{section} = $section if $conf->exists('separate_usage');
+ push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
+
}
$cust_bill_pkg->set('display', \@display);
unless (@taxclassnums) {
@taxclassnums = map { $_->taxclassnum }
+ grep { $_->taxable eq 'Y' }
$part_pkg->part_pkg_taxrate('cch', $geocode, $class);
}
warn "Found taxclassnum values of ". join(',', @taxclassnums)
'payinfo' => $payinfo,
'paydate' => $paydate,
'recurring_billing' => $content{recurring_billing},
+ 'pkgnum' => $options{'pkgnum'},
'status' => 'new',
'gatewaynum' => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ),
};
'payinfo' => $payinfo,
'paybatch' => $paybatch,
'paydate' => $paydate,
+ 'pkgnum' => $options{'pkgnum'},
} );
#doesn't hurt to know, even though the dup check is in cust_pay_pending now
$cust_pay->payunique( $options{payunique} )
$template->compile()
or return "($perror) can't compile template: $Text::Template::ERROR";
- my $templ_hash = { error => $transaction->error_message };
+ my $templ_hash = {
+ 'company_name' =>
+ scalar( $conf->config('company_name', $self->agentnum ) ),
+ 'company_address' =>
+ join("\n", $conf->config('company_address', $self->agentnum ) ),
+ 'error' => $transaction->error_message,
+ };
my $error = send_email(
'from' => $conf->config('invoice_from', $self->agentnum ),
Returns false or a hashref upon success. The hashref contains keys popup_url reference, and collectitems. The first is a URL to which a browser should be redirected for completion of collection. The second is a reference id for the transaction suitable for the end user. The collectitems is a reference to a list of name value pairs suitable for assigning to a html form and posted to popup_url.
-Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
+Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
then it is deduced from the customer record.
'payinfo' => $options{payinfo},
'paydate' => $paydate,
'recurring_billing' => $content{recurring_billing},
+ 'pkgnum' => $options{'pkgnum'},
'status' => 'new',
'gatewaynum' => $payment_gateway->gatewaynum || '',
'session_id' => $options{session_id} || '',
#'payinfo' => $payinfo,
'paybatch' => $paybatch,
'paydate' => $cust_pay_pending->paydate,
+ 'pkgnum' => $cust_pay_pending->pkgnum,
} );
#doesn't hurt to know, even though the dup check is in cust_pay_pending now
$cust_pay->payunique( $options{payunique} )
$template->compile()
or return "($perror) can't compile template: $Text::Template::ERROR";
- my $templ_hash = { error => $transaction->error_message };
+ my $templ_hash = {
+ 'company_name' =>
+ scalar( $conf->config('company_name', $self->agentnum ) ),
+ 'company_address' =>
+ join("\n", $conf->config('company_address', $self->agentnum ) ),
+ 'error' => $transaction->error_message,
+ };
my $error = send_email(
'from' => $conf->config('invoice_from', $self->agentnum ),
@invoices = sort { $b->_date <=> $a->_date } @invoices
if defined($opt{'order'}) && $opt{'order'} eq 'newest';
+ if ( $conf->exists('pkg-balances') ) {
+ # limit @credits to those w/ a pkgnum grepped from $self
+ my %pkgnums = ();
+ foreach my $i (@invoices) {
+ foreach my $li ( $i->cust_bill_pkg ) {
+ $pkgnums{$li->pkgnum} = 1;
+ }
+ }
+ @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
+ }
+
my $credit;
+
foreach my $cust_bill ( @invoices ) {
- my $amount;
if ( !defined($credit) || $credit->credited == 0) {
$credit = pop @credits or last;
}
- if ($cust_bill->owed >= $credit->credited) {
- $amount=$credit->credited;
- }else{
- $amount=$cust_bill->owed;
+ my $owed;
+ if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
+ $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
+ } else {
+ $owed = $cust_bill->owed;
}
+ unless ( $owed > 0 ) {
+ push @credits, $credit;
+ next;
+ }
+
+ my $amount = min( $credit->credited, $owed );
my $cust_credit_bill = new FS::cust_credit_bill ( {
'crednum' => $credit->crednum,
'invnum' => $cust_bill->invnum,
'amount' => $amount,
} );
+ $cust_credit_bill->pkgnum( $credit->pkgnum )
+ if $conf->exists('pkg-balances') && $credit->pkgnum;
my $error = $cust_credit_bill->insert;
if ( $error ) {
$dbh->rollback or die $dbh->errstr if $oldAutoCommit;
die $error;
}
- redo if ($cust_bill->owed > 0);
+ redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
}
grep { $_->owed > 0 }
$self->cust_bill;
+ if ( $conf->exists('pkg-balances') ) {
+ # limit @payments to those w/ a pkgnum grepped from $self
+ my %pkgnums = ();
+ foreach my $i (@invoices) {
+ foreach my $li ( $i->cust_bill_pkg ) {
+ $pkgnums{$li->pkgnum} = 1;
+ }
+ }
+ @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
+ }
+
my $payment;
foreach my $cust_bill ( @invoices ) {
- my $amount;
if ( !defined($payment) || $payment->unapplied == 0 ) {
$payment = pop @payments or last;
}
- if ( $cust_bill->owed >= $payment->unapplied ) {
- $amount = $payment->unapplied;
+ my $owed;
+ if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
+ $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
} else {
- $amount = $cust_bill->owed;
+ $owed = $cust_bill->owed;
}
+ unless ( $owed > 0 ) {
+ push @payments, $payment;
+ next;
+ }
+
+ my $amount = min( $payment->unapplied, $owed );
my $cust_bill_pay = new FS::cust_bill_pay ( {
'paynum' => $payment->paynum,
'invnum' => $cust_bill->invnum,
'amount' => $amount,
} );
+ $cust_bill_pay->pkgnum( $payment->pkgnum )
+ if $conf->exists('pkg-balances') && $payment->pkgnum;
my $error = $cust_bill_pay->insert;
if ( $error ) {
$dbh->rollback or die $dbh->errstr if $oldAutoCommit;
die $error;
}
- redo if ( $cust_bill->owed > 0);
+ redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
}
}
+=item total_owed_pkgnum PKGNUM
+
+Returns the total owed on all invoices for this customer's specific package
+when using experimental package balances (see L<FS::cust_bill/owed_pkgnum>).
+
+=cut
+
+sub total_owed_pkgnum {
+ my( $self, $pkgnum ) = @_;
+ $self->total_owed_date_pkgnum(2145859200, $pkgnum); #12/31/2037
+}
+
+=item total_owed_date_pkgnum TIME PKGNUM
+
+Returns the total owed for this customer's specific package when using
+experimental package balances on all invoices with date earlier than
+TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
+see L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+sub total_owed_date_pkgnum {
+ my( $self, $time, $pkgnum ) = @_;
+
+ my $total_bill = 0;
+ foreach my $cust_bill (
+ grep { $_->_date <= $time }
+ qsearch('cust_bill', { 'custnum' => $self->custnum, } )
+ ) {
+ $total_bill += $cust_bill->owed_pkgnum($pkgnum);
+ }
+ sprintf( "%.2f", $total_bill );
+
+}
+
=item total_paid
Returns the total amount of all payments.
sprintf( "%.2f", $total_credit );
}
+=item total_unapplied_credits_pkgnum PKGNUM
+
+Returns the total outstanding credit (see L<FS::cust_credit>) for this
+customer. See L<FS::cust_credit/credited>.
+
+=cut
+
+sub total_unapplied_credits_pkgnum {
+ my( $self, $pkgnum ) = @_;
+ my $total_credit = 0;
+ $total_credit += $_->credited foreach $self->cust_credit_pkgnum($pkgnum);
+ sprintf( "%.2f", $total_credit );
+}
+
+
=item total_unapplied_payments
Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
sprintf( "%.2f", $total_unapplied );
}
+=item total_unapplied_payments_pkgnum PKGNUM
+
+Returns the total unapplied payments (see L<FS::cust_pay>) for this customer's
+specific package when using experimental package balances. See
+L<FS::cust_pay/unapplied>.
+
+=cut
+
+sub total_unapplied_payments_pkgnum {
+ my( $self, $pkgnum ) = @_;
+ my $total_unapplied = 0;
+ $total_unapplied += $_->unapplied foreach $self->cust_pay_pkgnum($pkgnum);
+ sprintf( "%.2f", $total_unapplied );
+}
+
+
=item total_unapplied_refunds
Returns the total unrefunded refunds (see L<FS::cust_refund>) for this
);
}
+=item balance_pkgnum PKGNUM
+
+Returns the balance for this customer's specific package when using
+experimental package balances (total_owed plus total_unrefunded, minus
+total_unapplied_credits minus total_unapplied_payments)
+
+=cut
+
+sub balance_pkgnum {
+ my( $self, $pkgnum ) = @_;
+
+ sprintf( "%.2f",
+ $self->total_owed_pkgnum($pkgnum)
+# n/a - refunds aren't part of pkg-balances since they don't apply to invoices
+# + $self->total_unapplied_refunds_pkgnum($pkgnum)
+ - $self->total_unapplied_credits_pkgnum($pkgnum)
+ - $self->total_unapplied_payments_pkgnum($pkgnum)
+ );
+}
+
=item in_transit_payments
Returns the total of requests for payments for this customer pending in
my( $self, $taxname ) = @_;
qsearchs( 'cust_main_exemption', { 'custnum' => $self->custnum,
- 'taxname' => { 'op' => 'LIKE',
- 'value' => $taxname.'%' },
- },
+ 'taxname' => $taxname,
+ },
);
}
+=item cust_main_exemption
+
+=cut
+
+sub cust_main_exemption {
+ my $self = shift;
+ qsearch( 'cust_main_exemption', { 'custnum' => $self->custnum } );
+}
+
=item invoicing_list [ ARRAYREF ]
If an arguement is given, sets these email addresses as invoice recipients
}
-=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;
qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
}
+=item cust_credit_pkgnum
+
+Returns all the credits (see L<FS::cust_credit>) for this customer's specific
+package when using experimental package balances.
+
+=cut
+
+sub cust_credit_pkgnum {
+ my( $self, $pkgnum ) = @_;
+ sort { $a->_date <=> $b->_date }
+ qsearch( 'cust_credit', { 'custnum' => $self->custnum,
+ 'pkgnum' => $pkgnum,
+ }
+ );
+}
+
=item cust_pay
Returns all the payments (see L<FS::cust_pay>) for this customer.
qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
}
+=item cust_pay_pkgnum
+
+Returns all the payments (see L<FS::cust_pay>) for this customer's specific
+package when using experimental package balances.
+
+=cut
+
+sub cust_pay_pkgnum {
+ my( $self, $pkgnum ) = @_;
+ sort { $a->_date <=> $b->_date }
+ qsearch( 'cust_pay', { 'custnum' => $self->custnum,
+ 'pkgnum' => $pkgnum,
+ }
+ );
+}
+
=item cust_pay_void
Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
}
+# Return a list of latitude/longitude for one of the services (if any)
+sub service_coordinates {
+ my $self = shift;
+
+ my @svc_X =
+ grep { $_->latitude && $_->longitude }
+ map { $_->svc_x }
+ map { $_->cust_svc }
+ $self->ncancelled_pkgs;
+
+ scalar(@svc_X) ? ( $svc_X[0]->latitude, $svc_X[0]->longitude ) : ()
+}
+
=back
=head1 CLASS METHODS
}
+=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;
}