customer bill/ship location refactoring, #940
authorMark Wells <mark@freeside.biz>
Fri, 25 May 2012 20:38:07 +0000 (13:38 -0700)
committerMark Wells <mark@freeside.biz>
Fri, 25 May 2012 20:38:18 +0000 (13:38 -0700)
35 files changed:
FS/FS/ClientAPI/MasonComponent.pm
FS/FS/ClientAPI/MyAccount.pm
FS/FS/ClientAPI/Signup.pm
FS/FS/Schema.pm
FS/FS/UI/Web/small_custview.pm
FS/FS/cust_bill.pm
FS/FS/cust_location.pm
FS/FS/cust_main.pm
FS/FS/cust_main/Billing.pm
FS/FS/cust_main/Location.pm [new file with mode: 0644]
FS/FS/cust_main/Packages.pm
FS/FS/cust_main/Search.pm
FS/FS/cust_pkg.pm
FS/FS/msg_template.pm
httemplate/edit/cust_main.cgi
httemplate/edit/cust_main/after_bill_location.html [new file with mode: 0644]
httemplate/edit/cust_main/before_bill_location.html [new file with mode: 0644]
httemplate/edit/cust_main/birthdate.html
httemplate/edit/cust_main/bottomfixup.js
httemplate/edit/cust_main/company.html [new file with mode: 0644]
httemplate/edit/cust_main/fax.html [new file with mode: 0644]
httemplate/edit/cust_main/name.html [new file with mode: 0644]
httemplate/edit/cust_main/phones.html [new file with mode: 0644]
httemplate/edit/cust_main/stateid.html [new file with mode: 0644]
httemplate/edit/cust_main/top_misc.html
httemplate/edit/msg_template.html
httemplate/edit/process/cust_location.cgi
httemplate/edit/process/cust_main.cgi
httemplate/elements/location.html
httemplate/elements/standardize_locations.js
httemplate/elements/tr-select-cust_location.html
httemplate/search/report_tax.cgi
httemplate/view/cust_main/contacts.html
httemplate/view/cust_main/locations.html
httemplate/view/cust_main/misc.html

index 37cf7ef..534b48a 100644 (file)
@@ -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
   },
index e79fbfc..54799b8 100644 (file)
@@ -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->{$_} )
index f17752a..b7dcdbb 100644 (file)
@@ -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'} }
index 2968903..5476589 100644 (file)
@@ -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' ]
                        ],
     },
 
index 53a3b5e..2c42a6b 100644 (file)
@@ -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>';
 
index 1f4943a..d1cb3ba 100644 (file)
@@ -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 );
   
index bcdb50c..1f07aa8 100644 (file)
@@ -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
 
index 9766579..56338e5 100644 (file)
@@ -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
index ca8d996..339fa44 100644 (file)
@@ -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 (file)
index 0000000..d1d6d67
--- /dev/null
@@ -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;
index 06331d3..887ac49 100644 (file)
@@ -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 );
 
index b663c20..ca4d167 100644 (file)
@@ -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;
index 788b1d3..6899fa4 100644 (file)
@@ -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
 
index 62bcebc..c3e781a 100644 (file)
@@ -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) } ],
index 0289246..a30c7c1 100755 (executable)
@@ -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
   <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 (file)
index 0000000..2f4c3b5
--- /dev/null
@@ -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 (file)
index 0000000..973201e
--- /dev/null
@@ -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>
index 6d1c221..5d6a123 100644 (file)
@@ -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',
index 800864b..77d4294 100644 (file)
@@ -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 (file)
index 0000000..8a6ed0b
--- /dev/null
@@ -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 (file)
index 0000000..237d4be
--- /dev/null
@@ -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 (file)
index 0000000..2641ec9
--- /dev/null
@@ -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 (file)
index 0000000..9b23e07
--- /dev/null
@@ -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 (file)
index 0000000..2655f51
--- /dev/null
@@ -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>
index 7ba167b..7ce283c 100644 (file)
        <% $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>
index f50d66d..115032a 100644 (file)
@@ -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',
index 790fc8e..b9f93db 100644 (file)
@@ -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);
 
index 3f5e19e..5ee553b 100755 (executable)
@@ -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,
                           );
index c606523..7672318 100644 (file)
@@ -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,
index e6a4aa6..86f8d2b 100644 (file)
@@ -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,
index 0ca255b..05712ee 100644 (file)
@@ -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
        )
   )
index 248f6c5..2786f57 100755 (executable)
@@ -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') ) {
 
index fe7cc5c..6213f27 100644 (file)
-% 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');
index 98c9336..b29d0ce 100755 (executable)
@@ -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),
 );
 
index 12faa57..a0ab403 100644 (file)
 
   <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>
 
 % }
 
   <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>
 
 % }