summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2012-05-25 13:38:07 -0700
committerMark Wells <mark@freeside.biz>2012-05-25 13:38:18 -0700
commit01629c3c934f1f6fd2ab9de5f7638f671fd59791 (patch)
treee20740d482c8ce8f47731b3213c65e7910e267f2 /FS
parentf2c26594352302de80c2cd0cbba8b0e2abada6f7 (diff)
customer bill/ship location refactoring, #940
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/ClientAPI/MasonComponent.pm2
-rw-r--r--FS/FS/ClientAPI/MyAccount.pm79
-rw-r--r--FS/FS/ClientAPI/Signup.pm49
-rw-r--r--FS/FS/Schema.pm23
-rw-r--r--FS/FS/UI/Web/small_custview.pm54
-rw-r--r--FS/FS/cust_bill.pm8
-rw-r--r--FS/FS/cust_location.pm176
-rw-r--r--FS/FS/cust_main.pm493
-rw-r--r--FS/FS/cust_main/Billing.pm56
-rw-r--r--FS/FS/cust_main/Location.pm252
-rw-r--r--FS/FS/cust_main/Packages.pm6
-rw-r--r--FS/FS/cust_main/Search.pm151
-rw-r--r--FS/FS/cust_pkg.pm52
-rw-r--r--FS/FS/msg_template.pm5
14 files changed, 889 insertions, 517 deletions
diff --git a/FS/FS/ClientAPI/MasonComponent.pm b/FS/FS/ClientAPI/MasonComponent.pm
index 37cf7ef..534b48a 100644
--- a/FS/FS/ClientAPI/MasonComponent.pm
+++ b/FS/FS/ClientAPI/MasonComponent.pm
@@ -36,7 +36,7 @@ my %session_callbacks = (
my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
or return "unknown custnum $custnum";
my %args = @$argsref;
- $args{object} = $cust_main;
+ $args{object} = $cust_main->bill_location;
@$argsref = ( %args );
return ''; #no error
},
diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index e79fbfc..54799b8 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -46,18 +46,17 @@ use FS::msg_template;
$DEBUG = 0;
$me = '[FS::ClientAPI::MyAccount]';
-use vars qw( @cust_main_editable_fields );
+use vars qw( @cust_main_editable_fields @location_editable_fields );
@cust_main_editable_fields = qw(
- first last company address1 address2 city
- county state zip country
- daytime night fax mobile
- ship_first ship_last ship_company ship_address1 ship_address2 ship_city
- ship_state ship_zip ship_country
- ship_daytime ship_night ship_fax ship_mobile
+ first last daytime night fax mobile
locale
payby payinfo payname paystart_month paystart_year payissue payip
ss paytype paystate stateid stateid_state
);
+@location_editable_fields = qw(
+ address1 address2 city county state zip country
+);
+
BEGIN { #preload to reduce time customer_info takes
if ( $FS::TicketSystem::system ) {
@@ -442,7 +441,6 @@ sub customer_info {
);
$return{name} = $cust_main->first. ' '. $cust_main->get('last');
- $return{ship_name} = $cust_main->ship_first. ' '. $cust_main->get('ship_last');
$return{has_ship_address} = $cust_main->has_ship_address;
$return{status} = $cust_main->status;
@@ -452,6 +450,18 @@ sub customer_info {
$return{$_} = $cust_main->get($_);
}
+ for (@location_editable_fields) {
+ $return{$_} = $cust_main->bill_location->get($_);
+ $return{'ship_'.$_} = $cust_main->ship_location->get($_);
+ }
+ $return{has_ship_address} = $cust_main->has_ship_address;
+ # compatibility: some places in selfservice use this to determine
+ # if there's a ship address
+ if ( $return{has_ship_address} ) {
+ $return{ship_last} = $cust_main->last;
+ $return{ship_first} = $cust_main->first;
+ }
+
if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
$return{payinfo} = $cust_main->paymask;
@return{'month', 'year'} = $cust_main->paydate_monthyear;
@@ -465,7 +475,7 @@ sub customer_info {
if (scalar($conf->config('support_packages'))) {
my @support_services = ();
foreach ($cust_main->support_services) {
- my $seconds = $_->svc_x->seconds;
+ my $seconds = $_->svc_x->seconds || 0;
my $time_remaining = (($seconds < 0) ? '-' : '' ).
int(abs($seconds)/3600)."h".
sprintf("%02d",(abs($seconds)%3600)/60)."m";
@@ -541,7 +551,6 @@ sub customer_info_short {
);
$return{name} = $cust_main->first. ' '. $cust_main->get('last');
- $return{ship_name} = $cust_main->ship_first. ' '. $cust_main->get('ship_last');
$return{payby} = $cust_main->payby;
@@ -549,7 +558,12 @@ sub customer_info_short {
for (@cust_main_editable_fields) {
$return{$_} = $cust_main->get($_);
}
-
+ #maybe a little more expensive, but it should be cached by now
+ for (@location_editable_fields) {
+ $return{$_} = $cust_main->bill_location->get($_);
+ $return{'ship_'.$_} = $cust_main->ship_location->get($_);
+ }
+
if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
$return{payinfo} = $cust_main->paymask;
@return{'month', 'year'} = $cust_main->paydate_monthyear;
@@ -692,15 +706,32 @@ sub edit_info {
or return { 'error' => "unknown custnum $custnum" };
my $new = new FS::cust_main { $cust_main->hash };
- # Avoid accidentally changing the service address.
- if ( !$new->has_ship_address ) {
- $new->set( $_ => $new->get($_) )
- foreach $new->addr_fields;
- }
$new->set( $_ => $p->{$_} )
foreach grep { exists $p->{$_} } @cust_main_editable_fields;
+ if ( exists($p->{address1}) ) {
+ my $bill_location = FS::cust_location->new({
+ map { $_ => $p->{$_} } @location_editable_fields
+ });
+ # if this is unchanged from before, cust_main::replace will ignore it
+ $new->set('bill_location' => $bill_location);
+ }
+
+ if ( exists($p->{ship_address1}) ) {
+ my $ship_location = FS::cust_location->new({
+ map { $_ => $p->{"ship_$_"} } @location_editable_fields
+ });
+ if ( !grep { length($p->{"ship_$_"}) } @location_editable_fields ) {
+ # Selfservice unfortunately tries to indicate "same as billing
+ # address" by sending all fields empty. Did this ever work?
+ $ship_location = $cust_main->bill_location;
+ }
+ $new->set('ship_location' => $ship_location);
+ }
+ # but if it hasn't been passed in at all, leave ship_location alone--
+ # DON'T change it to match bill_location.
+
my $payby = '';
if (exists($p->{'payby'})) {
$p->{'payby'} =~ /^([A-Z]{4})$/
@@ -838,7 +869,8 @@ sub payment_info {
$return{payname} = $cust_main->payname
|| ( $cust_main->first. ' '. $cust_main->get('last') );
- $return{$_} = $cust_main->get($_) for qw(address1 address2 city state zip);
+ $return{$_} = $cust_main->bill_location->get($_)
+ for qw(address1 address2 city state zip);
$return{payby} = $cust_main->payby;
$return{stateid_state} = $cust_main->stateid_state;
@@ -1062,13 +1094,12 @@ sub do_process_payment {
foreach qw( payname paystart_month paystart_year payissue payip );
$new->set( 'payby' => $validate->{'auto'} ? 'CARD' : 'DCRD' );
- # Avoid accidentally changing the service address.
- if ( !$new->has_ship_address ) {
- $new->set( "ship_$_" => $new->get($_) )
- foreach $new->addr_fields;
- }
- $new->set( $_ => $validate->{$_} )
- foreach qw(address1 address2 city state country zip);
+ my $bill_location = FS::cust_location->new({
+ map { $_ => $validate->{$_} }
+ qw(address1 address2 city state country zip)
+ }); # county?
+ $new->set('bill_location' => $bill_location);
+ # but don't allow the service address to change this way.
} elsif ($payby eq 'CHEK' || $payby eq 'DCHK') {
$new->set( $_ => $validate->{$_} )
diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm
index f17752a..b7dcdbb 100644
--- a/FS/FS/ClientAPI/Signup.pm
+++ b/FS/FS/ClientAPI/Signup.pm
@@ -405,8 +405,8 @@ sub signup_info {
&& $agent->agent_cust_main ) {
my $cust_main = $agent->agent_cust_main;
- my $prefix = length($cust_main->ship_last) ? 'ship_' : '';
- $signup_info_cache_agent->{"ship_$_"} = $cust_main->get("$prefix$_")
+ my $location = $cust_main->ship_location;
+ $signup_info_cache_agent->{"ship_$_"} = $location->get($_)
foreach qw( address1 city county state zip country );
}
@@ -509,6 +509,13 @@ sub new_customer {
|| $conf->config('signup_server-default_agentnum');
}
+ my ($bill_hash, $ship_hash);
+ foreach my $f (FS::cust_main->location_fields) {
+ # avoid having to change this in front-end code
+ $bill_hash->{$f} = $packet->{"bill_$f"} || $packet->{$f};
+ $ship_hash->{$f} = $packet->{"ship_$f"};
+ }
+
#shares some stuff with htdocs/edit/process/cust_main.cgi... take any
# common that are still here and library them.
my $template_custnum = $conf->config('signup_server-prepaid-template-custnum');
@@ -517,6 +524,7 @@ sub new_customer {
my $template_cust = qsearchs('cust_main', { 'custnum' => $template_custnum } );
return { 'error' => 'Configuration error' } unless $template_cust;
+ #XXX Copy template customer's locations
$cust_main = new FS::cust_main ( {
'agentnum' => $agentnum,
'refnum' => $packet->{refnum}
@@ -556,41 +564,48 @@ sub new_customer {
|| $conf->config('signup_server-default_refnum'),
map { $_ => $packet->{$_} } qw(
-
- last first ss company address1 address2
- city county state zip country
+ last first ss company
daytime night fax stateid stateid_state
-
- ship_last ship_first ship_ss ship_company ship_address1 ship_address2
- ship_city ship_county ship_state ship_zip ship_country
- ship_daytime ship_night ship_fax
-
payby
payinfo paycvv paydate payname paystate paytype
paystart_month paystart_year payissue
payip
override_ban_warn
-
referral_custnum comments
- )
+ ),
} );
}
+ my $bill_location = FS::cust_location->new($bill_hash);
+ my $ship_location;
my $agent = qsearchs('agent', { 'agentnum' => $agentnum } );
if ( $conf->exists('agent-ship_address', $agentnum)
&& $agent->agent_custnum ) {
my $agent_cust_main = $agent->agent_cust_main;
my $prefix = length($agent_cust_main->ship_last) ? 'ship_' : '';
- $cust_main->set("ship_$_", $agent_cust_main->get("$prefix$_") )
- foreach qw( address1 city county state zip country );
-
- $cust_main->set("ship_$_", $cust_main->get($_))
- foreach qw( last first );
+ $ship_location = FS::cust_location->new({
+ $agent_cust_main->ship_location->location_hash
+ });
}
+ # we don't have an equivalent of the "same" checkbox in selfservice
+ # so is there a ship address, and if so, is it different from the billing
+ # address?
+ elsif ( length($ship_hash->{address1}) > 0 and
+ grep { $bill_hash->{$_} ne $ship_hash->{$_} } keys(%$ship_hash)
+ ) {
+
+ $ship_location = FS::cust_location->new( $ship_hash );
+
+ }
+ else {
+ $ship_location = $bill_location;
+ }
+ $cust_main->set('bill_location' => $bill_location);
+ $cust_main->set('ship_location' => $ship_location);
return { 'error' => "Illegal payment type" }
unless grep { $_ eq $packet->{'payby'} }
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 2968903..5476589 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -861,13 +861,13 @@ sub tables_hashref {
'signupdate',@date_type, '', '',
'dundate', @date_type, '', '',
'company', 'varchar', 'NULL', $char_d, '', '',
- 'address1', 'varchar', '', $char_d, '', '',
+ 'address1', 'varchar', 'NULL', $char_d, '', '',
'address2', 'varchar', 'NULL', $char_d, '', '',
- 'city', 'varchar', '', $char_d, '', '',
+ 'city', 'varchar', 'NULL', $char_d, '', '',
'county', 'varchar', 'NULL', $char_d, '', '',
'state', 'varchar', 'NULL', $char_d, '', '',
'zip', 'varchar', 'NULL', 10, '', '',
- 'country', 'char', '', 2, '', '',
+ 'country', 'char', 'NULL', 2, '', '',
'latitude', 'decimal', 'NULL', '10,7', '', '',
'longitude','decimal', 'NULL', '10,7', '', '',
'coord_auto', 'char', 'NULL', 1, '', '',
@@ -896,7 +896,7 @@ sub tables_hashref {
'payby', 'char', '', 4, '', '',
'payinfo', 'varchar', 'NULL', 512, '', '',
'paycvv', 'varchar', 'NULL', 512, '', '',
- 'paymask', 'varchar', 'NULL', $char_d, '', '',
+ 'paymask', 'varchar', 'NULL', $char_d, '', '',
#'paydate', @date_type, '', '',
'paydate', 'varchar', 'NULL', 10, '', '',
'paystart_month', 'int', 'NULL', '', '', '',
@@ -929,6 +929,8 @@ sub tables_hashref {
'locale', 'varchar', 'NULL', 16, '', '',
'calling_list_exempt', 'char', 'NULL', 1, '', '',
'invoice_noemail', 'char', 'NULL', 1, '', '',
+ 'bill_locationnum', 'int', 'NULL', '', '', '',
+ 'ship_locationnum', 'int', 'NULL', '', '', '',
],
'primary_key' => 'custnum',
'unique' => [ [ 'agentnum', 'agent_custid' ] ],
@@ -939,16 +941,6 @@ sub tables_hashref {
[ 'referral_custnum' ],
[ 'payby' ], [ 'paydate' ],
[ 'archived' ],
- #billing
- [ 'last' ], [ 'company' ],
- [ 'county' ], [ 'state' ], [ 'country' ],
- [ 'zip' ],
- [ 'daytime' ], [ 'night' ], [ 'fax' ], [ 'mobile' ],
- #shipping
- [ 'ship_last' ], [ 'ship_company' ],
- [ 'ship_county' ], [ 'ship_state' ], [ 'ship_country' ],
- [ 'ship_zip' ],
- [ 'ship_daytime' ], [ 'ship_night' ], [ 'ship_fax' ], [ 'ship_mobile' ]
],
},
@@ -1081,6 +1073,8 @@ sub tables_hashref {
'country', 'char', '', 2, '', '',
'geocode', 'varchar', 'NULL', 20, '', '',
'district', 'varchar', 'NULL', 20, '', '',
+ 'censustract', 'varchar', 'NULL', 20, '', '',
+ 'censusyear', 'char', 'NULL', 4, '', '',
'location_type', 'varchar', 'NULL', 20, '', '',
'location_number', 'varchar', 'NULL', 20, '', '',
'location_kind', 'char', 'NULL', 1, '', '',
@@ -1090,6 +1084,7 @@ sub tables_hashref {
'unique' => [],
'index' => [ [ 'prospectnum' ], [ 'custnum' ],
[ 'county' ], [ 'state' ], [ 'country' ], [ 'zip' ],
+ [ 'city' ], [ 'district' ]
],
},
diff --git a/FS/FS/UI/Web/small_custview.pm b/FS/FS/UI/Web/small_custview.pm
index 53a3b5e..2c42a6b 100644
--- a/FS/FS/UI/Web/small_custview.pm
+++ b/FS/FS/UI/Web/small_custview.pm
@@ -82,45 +82,23 @@ sub small_custview {
$html .= '</TD></TR></TABLE></TD>';
- if ( defined $cust_main->dbdef_table->column('ship_last') ) {
-
- my $pre = $cust_main->ship_last ? 'ship_' : '';
-
- $html .= '<TD VALIGN="top">'. ntable("#cccccc",2).
- '<TR><TD ALIGN="right" VALIGN="top">Service<BR>Address</TD><TD BGCOLOR="#ffffff">'.
- $cust_main->get("${pre}last"). ', '.
- $cust_main->get("${pre}first"). '<BR>';
- $html .= $cust_main->get("${pre}company"). '<BR>'
- if $cust_main->get("${pre}company");
- $html .= $cust_main->get("${pre}address1"). '<BR>';
- $html .= $cust_main->get("${pre}address2"). '<BR>'
- if $cust_main->get("${pre}address2");
- $html .= $cust_main->get("${pre}city"). ', '.
- $cust_main->get("${pre}state"). ' '.
- $cust_main->get("${pre}zip"). '<BR>';
- $html .= $cust_main->get("${pre}country"). '<BR>'
- if $cust_main->get("${pre}country")
- && $cust_main->get("${pre}country") ne $countrydefault;
-
- $html .= '</TD></TR><TR><TD></TD><TD BGCOLOR="#ffffff">';
-
- if ( $cust_main->get("${pre}daytime") && $cust_main->get("${pre}night") ) {
- use FS::Msgcat;
- $html .= ( FS::Msgcat::_gettext('daytime') || 'Day' ).
- ' '. $cust_main->get("${pre}daytime").
- '<BR>'. ( FS::Msgcat::_gettext('night') || 'Night' ).
- ' '. $cust_main->get("${pre}night");
- } elsif ( $cust_main->get("${pre}daytime")
- || $cust_main->get("${pre}night") ) {
- $html .= $cust_main->get("${pre}daytime")
- || $cust_main->get("${pre}night");
- }
- if ( $cust_main->get("${pre}fax") ) {
- $html .= '<BR>Fax '. $cust_main->get("${pre}fax");
- }
+ my $ship = $cust_main->ship_location;
+
+ $html .= '<TD VALIGN="top">'. ntable("#cccccc",2).
+ '<TR><TD ALIGN="right" VALIGN="top">Service<BR>Address</TD><TD BGCOLOR="#ffffff">';
+ $html .= join('<BR>',
+ grep $_,
+ $cust_main->contact,
+ $cust_main->company,
+ $ship->address1,
+ $ship->address2,
+ ($ship->city . ', ' . $ship->state . ' ' . $ship->zip),
+ ($ship->country eq $countrydefault ? '' : $ship->country ),
+ );
+
+ # ship phone numbers no longer exist...
- $html .= '</TD></TR></TABLE></TD>';
- }
+ $html .= '</TD></TR></TABLE></TD>';
$html .= '</TR></TABLE>';
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
index 1f4943a..d1cb3ba 100644
--- a/FS/FS/cust_bill.pm
+++ b/FS/FS/cust_bill.pm
@@ -2780,11 +2780,13 @@ sub print_generic {
$invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
my $countrydefault = $conf->config('countrydefault') || 'US';
- my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
- foreach ( qw( contact company address1 address2 city state zip country fax) ){
- my $method = $prefix.$_;
+ foreach ( qw( address1 address2 city state zip country fax) ){
+ my $method = 'ship_'.$_;
$invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
}
+ foreach ( qw( contact company ) ) { #compatibility
+ $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
+ }
$invoice_data{'ship_country'} = ''
if ( $invoice_data{'ship_country'} eq $countrydefault );
diff --git a/FS/FS/cust_location.pm b/FS/FS/cust_location.pm
index bcdb50c..1f07aa8 100644
--- a/FS/FS/cust_location.pm
+++ b/FS/FS/cust_location.pm
@@ -113,11 +113,16 @@ otherwise returns false.
sub insert {
my $self = shift;
+ my $conf = new FS::Conf;
+
+ if ( $self->censustract ) {
+ $self->set('censusyear' => $conf->config('census_year') || 2012);
+ }
+
my $error = $self->SUPER::insert(@_);
#false laziness with cust_main, will go away eventually
- my $conf = new FS::Conf;
- if ( !$error and $conf->config('tax_district_method') ) {
+ if ( !$import and !$error and $conf->config('tax_district_method') ) {
my $queue = new FS::queue {
'job' => 'FS::geocode_Mixin::process_district_update'
@@ -144,21 +149,14 @@ sub replace {
my $self = shift;
my $old = shift;
$old ||= $self->replace_old;
- my $error = $self->SUPER::replace($old);
-
- #false laziness with cust_main, will go away eventually
- my $conf = new FS::Conf;
- if ( !$error and $conf->config('tax_district_method')
- and $self->get('address1') ne $old->get('address1') ) {
-
- my $queue = new FS::queue {
- 'job' => 'FS::geocode_Mixin::process_district_update'
- };
- $error = $queue->insert( ref($self), $self->locationnum );
-
+ # the following fields are immutable
+ foreach (qw(address1 address2 city state zip country)) {
+ if ( $self->$_ ne $old->$_ ) {
+ return "can't change cust_location field $_";
+ }
}
- $error || '';
+ $self->SUPER::replace($old);
}
@@ -174,6 +172,7 @@ and replace methods.
#fields anyway...
sub check {
my $self = shift;
+ my $conf = new FS::Conf;
my $error =
$self->ut_numbern('locationnum')
@@ -185,7 +184,7 @@ sub check {
|| $self->ut_textn('county')
|| $self->ut_textn('state')
|| $self->ut_country('country')
- || $self->ut_zip('zip', $self->country)
+ || (!$import && $self->ut_zip('zip', $self->country))
|| $self->ut_coordn('latitude')
|| $self->ut_coordn('longitude')
|| $self->ut_enum('coord_auto', [ '', 'Y' ])
@@ -194,22 +193,36 @@ sub check {
|| $self->ut_enum('location_kind', [ '', 'R', 'B' ] )
|| $self->ut_alphan('geocode')
|| $self->ut_alphan('district')
+ || $self->ut_numbern('censusyear')
;
return $error if $error;
+ if ( $self->censustract ne '' ) {
+ $self->censustract =~ /^\s*(\d{9})\.?(\d{2})\s*$/
+ or return "Illegal census tract: ". $self->censustract;
+
+ $self->censustract("$1.$2");
+ }
+
+ if ( $conf->exists('cust_main-require_address2') and
+ !$self->ship_address2 =~ /\S/ ) {
+ return "Unit # is required";
+ }
$self->set_coord
unless $import || ($self->latitude && $self->longitude);
- return "No prospect or customer!" unless $self->prospectnum || $self->custnum;
+ # tricky...we have to allow for the customer to not be inserted yet
+ return "No prospect or customer!" unless $self->prospectnum
+ || $self->custnum
+ || $self->get('custnum_pending');
return "Prospect and customer!" if $self->prospectnum && $self->custnum;
- my $conf = new FS::Conf;
return 'Location kind is required'
if $self->prospectnum
&& $conf->exists('prospect_main-alt_address_format')
&& ! $self->location_kind;
- unless ( qsearch('cust_main_county', {
+ unless ( $import or qsearch('cust_main_county', {
'country' => $self->country,
'state' => '',
} ) ) {
@@ -266,19 +279,40 @@ location_kind.
=cut
-=item move_to HASHREF
+=item disable_if_unused
-Takes a hashref with one or more cust_location fields. Creates a duplicate
-of the existing location with all fields set to the values in the hashref.
-Moves all packages that use the existing location to the new one, then sets
-the "disabled" flag on the old location. Returns nothing on success, an
-error message on error.
+Sets the "disabled" flag on the location if it is no longer in use as a
+prospect location, package location, or a customer's billing or default
+service address.
+
+=cut
+
+sub disable_if_unused {
+
+ my $self = shift;
+ my $locationnum = $self->locationnum;
+ return '' if FS::cust_main->count('bill_locationnum = '.$locationnum)
+ or FS::cust_main->count('ship_locationnum = '.$locationnum)
+ or FS::contact->count( 'locationnum = '.$locationnum)
+ or FS::cust_pkg->count('cancel IS NULL AND
+ locationnum = '.$locationnum)
+ ;
+ $self->disabled('Y');
+ $self->replace;
+
+}
+
+=item move_to
+
+Takes a new L<FS::cust_location> object. Moves all packages that use the
+existing location to the new one, then sets the "disabled" flag on the old
+location. Returns nothing on success, an error message on error.
=cut
sub move_to {
my $old = shift;
- my $hashref = shift;
+ my $new = shift;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
@@ -292,16 +326,12 @@ sub move_to {
my $dbh = dbh;
my $error = '';
- my $new = FS::cust_location->new({
- $old->location_hash,
- 'custnum' => $old->custnum,
- 'prospectnum' => $old->prospectnum,
- %$hashref
- });
- $error = $new->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "Error creating location: $error";
+ if ( !$new->locationnum ) {
+ $error = $new->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error creating location: $error";
+ }
}
my @pkgs = qsearch('cust_pkg', {
@@ -319,15 +349,14 @@ sub move_to {
}
}
- $old->disabled('Y');
- $error = $old->replace;
+ $error = $old->disable_if_unused;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "Error disabling old location: $error";
}
$dbh->commit if $oldAutoCommit;
- return;
+ '';
}
=item alternize
@@ -421,14 +450,15 @@ sub location_label {
my $conf = new FS::Conf;
my $prefix = '';
my $format = $conf->config('cust_location-label_prefix') || '';
+ my $cust_or_prospect;
+ if ( $self->custnum ) {
+ $cust_or_prospect = FS::cust_main->by_key($self->custnum);
+ }
+ elsif ( $self->prospectnum ) {
+ $cust_or_prospect = FS::prospect_main->by_key($self->prospectnum);
+ }
+
if ( $format eq 'CoStAg' ) {
- my $cust_or_prospect;
- if ( $self->custnum ) {
- $cust_or_prospect = FS::cust_main->by_key($self->custnum);
- }
- elsif ( $self->prospectnum ) {
- $cust_or_prospect = FS::prospect_main->by_key($self->prospectnum);
- }
my $agent = $conf->config('cust_main-custnum-display_prefix',
$cust_or_prospect->agentnum)
|| $cust_or_prospect->agent->agent;
@@ -440,15 +470,65 @@ sub location_label {
sprintf('%05d', $self->locationnum)
) );
}
+ elsif ( $self->custnum and
+ $self->locationnum == $cust_or_prospect->ship_locationnum ) {
+ $prefix = 'Default service location';
+ }
$prefix .= ($opt{join_string} || ': ') if $prefix;
$prefix . $self->SUPER::location_label(%opt);
}
=back
-=head1 BUGS
+=head1 CLASS METHODS
+
+=item in_county_sql OPTIONS
+
+Returns an SQL expression to test membership in a cust_main_county
+geographic area. By default, this requires district, city, county,
+state, and country to match exactly. Pass "ornull => 1" to allow
+partial matches where some fields are NULL in the cust_main_county
+record but not in the location.
+
+Pass "param => 1" to receive a parameterized expression (rather than
+one that requires a join to cust_main_county) and a list of parameter
+names in order.
+
+=cut
-Not yet used for cust_main billing and shipping addresses.
+sub in_county_sql {
+ # replaces FS::cust_pkg::location_sql
+ my ($class, %opt) = @_;
+ my $ornull = $opt{ornull} ? ' OR ? IS NULL' : '';
+ my $x = $ornull ? 3 : 2;
+ my @fields = (('district') x 3,
+ ('city') x 3,
+ ('county') x $x,
+ ('state') x $x,
+ 'country');
+
+ my @where = (
+ "cust_location.district = ? OR ? = '' OR CAST(? AS text) IS NULL",
+ "cust_location.city = ? OR ? = '' OR CAST(? AS text) IS NULL",
+ "cust_location.county = ? OR (? = '' AND cust_location.county IS NULL) $ornull",
+ "cust_location.state = ? OR (? = '' AND cust_location.state IS NULL ) $ornull",
+ "cust_location.country = ?"
+ );
+ my $sql = join(' AND ', map "($_)\n", @where);
+ if ( $opt{param} ) {
+ return $sql, @fields;
+ }
+ else {
+ # do the substitution here
+ foreach (@fields) {
+ $sql =~ s/\?/cust_main_county.$_/;
+ $sql =~ s/cust_main_county.$_ = ''/cust_main_county.$_ IS NULL/;
+ }
+ return $sql;
+ }
+}
+
+=head1 BUGS
=head1 SEE ALSO
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 9766579..56338e5 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -6,6 +6,7 @@ use strict;
use base qw( FS::cust_main::Packages FS::cust_main::Status
FS::cust_main::Billing FS::cust_main::Billing_Realtime
FS::cust_main::Billing_Discount
+ FS::cust_main::Location
FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin
FS::geocode_Mixin
FS::o2m_Common
@@ -14,7 +15,7 @@ use base qw( FS::cust_main::Packages FS::cust_main::Status
use vars qw( $DEBUG $me $conf
@encrypted_fields
$import
- $ignore_expired_card $ignore_illegal_zip $ignore_banned_card
+ $ignore_expired_card $ignore_banned_card $ignore_illegal_zip
$skip_fuzzyfiles
@paytypes
);
@@ -80,7 +81,6 @@ $me = '[FS::cust_main]';
$import = 0;
$ignore_expired_card = 0;
-$ignore_illegal_zip = 0;
$ignore_banned_card = 0;
$skip_fuzzyfiles = 0;
@@ -178,28 +178,6 @@ Cocial security number (optional)
(optional)
-=item address1
-
-=item address2
-
-(optional)
-
-=item city
-
-=item county
-
-(optional, see L<FS::cust_main_county>)
-
-=item state
-
-(see L<FS::cust_main_county>)
-
-=item zip
-
-=item country
-
-(see L<FS::cust_main_county>)
-
=item daytime
phone (optional)
@@ -216,56 +194,6 @@ phone (optional)
phone (optional)
-=item ship_first
-
-Shipping first name
-
-=item ship_last
-
-Shipping last name
-
-=item ship_company
-
-(optional)
-
-=item ship_address1
-
-=item ship_address2
-
-(optional)
-
-=item ship_city
-
-=item ship_county
-
-(optional, see L<FS::cust_main_county>)
-
-=item ship_state
-
-(see L<FS::cust_main_county>)
-
-=item ship_zip
-
-=item ship_country
-
-(see L<FS::cust_main_county>)
-
-=item ship_daytime
-
-phone (optional)
-
-=item ship_night
-
-phone (optional)
-
-=item ship_fax
-
-phone (optional)
-
-=item ship_mobile
-
-phone (optional)
-
=item payby
Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
@@ -364,6 +292,12 @@ sub table { 'cust_main'; }
Adds this customer to the database. If there is an error, returns the error,
otherwise returns false.
+Usually the customer's location will not yet exist in the database, and
+the C<bill_location> and C<ship_location> pseudo-fields must be set to
+uninserted L<FS::cust_location> objects. These will be inserted and linked
+(in both directions) to the new customer record. If they're references
+to the same object, they will become the same location.
+
CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
are inserted atomicly, or the transaction is rolled back. Passing an empty
@@ -462,13 +396,44 @@ sub insert {
}
+ # insert locations
+ foreach my $l (qw(bill_location ship_location)) {
+ my $loc = delete $self->hashref->{$l};
+ # XXX if we're moving a prospect's locations, do that here
+
+ if ( !$loc->locationnum ) {
+ # warn the location that we're going to insert it with no custnum
+ $loc->set(custnum_pending => 1);
+ warn " inserting $l\n"
+ if $DEBUG > 1;
+ my $error = $loc->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ my $label = $l eq 'ship_location' ? 'service' : 'billing';
+ return "$error (in $label location)";
+ }
+ }
+ elsif ( $loc->custnum != $self->custnum or $loc->prospectnum > 0 ) {
+ # this shouldn't happen
+ $dbh->rollback if $oldAutoCommit;
+ return "$l belongs to customer ".$loc->custnum;
+ }
+ # else it already belongs to this customer
+ # (happens when ship_location is identical to bill_location)
+
+ $self->set($l.'num', $loc->locationnum);
+
+ if ( $self->get($l.'num') eq '' ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "$l not set";
+ }
+ }
+
warn " inserting $self\n"
if $DEBUG > 1;
$self->signupdate(time) unless $self->signupdate;
- $self->censusyear($conf->config('census_year')||'2012') if $self->censustract;
-
$self->auto_agent_custid()
if $conf->config('cust_main-auto_agent_custid') && ! $self->agent_custid;
@@ -479,6 +444,20 @@ sub insert {
return $error;
}
+ # now set cust_location.custnum
+ foreach my $l (qw(bill_location ship_location)) {
+ warn " setting $l.custnum\n"
+ if $DEBUG > 1;
+ my $loc = $self->$l;
+ $loc->set(custnum => $self->custnum);
+ $error ||= $loc->replace;
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error setting $l custnum: $error";
+ }
+ }
+
warn " setting invoicing list\n"
if $DEBUG > 1;
@@ -1318,7 +1297,7 @@ sub merge {
}
- my $name = $self->ship_name;
+ my $name = $self->ship_name; #?
my $locationnum = '';
foreach my $cust_pkg ( $self->all_pkgs ) {
@@ -1454,10 +1433,13 @@ sub merge {
=item replace [ OLD_RECORD ] [ INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
-
Replaces the OLD_RECORD with this one in the database. If there is an error,
returns the error, otherwise returns false.
+To change the customer's address, set the pseudo-fields C<bill_location> and
+C<ship_location>. The address will still only change if at least one of the
+address fields differs from the existing values.
+
INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
be set as the invoicing list (see L<"invoicing_list">). Errors return as
expected and rollback the entire transaction; it is not necessary to call
@@ -1494,41 +1476,19 @@ sub replace {
return "You are not permitted to create complimentary accounts.";
}
- if ( $old->get('geocode') && $old->get('geocode') eq $self->get('geocode')
- && $conf->exists('enable_taxproducts')
- )
- {
- my $pre = ($conf->exists('tax-ship_address') && $self->ship_zip)
- ? 'ship_' : '';
- $self->set('geocode', '')
- if $old->get($pre.'zip') ne $self->get($pre.'zip')
- && length($self->get($pre.'zip')) >= 10;
- }
-
- for my $pre ( grep $old->get($_.'coord_auto'), ( '', 'ship_' ) ) {
-
- $self->set($pre.'coord_auto', '') && next
- if $self->get($pre.'latitude') && $self->get($pre.'longitude')
- && ( $self->get($pre.'latitude') != $old->get($pre.'latitude')
- || $self->get($pre.'longitude') != $old->get($pre.'longitude')
- );
-
- $self->set_coord($pre)
- if $old->get($pre.'address1') ne $self->get($pre.'address1')
- || $old->get($pre.'city') ne $self->get($pre.'city')
- || $old->get($pre.'state') ne $self->get($pre.'state')
- || $old->get($pre.'country') ne $self->get($pre.'country');
-
- }
+ # should be unnecessary--geocode will default to null on new locations
+ #if ( $old->get('geocode') && $old->get('geocode') eq $self->get('geocode')
+ # && $conf->exists('enable_taxproducts')
+ # )
+ #{
+ # my $pre = ($conf->exists('tax-ship_address') && $self->ship_zip)
+ # ? 'ship_' : '';
+ # $self->set('geocode', '')
+ # if $old->get($pre.'zip') ne $self->get($pre.'zip')
+ # && length($self->get($pre.'zip')) >= 10;
+ #}
- unless ( $import ) {
- $self->set_coord
- if ! $self->coord_auto && ! $self->latitude && ! $self->longitude;
-
- $self->set_coord('ship_')
- if $self->has_ship_address && ! $self->ship_coord_auto
- && ! $self->ship_latitude && ! $self->ship_longitude;
- }
+ # set_coord/coord_auto stuff is now handled by cust_location
local($ignore_expired_card) = 1
if $old->payby =~ /^(CARD|DCRD)$/
@@ -1540,11 +1500,6 @@ sub replace {
|| $old->payby =~ /^(CHEK|DCHK)$/ && $self->payby =~ /^(CHEK|DCHK)$/ )
&& ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
- if ( $self->censustract ne '' and $self->censustract ne $old->censustract ) {
- # update censusyear whenever tract code changes
- $self->censusyear($conf->config('census_year')||'2012');
- }
-
return "Invoicing locale is required"
if $old->locale
&& ! $self->locale
@@ -1561,6 +1516,47 @@ sub replace {
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ for my $l (qw(bill_location ship_location)) {
+ my $old_loc = $old->$l;
+ my $new_loc = $self->$l;
+
+ if ( !$new_loc->locationnum ) {
+ # changing location
+ # If the new location is all empty fields, or if it's identical to
+ # the old location in all fields, don't replace.
+ my @nonempty = grep { $new_loc->$_ } $self->location_fields;
+ next if !@nonempty;
+ my @unlike = grep { $new_loc->$_ ne $old_loc->$_ } $self->location_fields;
+
+ if ( @unlike or $old_loc->disabled ) {
+ warn " changed $l fields: ".join(',',@unlike)."\n"
+ if $DEBUG;
+ $new_loc->set(custnum => $self->custnum);
+
+ # insert it--the old location will be disabled later
+ my $error = $new_loc->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ } else {
+ # no fields have changed and $old_loc isn't disabled, so don't change it
+ next;
+ }
+
+ }
+ elsif ( $new_loc->custnum ne $self->custnum or $new_loc->prospectnum ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "$l belongs to customer ".$new_loc->custnum;
+ }
+ # else the new location belongs to this customer so we're good
+
+ # set the foo_locationnum now that we have one.
+ $self->set($l.'num', $new_loc->locationnum);
+
+ } #for $l
+
my $error = $self->SUPER::replace($old);
if ( $error ) {
@@ -1568,6 +1564,27 @@ sub replace {
return $error;
}
+ # now move packages to the new service location
+ $self->set('ship_location', ''); #flush cache
+ if ( $old->ship_locationnum and # should only be null during upgrade...
+ $old->ship_locationnum != $self->ship_locationnum ) {
+ $error = $old->ship_location->move_to($self->ship_location);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+ # don't move packages based on the billing location, but
+ # disable it if it's no longer in use
+ if ( $old->bill_locationnum and
+ $old->bill_locationnum != $self->bill_locationnum ) {
+ $error = $old->bill_location->disable_if_unused;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
if ( @param && ref($param[0]) eq 'ARRAY' ) { # INVOICING_LIST_ARYREF
my $invoicing_list = shift @param;
$error = $self->check_invoicing_list( $invoicing_list );
@@ -1669,24 +1686,7 @@ sub replace {
}
}
- # FS::geocode_Mixin::after_replace ?
- # though this will go away anyway once we move customer bill/service
- # locations into cust_location
- # We can trigger this on any address change--just have to make sure
- # not to trigger it on itself.
- if ( $conf->config('tax_district_method') and !$import
- and ( $self->get('ship_address1') ne $old->get('ship_address1')
- or $self->get('address1') ne $old->get('address1') ) ) {
- my $queue = new FS::queue {
- 'job' => 'FS::geocode_Mixin::process_district_update',
- 'custnum' => $self->custnum,
- };
- my $error = $queue->insert( ref($self), $self->custnum );
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "queueing tax district update: $error";
- }
- }
+ # tax district update in cust_location
# cust_main exports!
@@ -1731,16 +1731,14 @@ sub queue_fuzzyfiles_update {
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- my $queue = new FS::queue { 'job' => 'FS::cust_main::Search::append_fuzzyfiles' };
- my $error = $queue->insert( map $self->getfield($_), @FS::cust_main::Search::fuzzyfields );
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "queueing job (transaction rolled back): $error";
- }
-
- if ( $self->ship_last ) {
- $queue = new FS::queue { 'job' => 'FS::cust_main::Search::append_fuzzyfiles' };
- $error = $queue->insert( map $self->getfield("ship_$_"), @FS::cust_main::Search::fuzzyfields );
+ my @locations = $self->bill_location;
+ push @locations, $self->ship_location if $self->has_ship_address;
+ foreach my $location (@locations) {
+ my $queue = new FS::queue {
+ 'job' => 'FS::cust_main::Search::append_fuzzyfiles'
+ };
+ my @args = map $location->get($_), @FS::cust_main::Search::fuzzyfields;
+ my $error = $queue->insert( @args );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "queueing job (transaction rolled back): $error";
@@ -1771,6 +1769,8 @@ sub check {
|| $self->ut_number('agentnum')
|| $self->ut_textn('agent_custid')
|| $self->ut_number('refnum')
+ || $self->ut_foreign_key('bill_locationnum', 'cust_location','locationnum')
+ || $self->ut_foreign_key('ship_locationnum', 'cust_location','locationnum')
|| $self->ut_foreign_keyn('classnum', 'cust_class', 'classnum')
|| $self->ut_textn('custbatch')
|| $self->ut_name('last')
@@ -1778,16 +1778,6 @@ sub check {
|| $self->ut_snumbern('birthdate')
|| $self->ut_snumbern('signupdate')
|| $self->ut_textn('company')
- || $self->ut_text('address1')
- || $self->ut_textn('address2')
- || $self->ut_text('city')
- || $self->ut_textn('county')
- || $self->ut_textn('state')
- || $self->ut_country('country')
- || $self->ut_coordn('latitude')
- || $self->ut_coordn('longitude')
- || $self->ut_enum('coord_auto', [ '', 'Y' ])
- || $self->ut_numbern('censusyear')
|| $self->ut_anything('comments')
|| $self->ut_numbern('referral_custnum')
|| $self->ut_textn('stateid')
@@ -1804,9 +1794,6 @@ sub check {
|| $self->ut_enum('locale', [ '', FS::Locales->locales ])
;
- $self->set_coord
- unless $import || ($self->latitude && $self->longitude);
-
#barf. need message catalogs. i18n. etc.
$error .= "Please select an advertising source."
if $error =~ /^Illegal or empty \(numeric\) refnum: /;
@@ -1822,13 +1809,6 @@ sub check {
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 {
@@ -1839,23 +1819,7 @@ sub check {
$self->ss("$1-$2-$3");
}
-
-# bad idea to disable, causes billing to fail because of no tax rates later
-# except we don't fail any more
- unless ( $import ) {
- unless ( qsearch('cust_main_county', {
- 'country' => $self->country,
- 'state' => '',
- } ) ) {
- return "Unknown state/county/country: ".
- $self->state. "/". $self->county. "/". $self->country
- unless qsearch('cust_main_county',{
- 'state' => $self->state,
- 'county' => $self->county,
- 'country' => $self->country,
- } );
- }
- }
+ # cust_main_county verification now handled by cust_location check
$error =
$self->ut_phonen('daytime', $self->country)
@@ -1865,12 +1829,8 @@ sub check {
;
return $error if $error;
- unless ( $ignore_illegal_zip ) {
- $error = $self->ut_zip('zip', $self->country);
- return $error if $error;
- }
-
if ( $conf->exists('cust_main-require_phone', $self->agentnum)
+ && ! $import
&& ! length($self->daytime) && ! length($self->night) && ! length($self->mobile)
) {
@@ -1889,71 +1849,7 @@ sub check {
}
- if ( $self->has_ship_address
- && scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
- $self->addr_fields )
- )
- {
- my $error =
- $self->ut_name('ship_last')
- || $self->ut_name('ship_first')
- || $self->ut_textn('ship_company')
- || $self->ut_text('ship_address1')
- || $self->ut_textn('ship_address2')
- || $self->ut_text('ship_city')
- || $self->ut_textn('ship_county')
- || $self->ut_textn('ship_state')
- || $self->ut_country('ship_country')
- || $self->ut_coordn('ship_latitude')
- || $self->ut_coordn('ship_longitude')
- || $self->ut_enum('ship_coord_auto', [ '', 'Y' ] )
- ;
- return $error if $error;
-
- $self->set_coord('ship_')
- unless $import || ($self->ship_latitude && $self->ship_longitude);
-
- #false laziness with above
- unless ( qsearchs('cust_main_county', {
- 'country' => $self->ship_country,
- 'state' => '',
- } ) ) {
- return "Unknown ship_state/ship_county/ship_country: ".
- $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
- unless qsearch('cust_main_county',{
- 'state' => $self->ship_state,
- 'county' => $self->ship_county,
- 'country' => $self->ship_country,
- } );
- }
- #eofalse
-
- $error =
- $self->ut_phonen('ship_daytime', $self->ship_country)
- || $self->ut_phonen('ship_night', $self->ship_country)
- || $self->ut_phonen('ship_fax', $self->ship_country)
- || $self->ut_phonen('ship_mobile', $self->ship_country)
- ;
- return $error if $error;
-
- unless ( $ignore_illegal_zip ) {
- $error = $self->ut_zip('ship_zip', $self->ship_country);
- return $error if $error;
- }
- return "Unit # is required."
- if $self->ship_address2 =~ /^\s*$/
- && $conf->exists('cust_main-require_address2');
-
- } else { # ship_ info eq billing info, so don't store dup info in database
-
- $self->setfield("ship_$_", '')
- foreach $self->addr_fields;
-
- return "Unit # is required."
- if $self->address2 =~ /^\s*$/
- && $conf->exists('cust_main-require_address2');
-
- }
+ #ship_ fields are gone
#$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
# or return "Illegal payby: ". $self->payby;
@@ -1979,7 +1875,9 @@ sub check {
# check the credit card.
my $check_payinfo = ! $self->is_encrypted($self->payinfo);
- if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
+ # Need some kind of global flag to accept invalid cards, for testing
+ # on scrubbed data.
+ if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
my $payinfo = $self->payinfo;
$payinfo =~ s/\D//g;
@@ -2201,7 +2099,7 @@ Returns true if this customer record has a separate shipping address.
sub has_ship_address {
my $self = shift;
- scalar( grep { $self->getfield("ship_$_") ne '' } $self->addr_fields );
+ $self->bill_locationnum != $self->ship_locationnum;
}
=item location_hash
@@ -2212,6 +2110,11 @@ shipping address is used if present.
=cut
+sub location_hash {
+ my $self = shift;
+ $self->ship_location->location_hash;
+}
+
=item cust_location
Returns all locations (see L<FS::cust_location>) for this customer.
@@ -2617,6 +2520,8 @@ sub batch_card {
$options{$_} = '' unless exists($options{$_});
}
+ my $loc = $self->bill_location;
+
my $cust_pay_batch = new FS::cust_pay_batch ( {
'batchnum' => $pay_batch->batchnum,
'invnum' => $invnum || 0, # is there a better value?
@@ -2626,16 +2531,16 @@ sub batch_card {
'custnum' => $self->custnum,
'last' => $self->getfield('last'),
'first' => $self->getfield('first'),
- 'address1' => $options{address1} || $self->address1,
- 'address2' => $options{address2} || $self->address2,
- 'city' => $options{city} || $self->city,
- 'state' => $options{state} || $self->state,
- 'zip' => $options{zip} || $self->zip,
- 'country' => $options{country} || $self->country,
- 'payby' => $options{payby} || $self->payby,
- 'payinfo' => $options{payinfo} || $self->payinfo,
- 'exp' => $options{paydate} || $self->paydate,
- 'payname' => $options{payname} || $self->payname,
+ 'address1' => $options{address1} || $loc->address1,
+ 'address2' => $options{address2} || $loc->address2,
+ 'city' => $options{city} || $loc->city,
+ 'state' => $options{state} || $loc->state,
+ 'zip' => $options{zip} || $loc->zip,
+ 'country' => $options{country} || $loc->country,
+ 'payby' => $options{payby} || $loc->payby,
+ 'payinfo' => $options{payinfo} || $loc->payinfo,
+ 'exp' => $options{paydate} || $loc->paydate,
+ 'payname' => $options{payname} || $loc->payname,
'amount' => $amount, # consolidating
} );
@@ -3027,7 +2932,8 @@ sub payment_info {
$return{payname} = $self->payname
|| ( $self->first. ' '. $self->get('last') );
- $return{$_} = $self->get($_) for qw(address1 address2 city state zip);
+ $return{$_} = $self->bill_location->$_
+ for qw(address1 address2 city state zip);
$return{payby} = $self->payby;
$return{stateid_state} = $self->stateid_state;
@@ -4037,6 +3943,27 @@ sub name {
$name;
}
+=item service_contact
+
+Returns the L<FS::contact> object for this customer that has the 'Service'
+contact class, or undef if there is no such contact. Deprecated; don't use
+this in new code.
+
+=cut
+
+sub service_contact {
+ my $self = shift;
+ if ( !exists($self->{service_contact}) ) {
+ my $classnum = $self->scalar_sql(
+ 'SELECT classnum FROM contact_class WHERE classname = \'Service\''
+ ) || 0; #if it's zero, qsearchs will return nothing
+ $self->{service_contact} = qsearchs('contact', {
+ 'classnum' => $classnum, 'custnum' => $self->custnum
+ }) || undef;
+ }
+ $self->{service_contact};
+}
+
=item ship_name
Returns a name string for this (service/shipping) contact, either
@@ -4046,13 +3973,10 @@ Returns a name string for this (service/shipping) contact, either
sub ship_name {
my $self = shift;
- if ( $self->get('ship_last') ) {
- my $name = $self->ship_contact;
- $name = $self->ship_company. " ($name)" if $self->ship_company;
- $name;
- } else {
- $self->name;
- }
+
+ my $name = $self->ship_contact;
+ $name = $self->company. " ($name)" if $self->company;
+ $name;
}
=item name_short
@@ -4075,13 +3999,9 @@ or "First Last".
sub ship_name_short {
my $self = shift;
- if ( $self->get('ship_last') ) {
- $self->ship_company !~ /^\s*$/
- ? $self->ship_company
- : $self->ship_contact_firstlast;
- } else {
- $self->name_company_or_firstlast;
- }
+ $self->service_contact
+ ? $self->ship_contact_firstlast
+ : $self->name_company_or_firstlast;
}
=item contact
@@ -4103,9 +4023,8 @@ Returns this customer's full (shipping) contact name only, "Last, First"
sub ship_contact {
my $self = shift;
- $self->get('ship_last')
- ? $self->get('ship_last'). ', '. $self->ship_first
- : $self->contact;
+ my $contact = $self->service_contact || $self;
+ $contact->get('last') . ', ' . $contact->get('first');
}
=item contact_firstlast
@@ -4127,9 +4046,8 @@ Returns this customer's full (shipping) contact name only, "First Last".
sub ship_contact_firstlast {
my $self = shift;
- $self->get('ship_last')
- ? $self->first. ' '. $self->get('ship_last')
- : $self->contact_firstlast;
+ my $contact = $self->service_contact || $self;
+ $contact->get('first') . ' '. $contact->get('last');
}
=item country_full
@@ -5113,6 +5031,8 @@ sub process_censustract_update {
# upgrade journal again? this is also an ancient problem
# - otaker upgrade? journal and call it good? (double check to make sure
# we're not still setting otaker here)
+#
+#only going to get worse with new location stuff...
sub _upgrade_data { #class method
my ($class, %opts) = @_;
@@ -5141,12 +5061,13 @@ sub _upgrade_data { #class method
}
local($ignore_expired_card) = 1;
- local($ignore_illegal_zip) = 1;
local($ignore_banned_card) = 1;
local($skip_fuzzyfiles) = 1;
local($import) = 1; #prevent automatic geocoding (need its own variable?)
$class->_upgrade_otaker(%opts);
+ FS::cust_main::Location->_upgrade_data(%opts);
+
}
=back
diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm
index ca8d996..339fa44 100644
--- a/FS/FS/cust_main/Billing.pm
+++ b/FS/FS/cust_main/Billing.pm
@@ -721,6 +721,11 @@ jurisdictions (i.e. Texas) have tax exemptions which are date sensitive.
sub calculate_taxes {
my ($self, $cust_bill_pkg, $taxlisthash, $invoice_time) = @_;
+ # $taxlisthash is a hashref
+ # keys are identifiers, values are arrayrefs
+ # each arrayref starts with a tax object (cust_main_county or tax_rate)
+ # then any cust_bill_pkg objects the tax applies to
+
local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
warn "$me calculate_taxes\n"
@@ -746,9 +751,15 @@ sub calculate_taxes {
my %tax_rate_location = ();
foreach my $tax ( keys %$taxlisthash ) {
+ # $tax is a tax identifier
my $tax_object = shift @{ $taxlisthash->{$tax} };
+ # $tax_object is a cust_main_county or tax_rate
+ # (with pkgnum and locationnum set)
+ # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg objects
warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
warn " ". join('/', @{ $taxlisthash->{$tax} } ). "\n" if $DEBUG > 2;
+ # taxline calculates the tax on all cust_bill_pkgs in the
+ # first (arrayref) argument
my $hashref_or_error =
$tax_object->taxline( $taxlisthash->{$tax},
'custnum' => $self->custnum,
@@ -767,8 +778,10 @@ sub calculate_taxes {
$tax{ $tax } += $amount;
+ # link records between cust_main_county/tax_rate and cust_location
$tax_location{ $tax } ||= [];
- if ( $tax_object->get('pkgnum') || $tax_object->get('locationnum') ) {
+ $tax_rate_location{ $tax } ||= [];
+ if ( ref($tax_object) eq 'FS::cust_main_county' ) {
push @{ $tax_location{ $tax } },
{
'taxnum' => $tax_object->taxnum,
@@ -778,9 +791,7 @@ sub calculate_taxes {
'amount' => sprintf('%.2f', $amount ),
};
}
-
- $tax_rate_location{ $tax } ||= [];
- if ( ref($tax_object) eq 'FS::tax_rate' ) {
+ elsif ( ref($tax_object) eq 'FS::tax_rate' ) {
my $taxratelocationnum =
$tax_object->tax_rate_location->taxratelocationnum;
push @{ $tax_rate_location{ $tax } },
@@ -1206,21 +1217,12 @@ sub _handle_taxes {
} else {
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;
- %taxhash = map { $_ => $cust_location->$_() } @loc_keys;
- } else {
- my $prefix =
- ( $conf->exists('tax-ship_address') && length($self->ship_last) )
- ? 'ship_'
- : '';
- %taxhash = map { $_ => $self->get("$prefix$_") } @loc_keys;
- }
+ my $location = $cust_pkg->tax_location;
+ my %taxhash = map { $_ => $location->$_ } @loc_keys;
$taxhash{'taxclass'} = $part_pkg->taxclass;
- my @taxes = ();
+ my @taxes = (); # entries are cust_main_county objects
my %taxhash_elim = %taxhash;
my @elim = qw( district city county state );
do {
@@ -1243,11 +1245,13 @@ sub _handle_taxes {
@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 );
- $_->set('locationnum', $cust_pkg->locationnum );
- }
+ # all packages now have a locationnum and should get a
+ # cust_bill_pkg_tax_location record. The tax_locationnum
+ # may be the package's locationnum, or the customer's bill
+ # or service location.
+ foreach (@taxes) {
+ $_->set('pkgnum', $cust_pkg->pkgnum);
+ $_->set('locationnum', $cust_pkg->tax_locationnum);
}
$taxes{''} = [ @taxes ];
@@ -1274,17 +1278,27 @@ sub _handle_taxes {
my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
foreach my $key (keys %tax_cust_bill_pkg) {
+ # $key is "setup", "recur", or a usage class name. ('' is a usage class.)
+ # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of
+ # the line item.
+ # $taxes{$key} is an arrayref of cust_main_county or tax_rate objects that
+ # apply to $key-class charges.
my @taxes = @{ $taxes{$key} || [] };
my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
my %localtaxlisthash = ();
foreach my $tax ( @taxes ) {
+ # this is the tax identifier, not the taxname
my $taxname = ref( $tax ). ' '. $tax->taxnum;
# $taxname .= ' pkgnum'. $cust_pkg->pkgnum.
# ' locationnum'. $cust_pkg->locationnum
# if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum;
+ # $taxlisthash: keys are "setup", "recur", and usage classes
+ # values are arrayrefs, first the tax object (cust_main_county
+ # or tax_rate) and then any cust_bill_pkg objects that the
+ # tax applies to
$taxlisthash->{ $taxname } ||= [ $tax ];
push @{ $taxlisthash->{ $taxname } }, $tax_cust_bill_pkg;
diff --git a/FS/FS/cust_main/Location.pm b/FS/FS/cust_main/Location.pm
new file mode 100644
index 0000000..d1d6d67
--- /dev/null
+++ b/FS/FS/cust_main/Location.pm
@@ -0,0 +1,252 @@
+package FS::cust_main::Location;
+
+use strict;
+use vars qw( $DEBUG $me @location_fields );
+use FS::Record qw(qsearch qsearchs);
+use FS::UID qw(dbh);
+use FS::cust_location;
+
+use Carp qw(carp);
+
+$DEBUG = 1;
+$me = '[FS::cust_main::Location]';
+
+my $init = 0;
+BEGIN {
+ # set up accessors for location fields
+ if (!$init) {
+ no strict 'refs';
+ @location_fields =
+ qw( address1 address2 city county state zip country district
+ latitude longitude coord_auto censustract censusyear geocode );
+
+ foreach my $f (@location_fields) {
+ *{"FS::cust_main::Location::$f"} = sub {
+ carp "WARNING: tried to set cust_main.$f with accessor" if (@_ > 1);
+ shift->bill_location->$f
+ };
+ *{"FS::cust_main::Location::ship_$f"} = sub {
+ carp "WARNING: tried to set cust_main.ship_$f with accessor" if (@_ > 1);
+ shift->ship_location->$f
+ };
+ }
+ $init++;
+ }
+}
+
+#debugging shim--probably a performance hit, so remove this at some point
+sub get {
+ my $self = shift;
+ my $field = shift;
+ if ( $DEBUG and grep (/^(ship_)?($field)$/, @location_fields) ) {
+ carp "WARNING: tried to get() location field $field";
+ $self->$field;
+ }
+ $self->FS::Record::get($field);
+}
+
+=head1 NAME
+
+FS::cust_main::Location - Location-related methods for cust_main
+
+=head1 DESCRIPTION
+
+These methods are available on FS::cust_main objects;
+
+=head1 METHODS
+
+=over 4
+
+=item bill_location
+
+Returns an L<FS::cust_location> object for the customer's billing address.
+
+=cut
+
+sub bill_location {
+ my $self = shift;
+ $self->hashref->{bill_location}
+ ||= FS::cust_location->by_key($self->bill_locationnum);
+}
+
+=item ship_location
+
+Returns an L<FS::cust_location> object for the customer's service address.
+
+=cut
+
+sub ship_location {
+ my $self = shift;
+ $self->hashref->{ship_location}
+ ||= FS::cust_location->by_key($self->ship_locationnum);
+}
+
+=item location TYPE
+
+An alternative way of saying "bill_location or ship_location, depending on
+if TYPE is 'bill' or 'ship'".
+
+=cut
+
+sub location {
+ my $self = shift;
+ return $self->bill_location if $_[0] eq 'bill';
+ return $self->ship_location if $_[0] eq 'ship';
+ die "bad location type '$_[0]'";
+}
+
+=back
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item location_fields
+
+Returns a list of fields found in the location objects. All of these fields
+can be read (but not written) by calling them as methods on the
+L<FS::cust_main> object (prefixed with 'ship_' for the service address
+fields).
+
+=cut
+
+sub location_fields { @location_fields }
+
+sub _upgrade_data {
+ my $class = shift;
+ eval "use FS::contact;
+ use FS::contact_class;
+ use FS::contact_phone;
+ use FS::phone_type";
+
+ local $FS::cust_location::import = 1;
+ local $DEBUG = 0;
+ my $error;
+
+ # Step 0: set up contact classes and phone types
+ my $service_contact_class =
+ qsearchs('contact_class', { classname => 'Service'})
+ || new FS::contact_class { classname => 'Service'};
+
+ if ( !$service_contact_class->classnum ) {
+ $error = $service_contact_class->insert;
+ die "error creating contact class for Service: $error" if $error;
+ }
+ my %phone_type = ( # fudge slightly
+ daytime => 'Work',
+ night => 'Home',
+ mobile => 'Mobile',
+ fax => 'Fax'
+ );
+ my $w = 10;
+ foreach (keys %phone_type) {
+ $phone_type{$_} = qsearchs('phone_type', { typename => $phone_type{$_}})
+ || new FS::phone_type { typename => $phone_type{$_},
+ weight => $w };
+ # just in case someone still doesn't have these
+ if ( !$phone_type{$_}->phonetypenum ) {
+ $error = $phone_type{$_}->insert;
+ die "error creating phone type '$_': $error";
+ }
+ }
+
+ foreach my $cust_main (qsearch('cust_main', { bill_locationnum => '' })) {
+ # Step 1: extract billing and service addresses into cust_location
+ my $custnum = $cust_main->custnum;
+ my $bill_location = FS::cust_location->new(
+ {
+ custnum => $custnum,
+ map { $_ => $cust_main->get($_) } location_fields()
+ }
+ );
+ $error = $bill_location->insert;
+ die "error migrating billing address for customer $custnum: $error"
+ if $error;
+
+ $cust_main->set(bill_locationnum => $bill_location->locationnum);
+
+ if ( $cust_main->get('ship_address1') ) {
+ my $ship_location = FS::cust_location->new(
+ {
+ custnum => $custnum,
+ map { $_ => $cust_main->get("ship_$_") } location_fields()
+ }
+ );
+ $error = $ship_location->insert;
+ die "error migrating service address for customer $custnum: $error"
+ if $error;
+
+ $cust_main->set(ship_locationnum => $ship_location->locationnum);
+
+ # Step 2: Extract shipping address contact fields into contact
+ my %unlike = map { $_ => 1 }
+ grep { $cust_main->get($_) ne $cust_main->get("ship_$_") }
+ qw( last first company daytime night fax mobile );
+
+ if ( %unlike ) {
+ # then there IS a service contact
+ my $contact = FS::contact->new({
+ 'custnum' => $custnum,
+ 'classnum' => $service_contact_class->classnum,
+ 'locationnum' => $ship_location->locationnum,
+ 'last' => $cust_main->get('ship_last'),
+ 'first' => $cust_main->get('ship_first'),
+ });
+ if ( $unlike{'company'} ) {
+ # there's no contact.company field, but keep a record of it
+ $contact->set(comment => 'Company: '.$cust_main->get('ship_company'));
+ }
+ $error = $contact->insert;
+ die "error migrating service contact for customer $custnum: $error"
+ if $error;
+
+ foreach ( grep { $unlike{$_} } qw( daytime night fax mobile ) ) {
+ my $phone = $cust_main->get("ship_$_");
+ next if !$phone;
+ my $contact_phone = FS::contact_phone->new({
+ 'contactnum' => $contact->contactnum,
+ 'phonetypenum' => $phone_type{$_}->phonetypenum,
+ FS::contact::_parse_phonestring( $phone )
+ });
+ $error = $contact_phone->insert;
+ # die "whose responsible this"
+ die "error migrating service contact phone for customer $custnum: $error"
+ if $error;
+ $cust_main->set("ship_$_" => '');
+ }
+
+ $cust_main->set("ship_$_" => '') foreach qw(last first company);
+ } #if %unlike
+ } #if ship_address1
+ else {
+ $cust_main->set(ship_locationnum => $bill_location->locationnum);
+ }
+
+ # Step 3: Wipe the migrated fields and update the cust_main
+
+ $cust_main->set("ship_$_" => '') foreach location_fields();
+ $cust_main->set($_ => '') foreach location_fields();
+
+ $error = $cust_main->replace;
+ die "error migrating addresses for customer $custnum: $error"
+ if $error;
+
+ # Step 4: set packages at the "default service location" to ship_location
+ foreach my $cust_pkg (
+ qsearch('cust_pkg', { custnum => $custnum, locationnum => '' })
+ ) {
+ # not a location change
+ $cust_pkg->set('locationnum', $cust_main->ship_locationnum);
+ $error = $cust_pkg->replace;
+ die "error migrating package ".$cust_pkg->pkgnum.": $error"
+ if $error;
+ }
+
+ } #foreach $cust_main
+}
+
+=back
+
+=cut
+
+1;
diff --git a/FS/FS/cust_main/Packages.pm b/FS/FS/cust_main/Packages.pm
index 06331d3..887ac49 100644
--- a/FS/FS/cust_main/Packages.pm
+++ b/FS/FS/cust_main/Packages.pm
@@ -40,7 +40,8 @@ FS::cust_pkg object
=item cust_location
-Optional FS::cust_location object
+Optional FS::cust_location object. If not specified, the customer's
+ship_location will be used.
=item svcs
@@ -105,6 +106,9 @@ sub order_pkg {
}
$cust_pkg->locationnum($opt->{'cust_location'}->locationnum);
}
+ else {
+ $cust_pkg->locationnum($self->ship_locationnum);
+ }
$cust_pkg->custnum( $self->custnum );
diff --git a/FS/FS/cust_main/Search.pm b/FS/FS/cust_main/Search.pm
index b663c20..ca4d167 100644
--- a/FS/FS/cust_main/Search.pm
+++ b/FS/FS/cust_main/Search.pm
@@ -85,8 +85,7 @@ sub smart_search {
'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
' ( '.
join(' OR ', map "$_ = '$phonen'",
- qw( daytime night fax
- ship_daytime ship_night ship_fax )
+ qw( daytime night fax )
).
' ) '.
" AND $agentnums_sql", #agent virtualization
@@ -101,8 +100,7 @@ sub smart_search {
'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
' ( '.
join(' OR ', map "$_ LIKE '$phonen\%'",
- qw( daytime night
- ship_daytime ship_night )
+ qw( daytime night )
).
' ) '.
" AND $agentnums_sql", #agent virtualization
@@ -175,16 +173,17 @@ sub smart_search {
if ( $conf->exists('address1-search') ) {
my $len = length($num);
$num = lc($num);
- foreach my $prefix ( '', 'ship_' ) {
- push @cust_main, qsearch( {
- 'table' => 'cust_main',
- 'hashref' => { %options, },
- 'extra_sql' =>
- ( keys(%options) ? ' AND ' : ' WHERE ' ).
- " LOWER(SUBSTRING(${prefix}address1 FROM 1 FOR $len)) = '$num' ".
- " AND $agentnums_sql",
- } );
- }
+ # probably the Right Thing: return customers that have any associated
+ # locations matching the string, not just bill/ship location
+ push @cust_main, qsearch( {
+ 'table' => 'cust_main',
+ 'addl_from' => ' JOIN cust_location USING (custnum) ',
+ 'hashref' => { %options, },
+ 'extra_sql' =>
+ ( keys(%options) ? ' AND ' : ' WHERE ' ).
+ " LOWER(SUBSTRING(cust_location.address1 FROM 1 FOR $len)) = '$num' ".
+ " AND $agentnums_sql",
+ } );
}
} elsif ( $search =~ /^\s*(\S.*\S)\s+\((.+), ([^,]+)\)\s*$/ ) {
@@ -196,20 +195,19 @@ sub smart_search {
#so just do an exact search (but case-insensitive, so USPS standardization
#doesn't throw a wrench in the works)
- foreach my $prefix ( '', 'ship_' ) {
- push @cust_main, qsearch( {
+ push @cust_main, qsearch( {
'table' => 'cust_main',
'hashref' => { %options },
'extra_sql' =>
- ( keys(%options) ? ' AND ' : ' WHERE ' ).
- join(' AND ',
- " LOWER(${prefix}first) = ". dbh->quote(lc($first)),
- " LOWER(${prefix}last) = ". dbh->quote(lc($last)),
- " LOWER(${prefix}company) = ". dbh->quote(lc($company)),
- $agentnums_sql,
- ),
- } );
- }
+ ( keys(%options) ? ' AND ' : ' WHERE ' ).
+ join(' AND ',
+ " LOWER(first) = ". dbh->quote(lc($first)),
+ " LOWER(last) = ". dbh->quote(lc($last)),
+ " LOWER(company) = ". dbh->quote(lc($company)),
+ $agentnums_sql,
+ ),
+ } ),
+ #contacts?
} elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search
# try (ship_){last,company}
@@ -247,16 +245,14 @@ sub smart_search {
#exact
my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
- $sql .= "
- ( ( LOWER(last) = $q_last AND LOWER(first) = $q_first )
- OR ( LOWER(ship_last) = $q_last AND LOWER(ship_first) = $q_first )
- )";
+ $sql .= "( LOWER(cust_main.last) = $q_last AND LOWER(cust_main.first) = $q_first )";
push @cust_main, qsearch( {
'table' => 'cust_main',
'hashref' => \%options,
'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
} );
+ #contacts?
# or it just be something that was typed in... (try that in a sec)
@@ -268,11 +264,13 @@ sub smart_search {
my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
$sql .= " ( LOWER(last) = $q_value
OR LOWER(company) = $q_value
- OR LOWER(ship_last) = $q_value
- OR LOWER(ship_company) = $q_value
";
- $sql .= " OR LOWER(address1) = $q_value
- OR LOWER(ship_address1) = $q_value
+ #yes, it's a kludge
+ $sql .= " OR EXISTS(
+ SELECT 1 FROM cust_location
+ WHERE LOWER(cust_location.address1) = $q_value
+ AND cust_location.custnum = cust_main.custnum
+ )
"
if $conf->exists('address1-search');
$sql .= " )";
@@ -294,32 +292,21 @@ sub smart_search {
my @hashrefs = (
{ 'company' => { op=>'ILIKE', value=>"%$value%" }, },
- { 'ship_company' => { op=>'ILIKE', value=>"%$value%" }, },
);
if ( $first && $last ) {
+ #contacts? ship_first/ship_last are gone
push @hashrefs,
{ 'first' => { op=>'ILIKE', value=>"%$first%" },
'last' => { op=>'ILIKE', value=>"%$last%" },
},
- { 'ship_first' => { op=>'ILIKE', value=>"%$first%" },
- 'ship_last' => { op=>'ILIKE', value=>"%$last%" },
- },
;
} else {
push @hashrefs,
{ 'last' => { op=>'ILIKE', value=>"%$value%" }, },
- { 'ship_last' => { op=>'ILIKE', value=>"%$value%" }, },
- ;
- }
-
- if ( $conf->exists('address1-search') ) {
- push @hashrefs,
- { 'address1' => { op=>'ILIKE', value=>"%$value%" }, },
- { 'ship_address1' => { op=>'ILIKE', value=>"%$value%" }, },
;
}
@@ -335,27 +322,38 @@ sub smart_search {
}
+ if ( $conf->exists('address1-search') ) {
+
+ push @cust_main, qsearch( {
+ 'table' => 'cust_main',
+ 'addl_from' => 'JOIN cust_location USING (custnum)',
+ 'extra_sql' => 'WHERE cust_location.address1 ILIKE '.
+ dbh->quote("%$value%"),
+ } );
+
+ }
+
#fuzzy
- my @fuzopts = (
- \%options, #hashref
- '', #select
- " AND $agentnums_sql", #extra_sql #agent virtualization
+ my %fuzopts = (
+ 'hashref' => \%options,
+ 'select' => '',
+ 'extra_sql' => " AND $agentnums_sql", #agent virtualization
);
if ( $first && $last ) {
push @cust_main, FS::cust_main::Search->fuzzy_search(
{ 'last' => $last, #fuzzy hashref
'first' => $first }, #
- @fuzopts
+ %fuzopts
);
}
foreach my $field ( 'last', 'company' ) {
push @cust_main,
- FS::cust_main::Search->fuzzy_search( { $field => $value }, @fuzopts );
+ FS::cust_main::Search->fuzzy_search( { $field => $value }, %fuzopts );
}
if ( $conf->exists('address1-search') ) {
push @cust_main,
- FS::cust_main::Search->fuzzy_search( { 'address1' => $value }, @fuzopts );
+ FS::cust_main::Search->fuzzy_search( { 'address1' => $value }, %fuzopts );
}
}
@@ -566,11 +564,12 @@ sub search {
##
if ( $params->{'address'} =~ /\S/ ) {
my $address = dbh->quote('%'. lc($params->{'address'}). '%');
- push @where, '('. join(' OR ',
- map "LOWER($_) LIKE $address",
- qw(address1 address2 ship_address1 ship_address2)
- ).
- ')';
+ push @where, "EXISTS(
+ SELECT 1 FROM cust_location
+ WHERE cust_location.custnum = cust_main.custnum
+ AND (LOWER(cust_location.address1) LIKE $address OR
+ LOWER(cust_location.address2) LIKE $address)
+ )";
}
###
@@ -839,20 +838,27 @@ sub search {
}
-=item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
+=item fuzzy_search FUZZY_HASHREF [ OPTS ]
Performs a fuzzy (approximate) search and returns the matching FS::cust_main
records. Currently, I<first>, I<last>, I<company> and/or I<address1> may be
-specified (the appropriate ship_ field is also searched).
+specified.
Additional options are the same as FS::Record::qsearch
=cut
sub fuzzy_search {
- my( $self, $fuzzy, $hash, @opt) = @_;
- #$self
- $hash ||= {};
+ my( $self, $fuzzy ) = @_;
+ # sensible defaults, then merge in any passed options
+ my %fuzopts = (
+ 'table' => 'cust_main',
+ 'addl_from' => '',
+ 'extra_sql' => '',
+ 'hashref' => {},
+ @_
+ );
+
my @cust_main = ();
check_and_rebuild_fuzzyfiles();
@@ -866,8 +872,25 @@ sub fuzzy_search {
my @fcust = ();
foreach ( keys %match ) {
- push @fcust, qsearch('cust_main', { %$hash, $field=>$_}, @opt);
- push @fcust, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt);
+ if ( $field eq 'address1' ) {
+ #because it lives outside the table
+ my $addl_from = $fuzopts{addl_from} .
+ 'JOIN cust_location USING (custnum)';
+ my $extra_sql = $fuzopts{extra_sql} .
+ " AND cust_location.address1 = ".dbh->quote($_);
+ push @fcust, qsearch({
+ %fuzopts,
+ 'addl_from' => $addl_from,
+ 'extra_sql' => $extra_sql,
+ });
+ } else {
+ my $hash = $fuzopts{hashref};
+ $hash->{$field} = $_;
+ push @fcust, qsearch({
+ %fuzopts,
+ 'hashref' => $hash
+ });
+ }
}
my %fsaw = ();
push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust;
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 788b1d3..6899fa4 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -2616,6 +2616,39 @@ Returns the label of the location object (see L<FS::cust_location>).
#end of subs in location_Mixin.pm now... unfortunately the POD doesn't mixin
+=item tax_locationnum
+
+Returns the foreign key to a L<FS::cust_location> object for calculating
+tax on this package, as determined by the C<tax-pkg_address> and
+C<tax-ship_address> configuration flags.
+
+=cut
+
+sub tax_locationnum {
+ my $self = shift;
+ my $conf = FS::Conf->new;
+ if ( $conf->exists('tax-pkg_address') ) {
+ return $self->locationnum;
+ }
+ elsif ( $conf->exists('tax-ship_address') ) {
+ return $self->cust_main->ship_locationnum;
+ }
+ else {
+ return $self->cust_main->bill_locationnum;
+ }
+}
+
+=item tax_location
+
+Returns the L<FS::cust_location> object for tax_locationnum.
+
+=cut
+
+sub tax_location {
+ my $self = shift;
+ FS::cust_location->by_key( $self->tax_locationnum )
+}
+
=item seconds_since TIMESTAMP
Returns the number of seconds all accounts (see L<FS::svc_acct>) in this
@@ -3602,6 +3635,25 @@ sub fcc_477_count {
}
+=item tax_locationnum_sql
+
+Returns an SQL expression for the tax location for a package, based
+on the settings of 'tax-pkg_address' and 'tax-ship_address'.
+
+=cut
+
+sub tax_locationnum_sql {
+ my $conf = FS::Conf->new;
+ if ( $conf->exists('tax-pkg_address') ) {
+ 'cust_pkg.locationnum';
+ }
+ elsif ( $conf->exists('tax-ship_address') ) {
+ 'cust_main.ship_locationnum';
+ }
+ else {
+ 'cust_main.bill_locationnum';
+ }
+}
=item location_sql
diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm
index 62bcebc..c3e781a 100644
--- a/FS/FS/msg_template.pm
+++ b/FS/FS/msg_template.pm
@@ -485,6 +485,11 @@ sub substitutions {
signupdate dundate
packages recurdates
),
+ #compatibility: obsolete ship_ fields
+ map ( { [ "ship_$_" => sub { shift->$_ } ] }
+ qw( last first company name name_short contact contact_firstlast
+ daytime night fax )
+ ),
[ expdate => sub { shift->paydate_epoch } ], #compatibility
[ signupdate_ymd => sub { $ymd->(shift->signupdate) } ],
[ dundate_ymd => sub { $ymd->(shift->dundate) } ],