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