use Carp;
use Exporter;
use Scalar::Util qw( blessed );
-use Time::Local qw(timelocal_nocheck);
+use Time::Local qw(timelocal timelocal_nocheck);
use Data::Dumper;
use Tie::IxHash;
use Digest::MD5 qw(md5_base64);
use Date::Format;
-use Date::Parse;
#use Date::Manip;
-use File::Slurp qw( slurp );
use File::Temp qw( tempfile );
use String::Approx qw(amatch);
use Business::CreditCard 0.28;
use FS::cust_svc;
use FS::cust_bill;
use FS::cust_bill_pkg;
+use FS::cust_bill_pkg_display;
use FS::cust_pay;
use FS::cust_pay_pending;
use FS::cust_pay_void;
use FS::cust_refund;
use FS::part_referral;
use FS::cust_main_county;
+use FS::tax_rate;
use FS::cust_tax_location;
+use FS::part_pkg_taxrate;
use FS::agent;
use FS::cust_main_invoice;
use FS::cust_credit_bill;
=item spool_cdr - Enable individual CDR spooling, empty or `Y'
+=item dundate - a suggestion to events (see L<FS::part_bill_event">) to delay until this unix timestamp
+
=item squelch_cdr - Discourage individual CDR printing, empty or `Y'
=back
$self->signupdate(time) unless $self->signupdate;
+ $self->auto_agent_custid()
+ if $conf->config('cust_main-auto_agent_custid') && ! $self->agent_custid;
+
my $error = $self->SUPER::insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
}
+use File::CounterFile;
+sub auto_agent_custid {
+ my $self = shift;
+
+ my $format = $conf->config('cust_main-auto_agent_custid');
+ my $agent_custid;
+ if ( $format eq '1YMMXXXXXXXX' ) {
+
+ my $counter = new File::CounterFile 'cust_main.agent_custid';
+ $counter->lock;
+
+ my $ym = 100000000000 + time2str('%y%m00000000', time);
+ if ( $ym > $counter->value ) {
+ $counter->{'value'} = $agent_custid = $ym;
+ $counter->{'updated'} = 1;
+ } else {
+ $agent_custid = $counter->inc;
+ }
+
+ $counter->unlock;
+
+ } else {
+ die "Unknown cust_main-auto_agent_custid format: $format";
+ }
+
+ $self->agent_custid($agent_custid);
+
+}
+
sub start_copy_skel {
my $self = shift;
|| $self->ut_textn('stateid')
|| $self->ut_textn('stateid_state')
|| $self->ut_textn('invoice_terms')
+ || $self->ut_alphan('geocode')
;
+
#barf. need message catalogs. i18n. etc.
$error .= "Please select an advertising source."
if $error =~ /^Illegal or empty \(numeric\) refnum: /;
$self->ncancelled_pkgs;
foreach my $cust_pkg ( @cancel_pkgs ) {
- my $error = $cust_pkg->cancel;
+ my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
+ my $error = $cust_pkg->cancel($cpr ? ( 'reason' => $cpr->reasonnum,
+ 'reason_otaker' => $cpr->otaker
+ )
+ : ()
+ );
warn "Error cancelling expired pkg ". $cust_pkg->pkgnum.
" for custnum ". $self->custnum. ": $error"
if $error;
$self->ncancelled_pkgs;
foreach my $cust_pkg ( @susp_pkgs ) {
- my $error = $cust_pkg->suspend;
+ my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
+ if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
+ my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
+ 'reason_otaker' => $cpr->otaker
+ )
+ : ()
+ );
+
warn "Error suspending package ". $cust_pkg->pkgnum.
" for custnum ". $self->custnum. ": $error"
if $error;
if $DEBUG;
my $time = $options{'time'} || time;
+ my $invoice_time = $options{'invoice_time'} || $time;
#put below somehow?
local $SIG{HUP} = 'IGNORE';
$self->select_for_update; #mutex
my @cust_bill_pkg = ();
- my @appended_cust_bill_pkg = ();
###
# find the packages which are due for billing, find out how much they are
'cust_pkg' => $cust_pkg,
'precommit_hooks' => \@precommit_hooks,
'line_items' => \@cust_bill_pkg,
- 'appended_line_items' => \@appended_cust_bill_pkg,
'setup' => \$total_setup,
'recur' => \$total_recur,
'tax_matrix' => \%taxlisthash,
} #foreach my $cust_pkg
- push @cust_bill_pkg, @appended_cust_bill_pkg;
-
unless ( @cust_bill_pkg ) { #don't create an invoice w/o line items
#but do commit any package date cycling that happened
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
return "can't charge postal invoice fee for customer ".
$self->custnum. ": $postal_pkg";
}
- if ( $postal_pkg ) {
+ if ( $postal_pkg &&
+ ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
+ !$conf->exists('postal_invoice-recurring_only')
+ )
+ )
+ {
foreach my $part_pkg ( $postal_pkg->part_pkg->self_and_bill_linked ) {
my $error =
$self->_make_lines( 'part_pkg' => $part_pkg,
'cust_pkg' => $postal_pkg,
'precommit_hooks' => \@precommit_hooks,
'line_items' => \@cust_bill_pkg,
- 'appended_line_items' => \@appended_cust_bill_pkg,
'setup' => \$total_setup,
'recur' => \$total_recur,
'tax_matrix' => \%taxlisthash,
foreach my $tax ( keys %taxlisthash ) {
my $tax_object = shift @{ $taxlisthash{$tax} };
warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
- my $listref_or_error = $tax_object->taxline( @{ $taxlisthash{$tax} } );
+ my $listref_or_error =
+ $tax_object->taxline( $taxlisthash{$tax},
+ 'custnum' => $self->custnum,
+ 'invoice_time' => $invoice_time
+ );
unless (ref($listref_or_error)) {
$dbh->rollback if $oldAutoCommit;
return $listref_or_error;
warn "adding ". $listref_or_error->[1].
" as ". $listref_or_error->[0]. "\n"
if $DEBUG > 2;
- $tax{ $tax_object->taxname } += $listref_or_error->[1];
+ $tax{ $tax } += $listref_or_error->[1];
if ( $taxname{ $listref_or_error->[0] } ) {
- push @{ $taxname{ $listref_or_error->[0] } }, $tax_object->taxname;
+ push @{ $taxname{ $listref_or_error->[0] } }, $tax;
}else{
- $taxname{ $listref_or_error->[0] } = [ $tax_object->taxname ];
+ $taxname{ $listref_or_error->[0] } = [ $tax ];
}
}
+ #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
+ my %packagemap = map { $_->pkgnum => $_ } @cust_bill_pkg;
+ foreach my $tax ( keys %taxlisthash ) {
+ foreach ( @{ $taxlisthash{$tax} }[1 ... scalar(@{ $taxlisthash{$tax} })] ) {
+ next unless ref($_) eq 'FS::cust_bill_pkg'; # shouldn't happen
+
+ push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
+ splice( @{ $_->_cust_tax_exempt_pkg } );
+ }
+ }
+
#some taxes are taxed
my %totlisthash;
# existing taxes
warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
if ( exists( $totlisthash{ $totname } ) ) {
- push @{ $totlisthash{ $totname } }, $tax{ $tax_object->taxname };
+ push @{ $totlisthash{ $totname } }, $tax{ $tax };
}else{
- $totlisthash{ $totname } = [ $tot, $tax{ $tax_object->taxname } ];
+ $totlisthash{ $totname } = [ $tot, $tax{ $tax } ];
}
}
}
my $tax_object = shift @{ $totlisthash{$tax} };
warn "found previously found taxed tax ". $tax_object->taxname. "\n"
if $DEBUG > 2;
- my $listref_or_error = $tax_object->taxline( @{ $totlisthash{$tax} } );
+ my $listref_or_error =
+ $tax_object->taxline( $totlisthash{$tax},
+ 'custnum' => $self->custnum,
+ 'invoice_time' => $invoice_time
+ );
unless (ref($listref_or_error)) {
$dbh->rollback if $oldAutoCommit;
return $listref_or_error;
warn "adding taxed tax amount ". $listref_or_error->[1].
" as ". $tax_object->taxname. "\n"
if $DEBUG;
- $tax{ $tax_object->taxname } += $listref_or_error->[1];
+ $tax{ $tax } += $listref_or_error->[1];
}
#consolidate and create tax line items
warn "adding $taxname\n" if $DEBUG > 1;
foreach my $taxitem ( @{ $taxname{$taxname} } ) {
$tax += $tax{$taxitem} unless $seen{$taxitem};
+ $seen{$taxitem} = 1;
warn "adding $tax{$taxitem}\n" if $DEBUG > 1;
}
next unless $tax;
#create the new invoice
my $cust_bill = new FS::cust_bill ( {
'custnum' => $self->custnum,
- '_date' => ( $options{'invoice_time'} || $time ),
+ '_date' => ( $invoice_time ),
'charged' => $charged,
} );
my $error = $cust_bill->insert;
my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
my $precommit_hooks = $params{precommit_hooks} or die "no package specified";
my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
- my $appended_cust_bill_pkg = $params{appended_line_items}
- or die "no appended line buffer specified";
my $total_setup = $params{setup} or die "no setup accumulator specified";
my $total_recur = $params{recur} or die "no recur accumulator specified";
my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified";
my $recur = 0;
my $unitrecur = 0;
my $sdate;
- if ( $part_pkg->getfield('freq') ne '0' &&
- ! $cust_pkg->getfield('susp') &&
- ( $cust_pkg->getfield('bill') || 0 ) <= $time
+ if ( ! $cust_pkg->getfield('susp') 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')
+ )
) {
# XXX should this be a package event? probably. events are called
$sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
#over two params! lets at least switch to a hashref for the rest...
- my %param = ( 'precommit_hooks' => $precommit_hooks, );
+ my $increment_next_bill = ( $part_pkg->freq ne '0'
+ && ( $cust_pkg->getfield('bill') || 0 ) <= $time
+ );
+ 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"
if ( $@ );
+ if ( $increment_next_bill ) {
- #change this bit to use Date::Manip? CAREFUL with timezones (see
- # mailing list archive)
- my ($sec,$min,$hour,$mday,$mon,$year) =
- (localtime($sdate) )[0,1,2,3,4,5];
-
- #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
- # only for figuring next bill date, nothing else, so, reset $sdate again
- # here
- $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
- $cust_pkg->last_bill($sdate);
-
- if ( $part_pkg->freq =~ /^\d+$/ ) {
- $mon += $part_pkg->freq;
- until ( $mon < 12 ) { $mon -= 12; $year++; }
- } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
- my $weeks = $1;
- $mday += $weeks * 7;
- } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
- my $days = $1;
- $mday += $days;
- } elsif ( $part_pkg->freq =~ /^(\d+)h$/ ) {
- my $hours = $1;
- $hour += $hours;
- } else {
- return "unparsable frequency: ". $part_pkg->freq;
+ #change this bit to use Date::Manip? CAREFUL with timezones (see
+ # mailing list archive)
+ my ($sec,$min,$hour,$mday,$mon,$year) =
+ (localtime($sdate) )[0,1,2,3,4,5];
+
+ #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
+ # only for figuring next bill date, nothing else, so, reset $sdate again
+ # here
+ $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
+ #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
+ $cust_pkg->last_bill($sdate);
+
+ if ( $part_pkg->freq =~ /^\d+$/ ) {
+ $mon += $part_pkg->freq;
+ until ( $mon < 12 ) { $mon -= 12; $year++; }
+ } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
+ my $weeks = $1;
+ $mday += $weeks * 7;
+ } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
+ my $days = $1;
+ $mday += $days;
+ } elsif ( $part_pkg->freq =~ /^(\d+)h$/ ) {
+ my $hours = $1;
+ $hour += $hours;
+ } else {
+ return "unparsable frequency: ". $part_pkg->freq;
+ }
+ $cust_pkg->setfield('bill',
+ timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
+
}
- $cust_pkg->setfield('bill',
- timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
}
warn " charges (setup=$setup, recur=$recur); adding line items\n"
if $DEBUG > 1;
+
+ my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
+ if ( $DEBUG > 1 ) {
+ warn " adding customer package invoice detail: $_\n"
+ foreach @cust_pkg_detail;
+ }
+ push @details, @cust_pkg_detail;
+
my $cust_bill_pkg = new FS::cust_bill_pkg {
'pkgnum' => $cust_pkg->pkgnum,
'setup' => $setup,
'recur' => $recur,
'unitrecur' => $unitrecur,
'quantity' => $cust_pkg->quantity,
- 'sdate' => $sdate,
- 'edate' => $cust_pkg->bill,
'details' => \@details,
};
+
+ 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
+ } 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->pkgpart_override($part_pkg->pkgpart)
unless $part_pkg->pkgpart == $real_pkgpart;
- push @$cust_bill_pkgs, $cust_bill_pkg;
$$total_setup += $setup;
$$total_recur += $recur;
# handle taxes
###
- unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
-
- $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg);
+ my $error =
+ $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg);
+ return $error if $error;
- } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
+ push @$cust_bill_pkgs, $cust_bill_pkg;
} #if $setup != 0 || $recur != 0
} #if $line_items
- if ( $part_pkg->can('append_cust_bill_pkgs') ) {
- my %param = ( 'precommit_hooks' => $precommit_hooks, );
- my ($more_cust_bill_pkgs) =
- eval { $part_pkg->append_cust_bill_pkgs( $cust_pkg, \$sdate, \%param ) };
-
- return "$@ running append_cust_bill_pkgs for $cust_pkg\n"
- if ( $@ );
- return "$more_cust_bill_pkgs"
- unless ( ref($more_cust_bill_pkgs) );
-
- foreach my $cust_bill_pkg ( @{$more_cust_bill_pkgs} ) {
-
- $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
- unless $part_pkg->pkgpart == $real_pkgpart;
- push @$appended_cust_bill_pkg, $cust_bill_pkg;
-
- unless ($cust_bill_pkg->duplicate) {
- $$total_setup += $cust_bill_pkg->setup;
- $$total_recur += $cust_bill_pkg->recur;
-
- ###
- # handle taxes
- ###
-
- unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
-
- $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg);
-
- } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
- }
- }
- }
+ '';
}
my $part_pkg = shift;
my $taxlisthash = shift;
my $cust_bill_pkg = shift;
+ my $cust_pkg = shift;
- my @taxes = ();
- my @taxoverrides = $part_pkg->part_pkg_taxoverride;
+ my %cust_bill_pkg = ();
+ my %taxes = ();
my $prefix =
( $conf->exists('tax-ship_address') && length($self->ship_last) )
? 'ship_'
: '';
+ my @classes;
+ #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
+ push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
+ push @classes, 'setup' if $cust_bill_pkg->setup;
+ push @classes, 'recur' if $cust_bill_pkg->recur;
+
if ( $conf->exists('enable_taxproducts')
- && (scalar(@taxoverrides) || $part_pkg->taxproductnum )
+ && (scalar($part_pkg->part_pkg_taxoverride) || $part_pkg->has_taxproduct)
+ && ( $self->tax !~ /Y/i && $self->payby ne 'COMP' )
)
{
- my @taxclassnums = ();
- my $geocode = $self->geocode('cch');
-
- if ( scalar( @taxoverrides ) ) {
- @taxclassnums = map { $_->taxclassnum } @taxoverrides;
- }elsif ( $part_pkg->taxproductnum ) {
- @taxclassnums = map { $_->taxclassnum }
- $part_pkg->part_pkg_taxrate('cch', $geocode);
+ foreach my $class (@classes) {
+ my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $prefix );
+ return $err_or_ref unless ref($err_or_ref);
+ $taxes{$class} = $err_or_ref;
}
- my $extra_sql =
- "AND (".
- join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
-
- @taxes = qsearch({ 'table' => 'tax_rate',
- 'hashref' => { 'geocode' => $geocode, },
- 'extra_sql' => $extra_sql,
- })
- if scalar(@taxclassnums);
-
+ unless (exists $taxes{''}) {
+ my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $prefix );
+ return $err_or_ref unless ref($err_or_ref);
+ $taxes{''} = $err_or_ref;
+ }
- }else{
+ } elsif ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) {
my %taxhash = map { $_ => $self->get("$prefix$_") }
qw( state county country );
$taxhash{'taxclass'} = $part_pkg->taxclass;
- @taxes = qsearch( 'cust_main_county', \%taxhash );
+ my @taxes = qsearch( 'cust_main_county', \%taxhash );
unless ( @taxes ) {
$taxhash{'taxclass'} = '';
@taxes = qsearch( 'cust_main_county', \%taxhash );
}
- } #if $conf->exists('enable_taxproducts')
+ $taxes{''} = [ @taxes ];
+ $taxes{'setup'} = [ @taxes ];
+ $taxes{'recur'} = [ @taxes ];
+ $taxes{$_} = [ @taxes ] foreach (@classes);
- # maybe eliminate this entirely, along with all the 0% records
- unless ( @taxes ) {
- my $error;
- if ( $conf->exists('enable_taxproducts') ) {
- $error =
- "fatal: can't find tax rate for zip/taxproduct/pkgpart ".
- join('/', ( map $self->get("$prefix$_"),
- qw(zip)
- ),
- $part_pkg->taxproduct_description,
- $part_pkg->pkgpart ). "\n";
- } else {
- $error =
+ # 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 $self->get("$prefix$_"),
qw(state county country)
),
$part_pkg->taxclass ). "\n";
}
- return $error;
+
+ } #if $conf->exists('enable_taxproducts') ...
+
+ my @display = ();
+ if ( $conf->exists('separate_usage') ) {
+ 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
+ };
+ if ($section && $summary) {
+ $display[2]->post_total('Y');
+ push @display, new FS::cust_bill_pkg_display { type => 'U',
+ summary => 'Y',
+ }
+ }
}
+ $cust_bill_pkg->set('display', \@display);
- foreach my $tax ( @taxes ) {
- my $taxname = ref( $tax ). ' '. $tax->taxnum;
- if ( exists( $taxlisthash->{ $taxname } ) ) {
- push @{ $taxlisthash->{ $taxname } }, $cust_bill_pkg;
- }else{
- $taxlisthash->{ $taxname } = [ $tax, $cust_bill_pkg ];
+ my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
+ foreach my $key (keys %tax_cust_bill_pkg) {
+ my @taxes = @{ $taxes{$key} || [] };
+ my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
+
+ foreach my $tax ( @taxes ) {
+ my $taxname = ref( $tax ). ' '. $tax->taxnum;
+ if ( exists( $taxlisthash->{ $taxname } ) ) {
+ push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
+ }else{
+ $taxlisthash->{ $taxname } = [ $tax, $tax_cust_bill_pkg ];
+ }
}
}
+ '';
+}
+
+sub _gather_taxes {
+ my $self = shift;
+ my $part_pkg = shift;
+ my $class = shift;
+ my $prefix = shift;
+
+ my @taxes = ();
+ my $geocode = $self->geocode('cch');
+
+ my @taxclassnums = map { $_->taxclassnum }
+ $part_pkg->part_pkg_taxoverride($class);
+
+ unless (@taxclassnums) {
+ @taxclassnums = map { $_->taxclassnum }
+ $part_pkg->part_pkg_taxrate('cch', $geocode, $class);
+ }
+ warn "Found taxclassnum values of ". join(',', @taxclassnums)
+ if $DEBUG;
+
+ my $extra_sql =
+ "AND (".
+ join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
+
+ @taxes = qsearch({ 'table' => 'tax_rate',
+ 'hashref' => { 'geocode' => $geocode, },
+ 'extra_sql' => $extra_sql,
+ })
+ if scalar(@taxclassnums);
+
+ # maybe eliminate this entirely, along with all the 0% records
+ unless ( @taxes ) {
+ return
+ "fatal: can't find tax rate for zip/taxproduct/pkgpart ".
+ join('/', ( map $self->get("$prefix$_"),
+ qw(zip)
+ ),
+ $part_pkg->taxproduct_description,
+ $part_pkg->pkgpart ). "\n";
+ }
+
+ warn "Found taxes ".
+ join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n"
+ if $DEBUG;
+
+ [ @taxes ];
+
}
=item collect OPTIONS
# 3: insert
##
- foreach my $cust_event ( @cust_event ) {
+ unless( $opt{testonly} ) {
+ foreach my $cust_event ( @cust_event ) {
- my $error = $cust_event->insert();
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return $error;
- }
+ my $error = $cust_event->insert();
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
}
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
die $error;
}
- my $unapplied = $self->total_credited + $self->total_unapplied_payments + $self->in_transit_payments;
+ my $unapplied = $self->total_unapplied_credits
+ + $self->total_unapplied_payments
+ + $self->in_transit_payments;
foreach my $cust_bill ($self->open_cust_bill) {
#$dbh->commit or die $dbh->errstr if $oldAutoCommit;
my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
'';
}
-=item total_owed
-
-Returns the total owed for this customer on all invoices
-(see L<FS::cust_bill/owed>).
-
-=cut
-
-sub total_owed {
- my $self = shift;
- $self->total_owed_date(2145859200); #12/31/2037
-}
-
-=item total_owed_date TIME
-
-Returns the total owed for this customer 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 {
- my $self = shift;
- my $time = shift;
- my $total_bill = 0;
- foreach my $cust_bill (
- grep { $_->_date <= $time }
- qsearch('cust_bill', { 'custnum' => $self->custnum, } )
- ) {
- $total_bill += $cust_bill->owed;
- }
- sprintf( "%.2f", $total_bill );
-}
-
=item apply_payments_and_credits
Applies unapplied payments and credits.
$self->select_for_update; #mutex
- unless ( $self->total_credited ) {
+ unless ( $self->total_unapplied_credits ) {
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
return 0;
}
}
- my $total_credited = $self->total_credited;
+ my $total_unapplied_credits = $self->total_unapplied_credits;
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
- return $total_credited;
+ return $total_unapplied_credits;
}
=item apply_payments
#return 0 unless
- my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
- qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
+ my @payments = sort { $b->_date <=> $a->_date }
+ grep { $_->unapplied > 0 }
+ $self->cust_pay;
- my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
- qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
+ my @invoices = sort { $a->_date <=> $b->_date}
+ grep { $_->owed > 0 }
+ $self->cust_bill;
my $payment;
return $total_unapplied_payments;
}
-=item total_credited
+=item total_owed
+
+Returns the total owed for this customer on all invoices
+(see L<FS::cust_bill/owed>).
+
+=cut
+
+sub total_owed {
+ my $self = shift;
+ $self->total_owed_date(2145859200); #12/31/2037
+}
+
+=item total_owed_date TIME
+
+Returns the total owed for this customer 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 {
+ my $self = shift;
+ my $time = shift;
+ my $total_bill = 0;
+ foreach my $cust_bill (
+ grep { $_->_date <= $time }
+ qsearch('cust_bill', { 'custnum' => $self->custnum, } )
+ ) {
+ $total_bill += $cust_bill->owed;
+ }
+ sprintf( "%.2f", $total_bill );
+}
+
+=item total_paid
+
+Returns the total amount of all payments.
+
+=cut
+
+sub total_paid {
+ my $self = shift;
+ my $total = 0;
+ $total += $_->paid foreach $self->cust_pay;
+ sprintf( "%.2f", $total );
+}
+
+=item total_unapplied_credits
Returns the total outstanding credit (see L<FS::cust_credit>) for this
customer. See L<FS::cust_credit/credited>.
+=item total_credited
+
+Old name for total_unapplied_credits. Don't use.
+
=cut
sub total_credited {
+ #carp "total_credited deprecated, use total_unapplied_credits";
+ shift->total_unapplied_credits(@_);
+}
+
+sub total_unapplied_credits {
my $self = shift;
my $total_credit = 0;
- foreach my $cust_credit ( qsearch('cust_credit', {
- 'custnum' => $self->custnum,
- } ) ) {
- $total_credit += $cust_credit->credited;
- }
+ $total_credit += $_->credited foreach $self->cust_credit;
sprintf( "%.2f", $total_credit );
}
sub total_unapplied_payments {
my $self = shift;
my $total_unapplied = 0;
- foreach my $cust_pay ( qsearch('cust_pay', {
- 'custnum' => $self->custnum,
- } ) ) {
- $total_unapplied += $cust_pay->unapplied;
- }
+ $total_unapplied += $_->unapplied foreach $self->cust_pay;
sprintf( "%.2f", $total_unapplied );
}
sub total_unapplied_refunds {
my $self = shift;
my $total_unapplied = 0;
- foreach my $cust_refund ( qsearch('cust_refund', {
- 'custnum' => $self->custnum,
- } ) ) {
- $total_unapplied += $cust_refund->unapplied;
- }
+ $total_unapplied += $_->unapplied foreach $self->cust_refund;
sprintf( "%.2f", $total_unapplied );
}
=item balance
Returns the balance for this customer (total_owed plus total_unrefunded, minus
-total_credited minus total_unapplied_payments).
+total_unapplied_credits minus total_unapplied_payments).
=cut
sprintf( "%.2f",
$self->total_owed
+ $self->total_unapplied_refunds
- - $self->total_credited
+ - $self->total_unapplied_credits
- $self->total_unapplied_payments
);
}
sprintf( "%.2f",
$self->total_owed_date($time)
+ $self->total_unapplied_refunds
- - $self->total_credited
+ - $self->total_unapplied_credits
- $self->total_unapplied_payments
);
}
qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
}
-=item credit AMOUNT, REASON
+=item credit AMOUNT, REASON [ , OPTION => VALUE ... ]
Applies a credit to this customer. If there is an error, returns the error,
otherwise returns false.
+REASON can be a text string, an FS::reason object, or a scalar reference to
+a reasonnum. If a text string, it will be automatically inserted as a new
+reason, and a 'reason_type' option must be passed to indicate the
+FS::reason_type for the new reason.
+
+An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
+
+Any other options are passed to FS::cust_credit::insert.
+
=cut
sub credit {
my( $self, $amount, $reason, %options ) = @_;
+
my $cust_credit = new FS::cust_credit {
'custnum' => $self->custnum,
'amount' => $amount,
- 'reason' => $reason,
};
+
+ if ( ref($reason) ) {
+
+ if ( ref($reason) eq 'SCALAR' ) {
+ $cust_credit->reasonnum( $$reason );
+ } else {
+ $cust_credit->reasonnum( $reason->reasonnum );
+ }
+
+ } else {
+ $cust_credit->set('reason', $reason)
+ }
+
+ $cust_credit->addlinfo( delete $options{'addlinfo'} )
+ if exists($options{'addlinfo'});
+
$cust_credit->insert(%options);
+
}
=item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
sub charge {
my $self = shift;
my ( $amount, $quantity, $pkg, $comment, $taxclass, $additional, $classnum );
+ my ( $taxproduct, $override );
if ( ref( $_[0] ) ) {
$amount = $_[0]->{amount};
$quantity = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
$taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
$classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
$additional = $_[0]->{additional};
+ $taxproduct = $_[0]->{taxproductnum};
+ $override = { '' => $_[0]->{tax_override} };
}else{
$amount = shift;
$quantity = 1;
my $dbh = dbh;
my $part_pkg = new FS::part_pkg ( {
- 'pkg' => $pkg,
- 'comment' => $comment,
- 'plan' => 'flat',
- 'freq' => 0,
- 'disabled' => 'Y',
- 'classnum' => $classnum ? $classnum : '',
- 'taxclass' => $taxclass,
+ 'pkg' => $pkg,
+ 'comment' => $comment,
+ 'plan' => 'flat',
+ 'freq' => 0,
+ 'disabled' => 'Y',
+ 'classnum' => $classnum ? $classnum : '',
+ 'taxclass' => $taxclass,
+ 'taxproductnum' => $taxproduct,
} );
my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
'setup_fee' => $amount,
);
- my $error = $part_pkg->insert( options => \%options );
+ my $error = $part_pkg->insert( options => \%options,
+ tax_overrides => $override,
+ );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error;
qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
}
+=item display_custnum
+
+Returns the displayed customer number for this customer: agent_custid if
+cust_main-default_agent_custid is set and it has a value, custnum otherwise.
+
+=cut
+
+sub display_custnum {
+ my $self = shift;
+ if ( $conf->exists('cust_main-default_agent_custid') && $self->agent_custid ){
+ return $self->agent_custid;
+ } else {
+ return $self->custnum;
+ }
+}
+
=item name
Returns a name string for this customer, either "Company (Last, First)" or
sub geocode {
my ($self, $data_vendor) = (shift, shift); #always cch for now
+ my $geocode = $self->get('geocode'); #XXX only one data_vendor for geocode
+ return $geocode if $geocode;
+
my $prefix = ( $conf->exists('tax-ship_address') && length($self->ship_last) )
? 'ship_'
: '';
#CCH specific location stuff
my $extra_sql = "AND plus4lo <= '$plus4' AND plus4hi >= '$plus4'";
- my $geocode = '';
- my $cust_tax_location =
- qsearchs( {
- 'table' => 'cust_tax_location',
- 'hashref' => { 'zip' => $zip, 'data_vendor' => $data_vendor },
- 'extra_sql' => $extra_sql,
- }
- );
- $geocode = $cust_tax_location->geocode
- if $cust_tax_location;
+ my @cust_tax_location =
+ qsearch( {
+ 'table' => 'cust_tax_location',
+ 'hashref' => { 'zip' => $zip, 'data_vendor' => $data_vendor },
+ 'extra_sql' => $extra_sql,
+ 'order_by' => 'ORDER BY plus4hi',#overlapping with distinct ends
+ }
+ );
+ $geocode = $cust_tax_location[0]->geocode
+ if scalar(@cust_tax_location);
$geocode;
}
Returns an SQL fragment to retreive the balance for this customer, only
considering invoices with date earlier than START_TIME, and optionally not
-later than END_TIME (total_owed_date minus total_credited minus
+later than END_TIME (total_owed_date minus total_unapplied_credits minus
total_unapplied_payments).
Times are specified as SQL fragments or numeric
# custnum search (also try agent_custid), with some tweaking options if your
# legacy cust "numbers" have letters
- } elsif ( $search =~ /^\s*(\d+)\s*$/
+ }
+
+ if ( $search =~ /^\s*(\d+)\s*$/
|| ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+'
&& $search =~ /^\s*(\w\w?\d+)\s*$/
)
)
{
- push @cust_main, qsearch( {
- 'table' => 'cust_main',
- 'hashref' => { 'custnum' => $1, %options },
- 'extra_sql' => " AND $agentnums_sql", #agent virtualization
- } );
+ my $num = $1;
+
+ if ( $num <= 2147483647 ) { #need a bigint custnum? wow.
+ push @cust_main, qsearch( {
+ 'table' => 'cust_main',
+ 'hashref' => { 'custnum' => $num, %options },
+ 'extra_sql' => " AND $agentnums_sql", #agent virtualization
+ } );
+ }
push @cust_main, qsearch( {
'table' => 'cust_main',
- 'hashref' => { 'agent_custid' => $1, %options },
+ 'hashref' => { 'agent_custid' => $num, %options },
'extra_sql' => " AND $agentnums_sql", #agent virtualization
} );
1;
}
-=item process_batch_import
-
-Load a batch import as a queued JSRPC job
-
-=cut
-
-use Storable qw(thaw);
-use Data::Dumper;
-use MIME::Base64;
-sub process_batch_import {
- my $job = shift;
-
- my $param = thaw(decode_base64(shift));
- warn Dumper($param) if $DEBUG;
-
- my $files = $param->{'uploaded_files'}
- or die "No files provided.\n";
-
- my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
-
- my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/';
- my $file = $dir. $files{'file'};
-
- my $type;
- if ( $file =~ /\.(\w+)$/i ) {
- $type = lc($1);
- } else {
- #or error out???
- warn "can't parse file type from filename $file; defaulting to CSV";
- $type = 'csv';
- }
-
- my $error =
- FS::cust_main::batch_import( {
- job => $job,
- file => $file,
- type => $type,
- custbatch => $param->{custbatch},
- agentnum => $param->{'agentnum'},
- refnum => $param->{'refnum'},
- pkgpart => $param->{'pkgpart'},
- #'fields' => [qw( cust_pkg.setup dayphone first last address1 address2
- # city state zip comments )],
- 'format' => $param->{'format'},
- } );
-
- unlink $file;
-
- die "$error\n" if $error;
-
-}
-
-=item batch_import
-
-=cut
-
-#some false laziness w/cdr.pm now
-sub batch_import {
- my $param = shift;
-
- my $job = $param->{job};
-
- my $filename = $param->{file};
- my $type = $param->{type} || 'csv';
-
- my $custbatch = $param->{custbatch};
-
- my $agentnum = $param->{agentnum};
- my $refnum = $param->{refnum};
- my $pkgpart = $param->{pkgpart};
-
- my $format = $param->{'format'};
-
- my @fields;
- my $payby;
- if ( $format eq 'simple' ) {
- @fields = qw( cust_pkg.setup dayphone first last
- address1 address2 city state zip comments );
- $payby = 'BILL';
- } elsif ( $format eq 'extended' ) {
- @fields = qw( agent_custid refnum
- last first address1 address2 city state zip country
- daytime night
- ship_last ship_first ship_address1 ship_address2
- ship_city ship_state ship_zip ship_country
- payinfo paycvv paydate
- invoicing_list
- cust_pkg.pkgpart
- svc_acct.username svc_acct._password
- );
- $payby = 'BILL';
- } elsif ( $format eq 'extended-plus_company' ) {
- @fields = qw( agent_custid refnum
- last first company address1 address2 city state zip country
- daytime night
- ship_last ship_first ship_company ship_address1 ship_address2
- ship_city ship_state ship_zip ship_country
- payinfo paycvv paydate
- invoicing_list
- cust_pkg.pkgpart
- svc_acct.username svc_acct._password
- );
- $payby = 'BILL';
- } else {
- die "unknown format $format";
- }
-
- my $count;
- my $parser;
- my @buffer = ();
- if ( $type eq 'csv' ) {
-
- eval "use Text::CSV_XS;";
- die $@ if $@;
-
- $parser = new Text::CSV_XS;
-
- @buffer = split(/\r?\n/, slurp($filename) );
- $count = scalar(@buffer);
-
- } elsif ( $type eq 'xls' ) {
-
- eval "use Spreadsheet::ParseExcel;";
- die $@ if $@;
-
- my $excel = new Spreadsheet::ParseExcel::Workbook->Parse($filename);
- $parser = $excel->{Worksheet}[0]; #first sheet
-
- $count = $parser->{MaxRow} || $parser->{MinRow};
- $count++;
-
- } else {
- die "Unknown file type $type\n";
- }
-
- #my $columns;
-
- local $SIG{HUP} = 'IGNORE';
- local $SIG{INT} = 'IGNORE';
- local $SIG{QUIT} = 'IGNORE';
- local $SIG{TERM} = 'IGNORE';
- local $SIG{TSTP} = 'IGNORE';
- local $SIG{PIPE} = 'IGNORE';
-
- my $oldAutoCommit = $FS::UID::AutoCommit;
- local $FS::UID::AutoCommit = 0;
- my $dbh = dbh;
-
- my $line;
- my $row = 0;
- my( $last, $min_sec ) = ( time, 5 ); #progressbar foo
- while (1) {
-
- my @columns = ();
- if ( $type eq 'csv' ) {
-
- last unless scalar(@buffer);
- $line = shift(@buffer);
-
- $parser->parse($line) or do {
- $dbh->rollback if $oldAutoCommit;
- return "can't parse: ". $parser->error_input();
- };
- @columns = $parser->fields();
-
- } elsif ( $type eq 'xls' ) {
-
- last if $row > ($parser->{MaxRow} || $parser->{MinRow});
-
- my @row = @{ $parser->{Cells}[$row] };
- @columns = map $_->{Val}, @row;
-
- #my $z = 'A';
- #warn $z++. ": $_\n" for @columns;
-
- } else {
- die "Unknown file type $type\n";
- }
-
- #warn join('-',@columns);
-
- my %cust_main = (
- custbatch => $custbatch,
- agentnum => $agentnum,
- refnum => $refnum,
- country => $conf->config('countrydefault') || 'US',
- payby => $payby, #default
- paydate => '12/2037', #default
- );
- my $billtime = time;
- my %cust_pkg = ( pkgpart => $pkgpart );
- my %svc_acct = ();
- foreach my $field ( @fields ) {
-
- if ( $field =~ /^cust_pkg\.(pkgpart|setup|bill|susp|adjourn|expire|cancel)$/ ) {
-
- #$cust_pkg{$1} = str2time( shift @$columns );
- if ( $1 eq 'pkgpart' ) {
- $cust_pkg{$1} = shift @columns;
- } elsif ( $1 eq 'setup' ) {
- $billtime = str2time(shift @columns);
- } else {
- $cust_pkg{$1} = str2time( shift @columns );
- }
-
- } elsif ( $field =~ /^svc_acct\.(username|_password)$/ ) {
-
- $svc_acct{$1} = shift @columns;
-
- } else {
-
- #refnum interception
- if ( $field eq 'refnum' && $columns[0] !~ /^\s*(\d+)\s*$/ ) {
-
- my $referral = $columns[0];
- my %hash = ( 'referral' => $referral,
- 'agentnum' => $agentnum,
- 'disabled' => '',
- );
-
- my $part_referral = qsearchs('part_referral', \%hash )
- || new FS::part_referral \%hash;
-
- unless ( $part_referral->refnum ) {
- my $error = $part_referral->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't auto-insert advertising source: $referral: $error";
- }
- }
-
- $columns[0] = $part_referral->refnum;
- }
-
- my $value = shift @columns;
- $cust_main{$field} = $value if length($value);
- }
- }
-
- $cust_main{'payby'} = 'CARD'
- if defined $cust_main{'payinfo'}
- && length $cust_main{'payinfo'};
-
- my $invoicing_list = $cust_main{'invoicing_list'}
- ? [ delete $cust_main{'invoicing_list'} ]
- : [];
-
- my $cust_main = new FS::cust_main ( \%cust_main );
-
- use Tie::RefHash;
- tie my %hash, 'Tie::RefHash'; #this part is important
-
- if ( $cust_pkg{'pkgpart'} ) {
- my $cust_pkg = new FS::cust_pkg ( \%cust_pkg );
-
- my @svc_acct = ();
- if ( $svc_acct{'username'} ) {
- my $part_pkg = $cust_pkg->part_pkg;
- unless ( $part_pkg ) {
- $dbh->rollback if $oldAutoCommit;
- return "unknown pkgpart: ". $cust_pkg{'pkgpart'};
- }
- $svc_acct{svcpart} = $part_pkg->svcpart( 'svc_acct' );
- push @svc_acct, new FS::svc_acct ( \%svc_acct )
- }
-
- $hash{$cust_pkg} = \@svc_acct;
- }
-
- my $error = $cust_main->insert( \%hash, $invoicing_list );
-
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't insert customer". ( $line ? " for $line" : '' ). ": $error";
- }
-
- if ( $format eq 'simple' ) {
-
- #false laziness w/bill.cgi
- $error = $cust_main->bill( 'time' => $billtime );
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't bill customer for $line: $error";
- }
-
- $error = $cust_main->apply_payments_and_credits;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't bill customer for $line: $error";
- }
-
- $error = $cust_main->collect();
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't collect customer for $line: $error";
- }
-
- }
-
- $row++;
-
- if ( $job && time - $min_sec > $last ) { #progress bar
- $job->update_statustext( int(100 * $row / $count) );
- $last = time;
- }
-
- }
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;;
-
- return "Empty file!" unless $row;
-
- ''; #no error
-
-}
-
=item batch_charge
=cut
" AND action = 'cust_bill_send_agent' ".
" AND ( disabled IS NULL OR disabled != 'Y' ) ".
" AND peo_agentnum.optionname = 'agentnum' ".
- " AND agentnum IS NULL OR agentnum = $agentnum ".
+ " AND ( agentnum IS NULL OR agentnum = $agentnum ) ".
" ORDER BY
CASE WHEN peo_cust_bill_age.optionname != 'cust_bill_age'
THEN -1