summaryrefslogtreecommitdiff
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
parentf2c26594352302de80c2cd0cbba8b0e2abada6f7 (diff)
customer bill/ship location refactoring, #940
-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
-rwxr-xr-xhttemplate/edit/cust_main.cgi194
-rw-r--r--httemplate/edit/cust_main/after_bill_location.html12
-rw-r--r--httemplate/edit/cust_main/before_bill_location.html10
-rw-r--r--httemplate/edit/cust_main/birthdate.html1
-rw-r--r--httemplate/edit/cust_main/bottomfixup.js28
-rw-r--r--httemplate/edit/cust_main/company.html7
-rw-r--r--httemplate/edit/cust_main/fax.html5
-rw-r--r--httemplate/edit/cust_main/name.html53
-rw-r--r--httemplate/edit/cust_main/phones.html29
-rw-r--r--httemplate/edit/cust_main/stateid.html39
-rw-r--r--httemplate/edit/cust_main/top_misc.html27
-rw-r--r--httemplate/edit/msg_template.html16
-rw-r--r--httemplate/edit/process/cust_location.cgi6
-rwxr-xr-xhttemplate/edit/process/cust_main.cgi43
-rw-r--r--httemplate/elements/location.html99
-rw-r--r--httemplate/elements/standardize_locations.js5
-rw-r--r--httemplate/elements/tr-select-cust_location.html50
-rwxr-xr-xhttemplate/search/report_tax.cgi98
-rw-r--r--httemplate/view/cust_main/contacts.html146
-rwxr-xr-xhttemplate/view/cust_main/locations.html22
-rw-r--r--httemplate/view/cust_main/misc.html6
35 files changed, 1413 insertions, 889 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) } ],
diff --git a/httemplate/edit/cust_main.cgi b/httemplate/edit/cust_main.cgi
index 0289246..a30c7c1 100755
--- a/httemplate/edit/cust_main.cgi
+++ b/httemplate/edit/cust_main.cgi
@@ -23,6 +23,8 @@
% }
%# agent, agent_custid, refnum (advertising source), referral_custnum
+%# better section title for this?
+<FONT CLASS="fsinnerbox-title"><% mt('Basics') |h %></FONT>
<& cust_main/top_misc.html, $cust_main, 'custnum' => $custnum &>
%# birthdate
@@ -33,113 +35,77 @@
<BR>
<& cust_main/birthdate.html, $cust_main &>
% }
-
-%# contact info
-
-% my $same_checked = '';
-% my $ship_disabled = '';
-% my @ship_style = ();
-% unless ( $cust_main->ship_last && $same ne 'Y' ) {
-% $same_checked = 'CHECKED';
-% $ship_disabled = 'DISABLED';
-% push @ship_style, 'background-color:#dddddd';
-% foreach (
-% qw( last first company address1 address2 city county state zip country
-% latitude longitude coord_auto
-% daytime night fax mobile )
-% ) {
-% $cust_main->set("ship_$_", $cust_main->get($_) );
-% }
-% }
-
+% my $has_ship_address = '';
+% if ( $cgi->param('error') ) {
+% $has_ship_address = !$cgi->param('same');
+% } elsif ( $cust_main->custnum ) {
+% $has_ship_address = $cust_main->has_ship_address;
+% }
<BR>
-<FONT CLASS="fsinnerbox-title"><% mt('Billing address') |h %></FONT>
-
-<& cust_main/contact.html,
- 'cust_main' => $cust_main,
- 'pre' => '',
- 'onchange' => 'bill_changed(this)',
- 'disabled' => '',
- 'ss' => $ss,
- 'stateid' => $stateid,
- 'same_checked' => $same_checked, #for address2 "Unit #" labeling
-&>
+<TABLE> <TR>
+ <TD STYLE="width:650px">
+%#; padding-right:2px; vertical-align:top">
+ <FONT CLASS="fsinnerbox-title"><% mt('Billing address') |h %></FONT>
+ <TABLE CLASS="fsinnerbox">
+ <& cust_main/before_bill_location.html, $cust_main &>
+ <& /elements/location.html,
+ object => $cust_main->bill_location,
+ prefix => 'bill_',
+ &>
+ <& cust_main/after_bill_location.html, $cust_main &>
+ </TABLE>
+ </TD>
+</TR>
+<TR><TD STYLE="height:40px"></TD></TR>
+<TR>
+ <TD STYLE="width:650px">
+%#; padding-left:2px; vertical-align:top">
+ <FONT CLASS="fsinnerbox-title"><% mt('Service address') |h %></FONT>
+ <INPUT TYPE="checkbox"
+ NAME="same"
+ ID="same"
+ onclick="samechanged(this)"
+ onkeyup="samechanged(this)"
+ VALUE="Y"
+ <% $has_ship_address ? '' : 'CHECKED' %>
+ ><% mt('same as billing address') |h %>
+ <TABLE CLASS="fsinnerbox" ID="table_ship_location">
+ <& /elements/location.html,
+ object => $cust_main->ship_location,
+ prefix => 'ship_',
+ enable_censustract => 1,
+ enable_district => 1,
+ &>
+ </TABLE>
+ <TABLE CLASS="fsinnerbox" ID="table_ship_location_blank"
+ STYLE="display:none">
+ <TR><TD></TD></TR>
+ </TABLE>
+ </TD>
+</TR></TABLE>
<SCRIPT>
-function bill_changed(what) {
- if ( what.form.same.checked ) {
-% for (qw( last first company address1 address2 city zip latitude longitude coord_auto daytime night fax mobile )) {
- what.form.ship_<%$_%>.value = what.form.<%$_%>.value;
-% }
-
- what.form.ship_country.selectedIndex = what.form.country.selectedIndex;
-
- function fix_ship_city() {
- what.form.ship_city_select.selectedIndex = what.form.city_select.selectedIndex;
- what.form.ship_city.style.display = what.form.city.style.display;
- what.form.ship_city_select.style.display = what.form.city_select.style.display;
- }
-
- function fix_ship_county() {
- what.form.ship_county.selectedIndex = what.form.county.selectedIndex;
- ship_county_changed(what.form.ship_county, fix_ship_city );
- }
-
- function fix_ship_state() {
- what.form.ship_state.selectedIndex = what.form.state.selectedIndex;
- ship_state_changed(what.form.ship_state, fix_ship_county );
- }
-
- ship_country_changed(what.form.ship_country, fix_ship_state );
-
- }
-}
function samechanged(what) {
+%# not display = 'none', because we still want it to take up space
+%# document.getElementById('table_ship_location').style.visibility =
+%# what.checked ? 'hidden' : 'visible';
+ var t1 = document.getElementById('table_ship_location');
+ var t2 = document.getElementById('table_ship_location_blank');
if ( what.checked ) {
- bill_changed(what);
-
-% my @fields = qw( last first company address1 address2 city city_select county state zip country latitude longitude daytime night fax mobile );
-% for (@fields) {
- what.form.ship_<%$_%>.disabled = true;
- what.form.ship_<%$_%>.style.backgroundColor = '#dddddd';
-% }
-
-% if ( $conf->exists('cust_main-require_address2') ) {
- document.getElementById('address2_required').style.visibility = '';
- document.getElementById('address2_label').style.visibility = '';
- document.getElementById('ship_address2_required').style.visibility = 'hidden';
- document.getElementById('ship_address2_label').style.visibility = 'hidden';
-% }
-
- } else {
-
-% for (@fields) {
- what.form.ship_<%$_%>.disabled = false;
- what.form.ship_<%$_%>.style.backgroundColor = '#ffffff';
-% }
-
-% if ( $conf->exists('cust_main-require_address2') ) {
- document.getElementById('address2_required').style.visibility = 'hidden';
- document.getElementById('address2_label').style.visibility = 'hidden';
- document.getElementById('ship_address2_required').style.visibility = '';
- document.getElementById('ship_address2_label').style.visibility = '';
-% }
-
+ t2.style.width = t1.clientWidth + 'px';
+ t2.style.height = t1.clientHeight + 'px';
+ t1.style.display = 'none';
+ t2.style.display = '';
+ }
+ else {
+ t2.style.display = 'none';
+ t1.style.display = '';
}
}
+samechanged(document.getElementById('same'));
</SCRIPT>
<BR>
-<FONT CLASS="fsinnerbox-title"><% mt('Service address') |h %></FONT>
-
-<INPUT TYPE="checkbox" NAME="same" VALUE="Y" onClick="samechanged(this)" <%$same_checked%>><% mt('same as billing address') |h %>
-<& cust_main/contact.html,
- 'cust_main' => $cust_main,
- 'pre' => 'ship_',
- 'onchange' => '',
- 'disabled' => $ship_disabled,
- 'style' => \@ship_style
-&>
<& cust_main/contacts_new.html,
'cust_main' => $cust_main,
@@ -242,10 +208,28 @@ my $locationnum = '';
if ( $cgi->param('error') ) {
+ # false laziness w/ edit/process/cust_main.cgi
+ my %locations;
+ for my $pre (qw(bill ship)) {
+ my %hash;
+ foreach ( FS::cust_main->location_fields ) {
+ $hash{$_} = scalar($cgi->param($pre.'_'.$_));
+ }
+ $hash{'custnum'} = $cgi->param('custnum');
+ $locations{$pre} = qsearchs('cust_location', \%hash)
+ || FS::cust_location->new( \%hash );
+ }
+
$cust_main = new FS::cust_main ( {
- map { $_, scalar($cgi->param($_)) } fields('cust_main')
+ map { ( $_, scalar($cgi->param($_)) ) } (fields('cust_main')),
+ map { ( "ship_$_", '' ) } (FS::cust_main->location_fields)
} );
+ for my $pre (qw(bill ship)) {
+ $cust_main->set($pre.'_location', $locations{$pre});
+ $cust_main->set($pre.'_locationnum', $locations{$pre}->locationnum);
+ }
+
$custnum = $cust_main->custnum;
die "access denied"
@@ -355,6 +339,20 @@ if ( $cgi->param('error') ) {
$svc_dsl{$_} = $qual->$_
foreach qw( phonenum vendor_qual_id );
}
+ else {
+ my $countrydefault = $conf->config('countrydefault') || 'US';
+ my $statedefault = $conf->config('statedefault') || 'CA';
+ $cust_main->set('bill_location',
+ FS::cust_location->new(
+ { country => $countrydefault, state => $statedefault }
+ )
+ );
+ $cust_main->set('ship_location',
+ FS::cust_location->new(
+ { country => $countrydefault, state => $statedefault }
+ )
+ );
+ }
if ( $cgi->param('lock_pkgpart') =~ /^(\d+)$/ ) {
my $pkgpart = $1;
diff --git a/httemplate/edit/cust_main/after_bill_location.html b/httemplate/edit/cust_main/after_bill_location.html
new file mode 100644
index 0000000..2f4c3b5
--- /dev/null
+++ b/httemplate/edit/cust_main/after_bill_location.html
@@ -0,0 +1,12 @@
+% if ( ! $conf->exists('cust-edit-alt-field-order') ) {
+ <& phones.html, $cust_main &>
+ <& fax.html, $cust_main &>
+% } else {
+ <& fax.html, $cust_main &>
+ <& company.html, $cust_main &>
+% }
+<& stateid.html, $cust_main &>
+<%init>
+my $cust_main = shift;
+my $conf = FS::Conf->new;
+</%init>
diff --git a/httemplate/edit/cust_main/before_bill_location.html b/httemplate/edit/cust_main/before_bill_location.html
new file mode 100644
index 0000000..973201e
--- /dev/null
+++ b/httemplate/edit/cust_main/before_bill_location.html
@@ -0,0 +1,10 @@
+<& name.html, $cust_main &>
+% if ( ! $conf->exists('cust-edit-alt-field-order') ) {
+ <& company.html, $cust_main &>
+% } else {
+ <& phones.html, $cust_main &>
+% }
+<%init>
+my $cust_main = shift;
+my $conf = FS::Conf->new;
+</%init>
diff --git a/httemplate/edit/cust_main/birthdate.html b/httemplate/edit/cust_main/birthdate.html
index 6d1c221..5d6a123 100644
--- a/httemplate/edit/cust_main/birthdate.html
+++ b/httemplate/edit/cust_main/birthdate.html
@@ -1,4 +1,5 @@
<% ntable("#cccccc", 2) %>
+% # maybe put after the contact names?
% if ( $conf->exists('cust_main-enable_birthdate') ) {
<% include( '/elements/tr-input-date-field.html', {
'name' => 'birthdate',
diff --git a/httemplate/edit/cust_main/bottomfixup.js b/httemplate/edit/cust_main/bottomfixup.js
index 800864b..77d4294 100644
--- a/httemplate/edit/cust_main/bottomfixup.js
+++ b/httemplate/edit/cust_main/bottomfixup.js
@@ -66,21 +66,25 @@ function copy_payby_fields() {
%# call submit_continue() on completion...
%# otherwise not touching standardize_locations for now
<% include( '/elements/standardize_locations.js',
- 'callback' => 'submit_continue();'
+ 'callback' => 'submit_continue();',
+ 'main_prefix' => 'bill_',
+ 'no_company' => 1,
)
%>
+var prefix;
function fetch_censustract() {
//alert('fetch census tract data');
+ prefix = document.getElementById('same').checked ? 'bill_' : 'ship_';
var cf = document.CustomerForm;
- var state_el = cf.elements['ship_state'];
+ var state_el = cf.elements[prefix + 'state'];
var census_data = new Array(
'year', <% $conf->config('census_year') || '2012' %>,
- 'address1', cf.elements['ship_address1'].value,
- 'city', cf.elements['ship_city'].value,
+ 'address1', cf.elements[prefix + 'address1'].value,
+ 'city', cf.elements[prefix + 'city'].value,
'state', state_el.options[ state_el.selectedIndex ].value,
- 'zip', cf.elements['ship_zip'].value
+ 'zip', cf.elements[prefix + 'zip'].value
);
censustract( census_data, update_censustract );
@@ -109,19 +113,21 @@ function update_censustract(arg) {
set_censustract = function () {
- cf.elements['censustract'].value = newcensus;
+ cf.elements[prefix + 'censustract'].value = newcensus;
submit_continue();
}
- if (error || cf.elements['censustract'].value != newcensus) {
+ if (error || cf.elements[prefix + 'censustract'].value != newcensus) {
// popup an entry dialog
if (error) { newcensus = error; }
newcensus.replace(/.*ndefined.*/, 'Not found');
- var latitude = cf.elements['latitude' ].value || '<% $company_latitude %>';
- var longitude= cf.elements['longitude'].value || '<% $company_longitude %>';
+ var latitude = cf.elements[prefix + 'latitude'].value
+ || '<% $company_latitude %>';
+ var longitude= cf.elements[prefix + 'longitude'].value
+ || '<% $company_longitude %>';
var choose_censustract =
'<CENTER><BR><B>Confirm censustract</B><BR>' +
@@ -132,14 +138,14 @@ function update_censustract(arg) {
'" target="_blank">Map service module location</A><BR>' +
'<A href="http://maps.ffiec.gov/FFIECMapper/TGMapSrv.aspx?' +
'census_year=<% $conf->config('census_year') || '2012' %>' +
- '&zip_code=' + cf.elements['ship_zip'].value +
+ '&zip_code=' + cf.elements[prefix + 'zip'].value +
'" target="_blank">Map zip code center</A><BR><BR>' +
'<TABLE>';
choose_censustract = choose_censustract +
'<TR><TH style="width:50%">Entered census tract</TH>' +
'<TH style="width:50%">Calculated census tract</TH></TR>' +
- '<TR><TD>' + cf.elements['censustract'].value +
+ '<TR><TD>' + cf.elements[prefix + 'censustract'].value +
'</TD><TD>' + newcensus + '</TD></TR>' +
'<TR><TD>&nbsp;</TD><TD>&nbsp;</TD></TR>';
diff --git a/httemplate/edit/cust_main/company.html b/httemplate/edit/cust_main/company.html
new file mode 100644
index 0000000..8a6ed0b
--- /dev/null
+++ b/httemplate/edit/cust_main/company.html
@@ -0,0 +1,7 @@
+% my $cust_main = shift;
+<TR ID="company_row" <% $cust_main->company ? '' : 'STYLE="display:none"' %>>
+ <TD ALIGN="right"><% mt('Company') |h %></TD>
+ <TD COLSPAN=6><INPUT TYPE="text" NAME="company" ID="company" SIZE=60
+ VALUE="<% $cust_main->company |h %>">
+ </TD>
+</TR>
diff --git a/httemplate/edit/cust_main/fax.html b/httemplate/edit/cust_main/fax.html
new file mode 100644
index 0000000..237d4be
--- /dev/null
+++ b/httemplate/edit/cust_main/fax.html
@@ -0,0 +1,5 @@
+% my $cust_main = shift;
+<TR>
+ <TD ALIGN="right"><% mt('Fax') |h %></TD>
+ <TD><INPUT TYPE="text" NAME="fax" VALUE="<% $cust_main->fax %>" SIZE=18></TD>
+</TR>
diff --git a/httemplate/edit/cust_main/name.html b/httemplate/edit/cust_main/name.html
new file mode 100644
index 0000000..2641ec9
--- /dev/null
+++ b/httemplate/edit/cust_main/name.html
@@ -0,0 +1,53 @@
+<%def .namepart>
+% my ($field, $value, $label, $extra) = @_;
+<TD>
+ <INPUT TYPE="text" NAME="<% $field %>" VALUE="<% $value |h %>" <%$extra%>>
+ <BR><FONT SIZE=-1><% mt($label) %></FONT>
+</TD>
+</%def>
+
+<TR>
+ <TH VALIGN="top" ALIGN="right"><%$r%><% mt('Contact name') |h %></TH>
+ <TD COLSPAN=6>
+ <TABLE CELLSPACING=0 CELLPADDING=0>
+ <TR>
+ <& .namepart, 'last', $cust_main->last, 'Last' &>
+ <TD VALIGN="top"> , </TD>
+ <& .namepart, 'first', $cust_main->first, 'First' &>
+% if ( $conf->exists('show_ss') ) {
+ <TD>&nbsp;</TD>
+ <& .namepart, 'ss', $ss, 'SS#', "SIZE=11" &>
+% } else {
+ <INPUT TYPE="hidden" NAME="ss" VALUE="<% $ss %>">
+% }
+ </TR>
+ </TABLE>
+ </TD>
+</TR>
+
+% if ( $conf->exists('cust-email-high-visibility') ) {
+<TR>
+ <TD ALIGN="right">
+ <% $conf->exists('cust_main-require_invoicing_list_email', $agentnum)
+ ? $r
+ : '' %>Email address(es)
+ </TD>
+ <TD BGCOLOR="#FFFF00">
+ <INPUT TYPE="text" NAME="invoicing_list"
+ VALUE=<% $cust_main->invoicing_list_emailonly_scalar %>>
+ </TD>
+</TR>
+% }
+<%init>
+my $cust_main = shift;
+my $agentnum = $cust_main->agentnum if $cust_main->custnum;
+my $conf = FS::Conf->new;
+my $r = '<font color="#ff0000">*</font>&nbsp;';
+my $ss;
+
+if ( $cgi->param('error') or $conf->exists('unmask_ss') ) {
+ $ss = $cust_main->ss;
+} else {
+ $ss = $cust_main->masked('ss');
+}
+</%init>
diff --git a/httemplate/edit/cust_main/phones.html b/httemplate/edit/cust_main/phones.html
new file mode 100644
index 0000000..9b23e07
--- /dev/null
+++ b/httemplate/edit/cust_main/phones.html
@@ -0,0 +1,29 @@
+<TR>
+ <TD VALIGN="top" ALIGN="right"><% mt('Phones') |h %></TD>
+ <TD COLSPAN=6>
+ <TABLE CELLSPACING=0 CELLPADDING=0>
+ <TR>
+% foreach my $phone (qw(daytime night mobile)) {
+ <TD>
+ <INPUT TYPE="text"
+ NAME="<% $phone %>"
+ VALUE="<% $cust_main->get($phone) %>"
+ SIZE=18
+ >
+ <BR><FONT SIZE=-1><% mt($phone_label{$phone}) |h %></FONT>
+ </TD>
+ <TD>&nbsp;</TD>
+% }
+ </TR>
+ </TABLE>
+ </TD>
+</TR>
+<%init>
+my $cust_main = shift;
+my $conf = FS::Conf->new;
+my %phone_label = (
+ daytime => 'Day Phone',
+ night => 'Night Phone',
+ mobile => 'Mobile',
+);
+</%init>
diff --git a/httemplate/edit/cust_main/stateid.html b/httemplate/edit/cust_main/stateid.html
new file mode 100644
index 0000000..2655f51
--- /dev/null
+++ b/httemplate/edit/cust_main/stateid.html
@@ -0,0 +1,39 @@
+% if ( $conf->exists('show_stateid') ) {
+<TR>
+ <TD ALIGN="right"><% $stateid_label %></TD>
+ <TD><INPUT TYPE="text" NAME="stateid" VALUE="<% $stateid %>" SIZE=12></TD>
+ <TD><& /elements/select-state.html,
+ state => $cust_main->stateid_state,
+ country => $cust_main->country, # how does this work on new customer?
+ prefix => 'stateid_',
+ disable_countyupdate => 1,
+ &></TD>
+</TR>
+% } else {
+<INPUT TYPE="hidden" NAME="stateid" VALUE="<% $stateid %>">
+<INPUT TYPE="hidden" NAME="stateid_state" VALUE="<% $cust_main->stateid_state %>">
+% }
+
+<%init>
+my $cust_main = shift;
+my $conf = FS::Conf->new;
+my $stateid;
+if ( $cgi->param('error') ) {
+ $stateid = $cust_main->stateid;
+} elsif ( $cust_main->custnum ) {
+ $stateid = $cust_main->masked('stateid');
+} else {
+ $stateid = '';
+}
+$cust_main->set('stateid_state' => $cust_main->state)
+ unless $cust_main->stateid_state;
+
+my $stateid_label = FS::Msgcat::_gettext('stateid') =~ /^(stateid)?$/
+ ? 'Driver&rsquo;s License'
+ : FS::Msgcat::_gettext('stateid') || 'Driver&rsquo;s License';
+
+my $stateid_state_label =
+ FS::Msgcat::_gettext('stateid_state') =~ /^(stateid_state)?$/
+ ? 'Driver&rsquo;s License State'
+ : FS::Msgcat::_gettext('stateid') || 'Driver&rsquo;s License State';
+</%init>
diff --git a/httemplate/edit/cust_main/top_misc.html b/httemplate/edit/cust_main/top_misc.html
index 7ba167b..7ce283c 100644
--- a/httemplate/edit/cust_main/top_misc.html
+++ b/httemplate/edit/cust_main/top_misc.html
@@ -20,27 +20,16 @@
<% $cust_main->residential_commercial eq 'Commercial' ? 'CHECKED' : '' %>
></TD>
</TR>
-
<SCRIPT TYPE="text/javascript">
- function rescom_changed() {
- var f = document.CustomerForm;
-
- if ( f.residential_commercial_Residential.checked ) {
- document.getElementById('contacts_div').style.display = 'none';
- } else { // if ( f.residential_commercial_Commercial.checked ) {
- document.getElementById('contacts_div').style.display = '';
- }
-
- if ( f.residential_commercial_Residential.checked && ! f.company.value.length ) {
- document.getElementById('company_row').style.display = 'none'
- } else { // if ( f.residential_commercial_Commercial.checked ) {
+ function rescom_changed(what) {
+ if ( what.checked == (what.value == 'Commercial' ) ) {
document.getElementById('company_row').style.display = '';
- }
-
- if ( f.residential_commercial_Residential.checked && ! f.ship_company.value.length ) {
- document.getElementById('ship_company_row').style.display = 'none'
- } else { // if ( f.residential_commercial_Commercial.checked ) {
- document.getElementById('ship_company_row').style.display = '';
+ document.getElementById('contacts_div').style.display = '';
+ } else {
+ if ( document.getElementById('company').value.length == 0 ) {
+ document.getElementById('company_row').style.display = 'none';
+ }
+ document.getElementById('contacts_div').style.display = 'none';
}
}
</SCRIPT>
diff --git a/httemplate/edit/msg_template.html b/httemplate/edit/msg_template.html
index f50d66d..115032a 100644
--- a/httemplate/edit/msg_template.html
+++ b/httemplate/edit/msg_template.html
@@ -227,6 +227,15 @@ my %substitutions = (
'$mobile' => 'Mobile phone',
'$fax' => 'Fax',
],
+ 'service' => [
+ '$ship_address1' => 'Address line 1',
+ '$ship_address2' => 'Address line 2',
+ '$ship_city' => 'City',
+ '$ship_county' => 'County',
+ '$ship_state' => 'State',
+ '$ship_zip' => 'Zip',
+ '$ship_country' => 'Country',
+ ],
'cust_bill' => [
'$invnum' => 'Invoice#',
],
@@ -281,15 +290,10 @@ my %substitutions = (
'$error' => 'Decline reason',
],
);
-my @c = @{ $substitutions{'contact'} };
-for (my $i=0; $i<scalar(@c); $i += 2) {
- $c[$i] =~ s/\$(.*)/\$ship_$1/;
-}
-$substitutions{'shipping'} = \@c;
tie my %sections, 'Tie::IxHash', (
'contact' => 'Name and contact info (billing)',
-'shipping' => 'Name and contact info (shipping)',
+'service' => 'Service address',
'cust_main' => 'Customer status and payment info',
'cust_pkg' => 'Package fields',
'cust_bill' => 'Invoice fields',
diff --git a/httemplate/edit/process/cust_location.cgi b/httemplate/edit/process/cust_location.cgi
index 790fc8e..b9f93db 100644
--- a/httemplate/edit/process/cust_location.cgi
+++ b/httemplate/edit/process/cust_location.cgi
@@ -28,10 +28,12 @@ my $cust_location = qsearchs({
});
die "unknown locationnum $locationnum" unless $cust_location;
-my $new = {
+my $new = FS::cust_location->new({
+ custnum => $cust_location->custnum,
+ prospectnum => $cust_location->prospectnum,
map { $_ => scalar($cgi->param($_)) }
qw( address1 address2 city county state zip country )
-};
+});
my $error = $cust_location->move_to($new);
diff --git a/httemplate/edit/process/cust_main.cgi b/httemplate/edit/process/cust_main.cgi
index 3f5e19e..5ee553b 100755
--- a/httemplate/edit/process/cust_main.cgi
+++ b/httemplate/edit/process/cust_main.cgi
@@ -57,19 +57,40 @@ push @invoicing_list, 'POST' if $cgi->param('invoicing_list_POST');
push @invoicing_list, 'FAX' if $cgi->param('invoicing_list_FAX');
$cgi->param('invoicing_list', join(',', @invoicing_list) );
+# is this actually used? if so, we need to clone locations...
+# but I can't find anything that sets this parameter to a non-empty value
+$cgi->param('duplicate_of_custnum') =~ /^(\d+)$/;
+my $duplicate_of = $1;
+
+my %locations;
+for my $pre (qw(bill ship)) {
+
+ my %hash;
+ foreach ( FS::cust_main->location_fields ) {
+ $hash{$_} = scalar($cgi->param($pre.'_'.$_));
+ }
+ $hash{'custnum'} = $cgi->param('custnum');
+ warn Dumper \%hash if $DEBUG;
+ # if we can qsearchs it, then it's unchanged, so use that
+ $locations{$pre} = qsearchs('cust_location', \%hash)
+ || FS::cust_location->new( \%hash );
+
+}
+
+if ( ($cgi->param('same') || '') eq 'Y' ) {
+ $locations{ship} = $locations{bill};
+}
#create new record object
+# but explicitly avoid setting ship_ fields
my $new = new FS::cust_main ( {
- map {
- $_, scalar($cgi->param($_))
- } fields('cust_main')
+ map { ( $_, scalar($cgi->param($_)) ) } (fields('cust_main')),
+ map { ( "ship_$_", '' ) } (FS::cust_main->location_fields)
} );
$new->invoice_noemail( ($cgi->param('invoice_email') eq 'Y') ? '' : 'Y' );
-$cgi->param('duplicate_of_custnum') =~ /^(\d+)$/;
-my $duplicate_of = $1;
if ( $duplicate_of ) {
# then negate all changes to the customer; the only change we should
# make is to order a package, if requested
@@ -78,11 +99,9 @@ if ( $duplicate_of ) {
or die "nonexistent existing customer (custnum $duplicate_of)";
}
-if ( defined($cgi->param('same')) && $cgi->param('same') eq "Y" ) {
- $new->setfield("ship_$_", '') foreach qw(
- last first company address1 address2 city county state zip
- country daytime night fax
- );
+for my $pre (qw(bill ship)) {
+ $new->set($pre.'_location', $locations{$pre});
+ $new->set($pre.'_locationnum', $locations{$pre}->locationnum);
}
if ( $cgi->param('no_credit_limit') ) {
@@ -261,6 +280,7 @@ if ( $new->custnum eq '' or $duplicate_of ) {
my $old = qsearchs( 'cust_main', { 'custnum' => $new->custnum } );
$error ||= "Old record not found!" unless $old;
+
if ( length($old->paycvv) && $new->paycvv =~ /^\s*\*+\s*$/ ) {
$new->paycvv($old->paycvv);
}
@@ -299,6 +319,9 @@ if ( $new->custnum eq '' or $duplicate_of ) {
local($FS::cust_main::DEBUG) = $DEBUG if $DEBUG;
local($FS::Record::DEBUG) = $DEBUG if $DEBUG;
+ local($Data::Dumper::Sortkeys) = 1;
+ warn Dumper({ new => $new, old => $old }) if $DEBUG;
+
$error ||= $new->replace( $old, \@invoicing_list,
'tax_exemption' => \%tax_exempt,
);
diff --git a/httemplate/elements/location.html b/httemplate/elements/location.html
index c606523..7672318 100644
--- a/httemplate/elements/location.html
+++ b/httemplate/elements/location.html
@@ -3,16 +3,16 @@
Example:
include( '/elements/location.html',
- 'object' => $cust_main, # or $cust_location
- 'prefix' => $pre, #only for cust_main objects
+ 'object' => $cust_location
+ 'prefix' => $pre, # prefixed to form field names
'onchange' => $javascript,
- 'disabled' => $disabled,
- 'same_checked' => $same_checked,
'geocode' => $geocode, #passed through
'censustract' => $censustract, #passed through
'no_asterisks' => 0, #set true to disable the red asterisks next
#to required fields
'address1_label' => 'Address', #label for address
+ 'enable_district' => 1, #show tax district field
+ 'enable_censustract' => 1, #show censustract field
)
</%doc>
@@ -40,12 +40,12 @@ Example:
% }
<TR>
- <<%$th%> ALIGN="right"><%$r%><% $opt{'address1_label'} || emt('Address') %></<%$th%>>
+ <<%$th%> STYLE="width:16ex" ALIGN="right"><%$r%><% $opt{'address1_label'} || emt('Address') %></<%$th%>>
<TD COLSPAN=7>
<INPUT TYPE = "text"
NAME = "<%$pre%>address1"
ID = "<%$pre%>address1"
- VALUE = "<% $object->get($pre.'address1') |h %>"
+ VALUE = "<% $object->get('address1') |h %>"
SIZE = 54
onChange = "<% $onchange %>"
<% $disabled %>
@@ -62,7 +62,7 @@ Example:
<INPUT TYPE = "text"
NAME = "<%$pre%>address2"
ID = "<%$pre%>address2"
- VALUE = "<% $object->get($pre.'address2') |h %>"
+ VALUE = "<% $object->get('address2') |h %>"
SIZE = 54
onChange = "<% $onchange %>"
<% $disabled %>
@@ -75,7 +75,7 @@ Example:
<INPUT TYPE = "hidden"
NAME = "<%$pre%>address2"
- VALUE = "<% $object->get($pre.'address2') |h %>"
+ VALUE = "<% $object->get('address2') |h %>"
>
<TR>
@@ -83,7 +83,7 @@ Example:
<TD COLSPAN=7>
% my $location_type = scalar($cgi->param('location_type'))
-% || $object->get($pre.'location_type');
+% || $object->get('location_type');
% #my $location_number = scalar($cgi->param('location_number'))
% # || $object->get($pre.'location_number');
%
@@ -130,7 +130,7 @@ Example:
<INPUT TYPE="text"
NAME = "location_number"
ID = "location_number"
- VALUE = "<% scalar($cgi->param('location_number')) || $object->get($pre.'location_number') |h %>"
+ VALUE = "<% scalar($cgi->param('location_number')) || $object->get('location_number') |h %>"
SIZE = "5"
<% $disabled || ($location_type ? '' : 'DISABLED') %>
<% $style %>
@@ -161,7 +161,7 @@ Example:
<INPUT TYPE = "text"
NAME = "<%$pre%>zip"
ID = "<%$pre%>zip"
- VALUE = "<% $object->get($pre.'zip') |h %>"
+ VALUE = "<% $object->get('zip') |h %>"
SIZE = 10
onChange = "<% $onchange %>"
<% $disabled %>
@@ -181,7 +181,7 @@ Example:
<INPUT TYPE = "text"
NAME = "<%$pre%>latitude"
ID = "<%$pre%>latitude"
- VALUE = "<% $object->get($pre.'latitude') |h %>"
+ VALUE = "<% $object->get('latitude') |h %>"
<% $disabled %>
<% $style %>
>
@@ -189,36 +189,44 @@ Example:
<INPUT TYPE = "text"
NAME = "<%$pre%>longitude"
ID = "<%$pre%>longitude"
- VALUE = "<% $object->get($pre.'longitude') |h %>"
+ VALUE = "<% $object->get('longitude') |h %>"
<% $disabled %>
<% $style %>
>
</TD>
</TR>
-<INPUT TYPE="hidden" NAME="<%$pre%>coord_auto" VALUE="<% $object->get($pre.'coord_auto') %>">
+<INPUT TYPE="hidden" NAME="<%$pre%>coord_auto" VALUE="<% $object->coord_auto %>">
-% if ( !$pre ) {
- <INPUT TYPE="hidden" NAME="geocode" VALUE="<% $opt{geocode} %>">
+<INPUT TYPE="hidden" NAME="<%$pre%>geocode" VALUE="<% $object->geocode %>">
+<INPUT TYPE="hidden" NAME="<%$pre%>censusyear" VALUE="<% $object->censusyear %>">
+<TR>
+% if ( $opt{enable_censustract} ) {
+ <TD ALIGN="right">Census&nbsp;tract</TD>
+ <TD COLSPAN=8>
+ <INPUT TYPE="text" SIZE=15
+ NAME="<%$pre%>censustract"
+ VALUE="<% $object->censustract %>">
+ <% '(automatic)' %>
+ </TD>
% } else {
-% if ( $pre eq 'ship_' && $conf->exists('cust_main-require_censustract') ) {
- <TR><<%$th%> ALIGN="right">Census tract<BR>(automatic)</<%$th%>>
- <TD>
- <INPUT TYPE="text" NAME="censustract" VALUE="<% $opt{censustract} %>">
- <INPUT TYPE="hidden" NAME="censusyear" VALUE="<% $object->get('censusyear') %>">
- </TD>
- </TR>
+ <INPUT TYPE="hidden" NAME="<%$pre%>censustract" VALUE="<% $object->censustract %>">
+% }
+</TR>
+% if ( $conf->config('tax_district_method') ) {
+ <TR>
+% if ( $opt{enable_district} ) {
+ <TD ALIGN="right">Tax&nbsp;district</TD>
+ <TD COLSPAN=8>
+ <INPUT TYPE="text" SIZE=15
+ NAME="<%$pre%>district"
+ VALUE="<% $object->district %>">
+ <% '(automatic)' %>
+ </TD>
% } else {
- <INPUT TYPE="hidden" NAME="censustract" VALUE="<% $opt{censustract} %>">
-% }
-% if ( $conf->config('tax_district_method') or $object->get('district') ) {
- <TR>
- <<%$th%> ALIGN="right">Tax district<BR>(automatic)</<%$th%>>
- <TD>
- <INPUT TYPE="text" NAME="district" VALUE="<%$object->get('district')%>">
- </TD>
- </TR>
+ <INPUT TYPE="hidden" NAME="<%$pre%>district" VALUE="<% $object->district %>">
% }
-% }
+ </TR>
+% }
<%init>
@@ -233,16 +241,13 @@ my $conf = new FS::Conf;
my $r = $opt{'no_asterisks'} ? '' : qq!<font color="#ff0000">*</font>&nbsp;!;
-#false laziness with ship state
my $countrydefault = $conf->config('countrydefault') || 'US';
-$object->set($pre.'country', $countrydefault )
- unless $object->get($pre.'country');
-
-my $statedefault = $conf->config('statedefault')
+my $statedefault = $conf->config('statedefault')
|| ($countrydefault eq 'US' ? 'CA' : '');
-$object->set($pre.'state', $statedefault )
- unless $object->get($pre.'state')
- || $object->get($pre.'country') ne $countrydefault;
+$object ||= FS::cust_location->new({
+ 'country' => $countrydefault,
+ 'state' => $statedefault,
+});
my $alt_err = ($opt{'alt_format'} && !$disabled) ? $object->alternize : '';
@@ -255,8 +260,8 @@ push @address2_label_style, 'visibility:hidden'
|| ! $conf->exists('cust_main-require_address2')
|| ( !$pre && !$opt{'same_checked'} );
-my @counties = counties( $object->get($pre.'state'),
- $object->get($pre.'country'),
+my @counties = counties( $object->get('state'),
+ $object->get('country'),
);
my @county_style = ();
push @county_style, 'display:none' # 'visibility:hidden'
@@ -276,10 +281,10 @@ my $county_style =
: '';
my %select_hash = (
- 'city' => $object->get($pre.'city'),
- 'county' => $object->get($pre.'county'),
- 'state' => $object->get($pre.'state'),
- 'country' => $object->get($pre.'country'),
+ 'city' => $object->get('city'),
+ 'county' => $object->get('county'),
+ 'state' => $object->get('state'),
+ 'country' => $object->get('country'),
'prefix' => $pre,
'onchange' => $onchange,
'disabled' => $disabled,
diff --git a/httemplate/elements/standardize_locations.js b/httemplate/elements/standardize_locations.js
index e6a4aa6..86f8d2b 100644
--- a/httemplate/elements/standardize_locations.js
+++ b/httemplate/elements/standardize_locations.js
@@ -10,7 +10,7 @@ function standardize_locations() {
'onlyship', 1,
% } else {
% if ( $withfirm ) {
- 'company', cf.elements['<% $main_prefix %>company'].value,
+ 'company', cf.elements['company'].value,
% }
'address1', cf.elements['<% $main_prefix %>address1'].value,
'address2', cf.elements['<% $main_prefix %>address2'].value,
@@ -18,9 +18,6 @@ function standardize_locations() {
'state', state_el.options[ state_el.selectedIndex ].value,
'zip', cf.elements['<% $main_prefix %>zip'].value,
% }
-% if ( $withfirm ) {
- 'ship_company', cf.elements['<% $ship_prefix %>company'].value,
-% }
'ship_address1', cf.elements['<% $ship_prefix %>address1'].value,
'ship_address2', cf.elements['<% $ship_prefix %>address2'].value,
'ship_city', cf.elements['<% $ship_prefix %>city'].value,
diff --git a/httemplate/elements/tr-select-cust_location.html b/httemplate/elements/tr-select-cust_location.html
index 0ca255b..05712ee 100644
--- a/httemplate/elements/tr-select-cust_location.html
+++ b/httemplate/elements/tr-select-cust_location.html
@@ -11,7 +11,6 @@ Example:
#optional
'empty_label' => '(default service address)',
- 'disable_empty' => 0, #1 to disable
)
</%doc>
@@ -52,11 +51,12 @@ Example:
var ftype = what.form.<%$_%>.tagName;
if( ftype != 'SELECT') what.form.<%$_%>.style.backgroundColor = '#ffffff';
% }
-
+% if ( $opt{'alt_format'} ) {
if ( what.form.location_type.options[what.form.location_type.selectedIndex].value ) {
what.form.location_number.disabled = false;
what.form.location_number.style.backgroundColor = '#ffffff';
}
+% }
}
function locationnum_changed(what) {
@@ -101,25 +101,8 @@ Example:
return;
}
- if ( locationnum == 0 ) { //(default service address)
-% if ( $cust_main ) {
- what.form.address1.value = <% $cust_main->get($prefix.'address1') |js_string %>;
- what.form.address2.value = <% $cust_main->get($prefix.'address2') |js_string %>;
- what.form.city.value = <% $cust_main->get($prefix.'city') |js_string %>;
- what.form.zip.value = <% $cust_main->get($prefix.'zip') |js_string %>;
-
- changeSelect(what.form.country, <% $cust_main->get($prefix.'country') | js_string %> );
-
- country_changed( what.form.country,
- fix_state_factory( <% $cust_main->get($prefix.'state') | js_string %>,
- <% $cust_main->get($prefix.'county') | js_string %>
- )
- );
-% }
-
- } else {
- get_location( locationnum, update_location );
- }
+%# default service address is now just another location
+ get_location( locationnum, update_location );
% if ( $editable ) {
if ( locationnum == 0 ) {
@@ -203,14 +186,16 @@ Example:
ID = "locationnum"
onChange = "locationnum_changed(this);"
>
-% if ( !$prospect_main && !$opt{'disable_empty'} ) {
- <OPTION VALUE=""><% $opt{'empty_label'} || '(default service address)' |h %>
+% if ( $cust_main ) {
+ <OPTION VALUE="<% $cust_main->ship_locationnum %>"><% $opt{'empty_label'} || '(default service address)' |h %>
% }
% if ( $opt{'is_optional'} ) {
<OPTION VALUE="-2" <% $locationnum == -2 ? 'SELECTED' : ''%>><% $opt{'optional_label'} || '(not required)' |h %>
% }
%
% foreach my $loc ( @cust_location ) {
+% # don't show the ship_location redundantly
+% next if $cust_main && $cust_main->ship_locationnum == $loc->locationnum;
<OPTION VALUE="<% $loc->locationnum %>"
<% $locationnum == $loc->locationnum ? 'SELECTED' : '' %>
><% $loc->line |h %>
@@ -233,7 +218,9 @@ Example:
'alt_format' => $opt{'alt_format'},
)
%>
-
+<SCRIPT TYPE="text/javascript">
+ locationnum_changed(document.getElementById('locationnum'));
+</SCRIPT>
<%init>
my $conf = new FS::Conf;
@@ -246,8 +233,7 @@ my $cgi = $opt{'cgi'};
my $cust_pkg = $opt{'cust_pkg'};
my $cust_main = $opt{'cust_main'};
my $prospect_main = $opt{'prospect_main'};
-
-my $prefix = ($cust_main && length($cust_main->ship_last)) ? 'ship_' : '';
+die "cust_main or prospect_main required" unless $cust_main or $prospect_main;
my $locationnum = '';
if ( $cgi->param('error') ) {
@@ -259,9 +245,9 @@ if ( $cgi->param('error') ) {
} elsif ($prospect_main) {
my @cust_location = $prospect_main->cust_location;
$locationnum = $cust_location[0]->locationnum if scalar(@cust_location)==1;
- } else { #?
+ } else { #$cust_main
$cgi->param('locationnum') =~ /^(\-?\d*)$/ or die "illegal locationnum";
- $locationnum = $1;
+ $locationnum = $1 || $cust_main->ship_locationnum;
}
}
@@ -277,7 +263,7 @@ if ( $opt{'alt_format'} ) {
push @location_fields, qw( location_type location_number location_kind );
}
-my $cust_location;
+my $cust_location; #the one that shows by default in the location edit space
if ( $locationnum && $locationnum > 0 ) {
$cust_location = qsearchs('cust_location', { 'locationnum' => $locationnum } )
or die "unknown locationnum";
@@ -290,7 +276,7 @@ if ( $locationnum && $locationnum > 0 ) {
$cust_location->$_( $pkg_location->$_ ) foreach @location_fields;
$opt{'empty_label'} ||= 'package address: '.$pkg_location->line;
} elsif ( $cust_main ) {
- $cust_location->$_( $cust_main->get($prefix.$_) ) foreach @location_fields;
+ $cust_location = $cust_main->ship_location; #I think
}
}
@@ -311,14 +297,14 @@ push @cust_location, $cust_location
@cust_location = sort $location_sort grep !$_->disabled, @cust_location;
$cust_location = $cust_location[0]
- if ( $prospect_main || $opt{'disable_empty'} )
+ if ( $prospect_main )
&& !$opt{'is_optional'}
&& @cust_location;
my $disabled =
( $locationnum < 0
|| ( $editable && $locationnum )
- || ( ( $prospect_main || $opt{'disable_empty'} )
+ || ( $prospect_main
&& !$opt{'is_optional'} && !@cust_location && $addnew
)
)
diff --git a/httemplate/search/report_tax.cgi b/httemplate/search/report_tax.cgi
index 248f6c5..2786f57 100755
--- a/httemplate/search/report_tax.cgi
+++ b/httemplate/search/report_tax.cgi
@@ -239,6 +239,8 @@ as <A HREF="<% $p.'search/report_tax-xls.cgi?'.$cgi->query_string%>">Excel sprea
<%init>
+my $DEBUG = $cgi->param('debug') || 0;
+
die "access denied"
unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
@@ -252,15 +254,19 @@ my $join_cust = ' JOIN cust_bill USING ( invnum )
LEFT JOIN cust_main USING ( custnum ) ';
my $join_cust_pkg = $join_cust.
' LEFT JOIN cust_pkg USING ( pkgnum )
- LEFT JOIN part_pkg USING ( pkgpart ) ';
-$join_cust_pkg .= ' LEFT JOIN cust_location USING ( locationnum )'
- if $conf->exists('tax-pkg_address');
+ LEFT JOIN part_pkg USING ( pkgpart )
+ LEFT JOIN cust_location
+ ON ( cust_location.locationnum = ' .
+ FS::cust_pkg->tax_locationnum_sql . ' )';
my $from_join_cust_pkg = " FROM cust_bill_pkg $join_cust_pkg ";
my $where = "WHERE _date >= $beginning AND _date <= $ending ";
-my( $location_sql, @base_param ) = FS::cust_pkg->location_sql;
+# this query will be run once per cust_main_county,
+# or maybe once per country/state/city tuple,
+# or maybe once per country/state...it's hard to say.
+my ($location_sql, @base_param) = FS::cust_location->in_county_sql(param => 1);
$where .= " AND $location_sql ";
my $agentname = '';
@@ -291,59 +297,27 @@ sub gotcust {
";
}
-my $gotcust;
-if ( $conf->exists('tax-ship_address') ) {
-
- $gotcust = "
- ( cust_main_county.country = cust_main.country
- OR cust_main_county.country = cust_main.ship_country
- )
-
- AND
-
- (
- ( ( ship_last IS NULL OR ship_last = '' )
- AND ". gotcust('cust_main'). "
- )
- OR
- ( ship_last IS NOT NULL AND ship_last != ''
- AND ". gotcust('cust_main', 'ship_'). "
- )
- )
- ";
-
-} else {
-
- $gotcust = gotcust('cust_main');
-
-}
-if ( $conf->exists('tax-pkg_address') ) {
- $gotcust = "
- ( cust_pkg.locationnum IS NULL AND $gotcust)
- OR ( cust_pkg.locationnum IS NOT NULL AND ". gotcust('cust_location'). " )";
- $gotcust =
- "WHERE 0 < ( SELECT COUNT(*) FROM cust_pkg
- LEFT JOIN cust_main USING ( custnum )
- LEFT JOIN cust_location USING ( locationnum )
- WHERE $gotcust
- LIMIT 1
- )
- ";
-} else {
- $gotcust =
- "WHERE 0 < ( SELECT COUNT(*) FROM cust_main WHERE $gotcust LIMIT 1 )";
-}
+#non-parameterized form
+my $location_in_county = FS::cust_location->in_county_sql;
+my $gotcust = "WHERE EXISTS(
+ SELECT 1 FROM cust_location WHERE $location_in_county AND disabled IS NULL
+)";
my $out = 'Out of taxable region(s)';
# these are actually tax labels, not regions
my %regions = ();
+# Phase 1: Taxable and exempt sales
+# Collect for each cust_main_county, and assign to a bin based on label.
+# Note that "label" includes city if show_cities is on, and taxclass if
+# show_taxclasses is on.
foreach my $r ( qsearch({ 'table' => 'cust_main_county',
'extra_sql' => $gotcust,
+ 'debug' => $DEBUG,
})
)
{
- #warn $r->county. ' '. $r->state. ' '. $r->country. "\n";
+ warn $r->county. ' '. $r->state. ' '. $r->country. "\n" if $DEBUG > 1;
# set up a %regions entry for this region's tax label
my $label = getlabel($r);
@@ -475,7 +449,7 @@ foreach my $r ( qsearch({ 'table' => 'cust_main_county',
$regions{$label}->{'rate'} = $r->tax.'%';
}
}
-#warn Dumper(\%regions);
+warn Dumper(\%regions) if $DEBUG > 1;
# $regions{$label} now contains 'total', 'exempt_cust', 'exempt_pkg',
# 'exempt_monthly', summed over each set of regions with the same label.
@@ -491,29 +465,27 @@ my $taxclass_distinct =
)." AS taxclass";
+# Phase 2: invoiced/credited tax items
+# Collect this data for each country/state/city/district/taxname(/taxclass).
my %qsearch = (
'select' => "DISTINCT $distinct, $taxclass_distinct",
'table' => 'cust_main_county',
'hashref' => {},
'extra_sql' => $gotcust,
+ 'debug' => $DEBUG,
);
-my $taxfromwhere = " FROM cust_bill_pkg $join_cust ";
+# Join to cust_main the same as before (we need agentnum)
+# but not to cust_pkg (because tax line items don't have a package)
+# and then to cust_location via cust_bill_pkg_tax_location
+my $taxfromwhere = "FROM cust_bill_pkg $join_cust
+ LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum )
+ LEFT JOIN cust_location USING ( locationnum )
+ ";
my $taxwhere = $where;
-if ( $conf->exists('tax-pkg_address') ) {
-
- $taxfromwhere .= 'LEFT JOIN cust_bill_pkg_tax_location USING ( billpkgnum )
- LEFT JOIN cust_location USING ( locationnum ) ';
- #quelle kludge
- $taxwhere =~ s/cust_pkg\.locationnum/cust_bill_pkg_tax_location.locationnum/g;
-
-}
my $creditfromwhere = $taxfromwhere.
- " JOIN cust_credit_bill_pkg USING (billpkgnum";
-$creditfromwhere .= " ,billpkgtaxlocationnum"
- if $conf->exists('tax-pkg_address');
-$creditfromwhere .= ")";
+ " JOIN cust_credit_bill_pkg USING (billpkgnum, billpkgtaxlocationnum)";
$taxfromwhere .= " $taxwhere "; #AND payby != 'COMP' ";
$creditfromwhere .= " $taxwhere AND billpkgtaxratelocationnum IS NULL"; #AND payby != 'COMP' ";
@@ -611,6 +583,10 @@ foreach my $r ( qsearch(\%qsearch) ) {
}
+# Phase 3: Non-taxclassed totals for invoiced/credited tax
+# (If show_taxclasses is not in use, this was phase 2, but it
+# displays somewhere different.)
+# Don't filter by report_groups.
my %base_regions = ();
if ( $cgi->param('show_taxclasses') ) {
diff --git a/httemplate/view/cust_main/contacts.html b/httemplate/view/cust_main/contacts.html
index fe7cc5c..6213f27 100644
--- a/httemplate/view/cust_main/contacts.html
+++ b/httemplate/view/cust_main/contacts.html
@@ -1,122 +1,128 @@
-% my %which = (
-% '' => emt('Billing'),
-% 'ship_' => emt('Service'),
-% );
-% foreach my $which ( '', 'ship_' ) {
-% my $pre = $cust_main->get("${which}last") ? $which : '';
-
-<FONT CLASS="fsinnerbox-title"><% $which{$which} %> <% mt('address') |h %></FONT>
+% my %addr_label = ('bill' => 'Billing address', 'ship' => 'Service address');
+
+%# Locations (possibly break this out)
+% my @which = ('bill');
+% push @which, 'ship' if $cust_main->has_ship_address;
+% while (@which) {
+% my $this = shift @which;
+% my $method = $this.'_location';
+% my $location = $cust_main->$method;
+<FONT CLASS="fsinnerbox-title"><% mt( $addr_label{$this} ) |h %></FONT>
<TABLE CLASS="fsinnerbox">
-<TR>
- <TD ALIGN="right"><% mt('Contact name') |h %></TD>
- <TD COLSPAN=5 BGCOLOR="#ffffff">
- <% $cust_main->get("${pre}last"). ', '. $cust_main->get("${pre}first") |h %>
- </TD>
-% if ( $which eq '' && $conf->exists('show_ss') ) {
- <TD ALIGN="right"><% mt('SS#') |h %></TD>
- <TD BGCOLOR="#ffffff"><% $conf->exists('unmask_ss') ? $cust_main->ss : $cust_main->masked('ss') || '&nbsp' %></TD>
-% }
-</TR>
-% if ( $conf->exists('cust-email-high-visibility') && $which eq '') {
+% if ( $this eq 'bill' ) {
+% #billing contact fields
+ <TR>
+ <TD ALIGN="right"><% mt('Contact name') |h %></TD>
+ <TD COLSPAN=5 BGCOLOR="#ffffff"><% $cust_main->contact |h %></TD>
+% if ( $conf->exists('show_ss') ) {
+ <TD ALIGN="right"><% mt('SS#') |h %></TD>
+ <TD BGCOLOR="#ffffff"><% $conf->exists('unmask_ss')
+ ? $cust_main->ss
+ : $cust_main->masked('ss') || '&nbsp;' %></TD>
+% }
+ </TR>
+% if ( $conf->exists('cust-email-high-visibility') ) {
<TR>
<TD ALIGN="right"><% mt('Email address(es)') |h %></TD>
<TD BGCOLOR="#ffff00">
- <% join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ) || $no %>
+ <% $cust_main->invoicing_list_emailonly_scalar || $no %>
</TD>
</TR>
-% }
-
-% if ( $cust_main->get("${pre}company") ) {
+% }
+% if ( $cust_main->company ) {
<TR>
<TD ALIGN="right"><% mt('Company') |h %></TD>
- <TD COLSPAN=7 BGCOLOR="#ffffff"><% $cust_main->get("${pre}company") |h %></TD>
+ <TD COLSPAN=7 BGCOLOR="#ffffff"><% $cust_main->company %></TD>
</TR>
-% }
-
+% }
+% } # if $this eq 'bill'
+% # now the actual address
<TR>
<TD ALIGN="right"><% mt('Address') |h %></TD>
- <TD COLSPAN=7 BGCOLOR="#ffffff"><% $cust_main->get("${pre}address1") |h %></TD>
+ <TD COLSPAN=7 BGCOLOR="#ffffff"><% $location->address1 |h %></TD>
</TR>
-% if ( $cust_main->get("${pre}address2") ) {
-% my $address2_label =
-% ( $conf->exists('cust_main-require_address2')
-% && ! ( $pre xor $cust_main->has_ship_address )
-% )
-% ? emt('Unit #')
-% : ' ';
+% if ( $location->get('address2') ) {
+% my $address2_label = $conf->exists('cust_main-require_address2')
+% ? emt('Unit #')
+% : ' ';
- <TR>
- <TD ALIGN="right"><% $address2_label %></TD>
- <TD COLSPAN=7 BGCOLOR="#ffffff"><% $cust_main->get("${pre}address2") |h %></TD>
- </TR>
+<TR>
+ <TD ALIGN="right"><% $address2_label %></TD>
+ <TD COLSPAN=7 BGCOLOR="#ffffff"><% $location->address2 |h %></TD>
+</TR>
% }
<TR>
<TD ALIGN="right"><% mt('City') |h %></TD>
- <TD BGCOLOR="#ffffff"><% $cust_main->get("${pre}city") |h %></TD>
-% if ( $cust_main->get("${pre}county") ) {
+ <TD BGCOLOR="#ffffff"><% $location->city |h %></TD>
+% if ( $location->county ) {
<TD ALIGN="right"><% mt('County') |h %></TD>
- <TD BGCOLOR="#ffffff"><% $cust_main->get("${pre}county") |h %></TD>
+ <TD BGCOLOR="#ffffff"><% $location->county |h %></TD>
% }
<TD ALIGN="right"><% mt('State') |h %></TD>
- <TD BGCOLOR="#ffffff"><% state_label( $cust_main->get("${pre}state"), $cust_main->get("${pre}country") ) |h %></TD>
+ <TD BGCOLOR="#ffffff"><% state_label( $location->state, $location->country ) |h %></TD>
<TD ALIGN="right"><% mt('Zip') |h %></TD>
- <TD BGCOLOR="#ffffff"><% $cust_main->get("${pre}zip") %></TD>
+ <TD BGCOLOR="#ffffff"><% $location->zip %></TD>
</TR>
<TR>
<TD ALIGN="right"><% mt('Country') |h %></TD>
- <TD BGCOLOR="#ffffff"><% code2country( $cust_main->get("${pre}country") ) %></TD>
+ <TD BGCOLOR="#ffffff"><% code2country( $location->country ) %></TD>
</TR>
-% if ( $cust_main->get($pre.'latitude') && $cust_main->get($pre.'longitude') ) {
- <& /elements/tr-coords.html, $cust_main->get($pre.'latitude'),
- $cust_main->get($pre.'longitude'),
+% if ( $location->latitude && $location->longitude ) {
+ <& /elements/tr-coords.html, $location->latitude,
+ $location->longitude,
$cust_main->name_short,
$cust_main->agentnum,
&>
% }
+
+% if ( $this eq 'bill' ) {
+% # billing contact phone numbers
+% foreach my $phone (qw(daytime night mobile)) {
+% next if !$cust_main->get($phone);
+<TR>
+ <TD ALIGN="right"><% $phone_label{$phone} %></TD>
+ <TD COLSPAN=3 BGCOLOR="#ffffff">
+ <& /elements/phonenumber.html,
+ $cust_main->get($phone),
+ callable => 1,
+ calling_list_exempt => $cust_main->calling_list_exempt,
+ &>
+ </TD>
+</TR>
-% foreach my $phone (grep $cust_main->get($pre.$_), qw( daytime night mobile )){
-
- <TR>
- <TD ALIGN="right"><% $phone_label{$phone} %></TD>
- <TD COLSPAN=3 BGCOLOR="#ffffff">
- <& /elements/phonenumber.html,
- $cust_main->get($pre.$phone),
- 'callable'=>1,
- 'calling_list_exempt'=>$cust_main->calling_list_exempt,
- &>
- </TD>
- </TR>
-
-% }
+% } #foreach $phone
+% if ( $cust_main->get('fax') ) {
-% if ( $cust_main->get("${pre}fax") ) {
<TR>
<TD ALIGN="right"><% mt('Fax') |h %></TD>
<TD COLSPAN=3 BGCOLOR="#ffffff">
- <% $cust_main->get("${pre}fax") || '&nbsp' %>
+ <% $cust_main->get('fax') || '&nbsp;' %>
</TD>
</TR>
-% }
-% if ( $which eq '' && $conf->exists('show_stateid') ) {
- <TR>
+% }
+%
+% if ( $conf->exists('show_stateid') ) {
+
+<TR>
<TD ALIGN="right"><% $stateid_label %></TD>
<TD BGCOLOR="#ffffff"><% $cust_main->masked('stateid') || '&nbsp' %></TD>
<TD ALIGN="right"><% $stateid_state_label %></TD>
<TD BGCOLOR="#ffffff"><% $cust_main->stateid_state || '&nbsp' %></TD>
</TR>
-% }
+% }
+% } #if $this eq 'bill'
</TABLE>
-% if ( $which ne 'ship_' ) {
+% if ( @which ) {
<BR>
% }
-% }
+% } #while @which
<%once>
my %phone_label = (
@@ -147,7 +153,7 @@ my $stateid_state_label = FS::Msgcat::_gettext('stateid_state') =~ /^(stateid_st
</%once>
<%init>
-my( $cust_main ) = @_;
+my $cust_main = shift;
my $conf = new FS::Conf;
my @invoicing_list = $cust_main->invoicing_list;
my $no = emt('no');
diff --git a/httemplate/view/cust_main/locations.html b/httemplate/view/cust_main/locations.html
index 98c9336..b29d0ce 100755
--- a/httemplate/view/cust_main/locations.html
+++ b/httemplate/view/cust_main/locations.html
@@ -5,12 +5,17 @@ span.loclabel {
background-color: #cccccc;
border: 1px solid black
}
+table.location {
+ width: 100%;
+ padding: 1px;
+ border-spacing: 0px;
+}
</STYLE>
% foreach my $locationnum (@sorted) {
% my $packages = $packages_in{$locationnum};
% my $loc = $locations{$locationnum};
% next if $loc->disabled and scalar(@$packages) == 0;
-<& /elements/table-grid.html &>
+<TABLE CLASS="grid location">
<TR><TH COLSPAN=3 ALIGN="left" VALIGN="bottom"
STYLE="padding-bottom: 0px;
padding-left: 0px;
@@ -18,10 +23,7 @@ STYLE="padding-bottom: 0px;
border-bottom-color: black;
border-bottom-width: 1px;">
<SPAN CLASS="loclabel">
-% if (! $locationnum) {
-<% mt('Default service location:') |h %>
-% }
-% elsif ( $loc->disabled ) {
+% if ( $loc->disabled ) {
<FONT COLOR="#808080"><I>
% }
<% $loc->location_label %></SPAN>
@@ -49,8 +51,7 @@ my %locations = map { $_->locationnum => $_ } qsearch({
'order_by' => 'ORDER BY country, state, city, address1, locationnum',
});
my @sections = keys %locations;
-$locations{''} = $cust_main;
-my %packages_in = map { $_ => [] } ('', @sections);
+my %packages_in = map { $_ => [] } (@sections);
my %active = (); # groups with non-canceled packages
foreach my $cust_pkg ( @$all_packages ) {
@@ -58,10 +59,13 @@ foreach my $cust_pkg ( @$all_packages ) {
push @{ $packages_in{$key} }, $cust_pkg;
$active{$key} = 1 if !$cust_pkg->getfield('cancel');
}
+# prevent disabling these
+$active{$cust_main->ship_locationnum} = 1;
+$active{$cust_main->bill_locationnum} = 1;
my @sorted = (
- '',
- grep ( { $active{$_} } @sections),
+ $cust_main->ship_locationnum,
+ grep ( { $active{$_} && $_ != $cust_main->ship_locationnum } @sections),
grep ( { !$active{$_} } @sections),
);
diff --git a/httemplate/view/cust_main/misc.html b/httemplate/view/cust_main/misc.html
index 12faa57..a0ab403 100644
--- a/httemplate/view/cust_main/misc.html
+++ b/httemplate/view/cust_main/misc.html
@@ -134,9 +134,9 @@
<TR>
<TD ALIGN="right">
- <% mt('Census tract ([_1])', $cust_main->censusyear) |h %>
+ <% mt('Census tract ([_1])', $cust_main->ship_location->censusyear) |h %>
</TD>
- <TD BGCOLOR="#ffffff"><% $cust_main->censustract %></TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->ship_location->censustract %></TD>
</TR>
% }
@@ -145,7 +145,7 @@
<TR>
<TD ALIGN="right"><% mt('Tax district') |h %></TD>
- <TD BGCOLOR="#ffffff"><% $cust_main->district %></TD>
+ <TD BGCOLOR="#ffffff"><% $cust_main->ship_location->district %></TD>
</TR>
% }