From: Mark Wells Date: Tue, 29 Apr 2014 06:29:03 +0000 (-0700) Subject: make cust_main location upgrade non-blocking, and make the system somewhat functional... X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=59958b6c8ec8418cf30d679a9afea478bab1f366 make cust_main location upgrade non-blocking, and make the system somewhat functional before the upgrade is complete, #28883 --- diff --git a/FS/FS/Cursor.pm b/FS/FS/Cursor.pm index 469a678d5..d94151fed 100644 --- a/FS/FS/Cursor.pm +++ b/FS/FS/Cursor.pm @@ -2,10 +2,11 @@ package FS::Cursor; use strict; use vars qw($DEBUG $buffer); -use FS::Record qw(dbh); +use FS::Record; +use FS::UID qw(myconnect); use Scalar::Util qw(refaddr); -$DEBUG = 0; +$DEBUG = 2; # this might become a parameter at some point, but right now, you can # "local $FS::Cursor::buffer = X;" @@ -38,11 +39,13 @@ and returns an FS::Cursor object to fetch the rows one at a time. sub new { my $class = shift; my $q = FS::Record::_query(@_); # builds the statement and parameter list + my $dbh = myconnect(); my $self = { query => $q, class => 'FS::' . ($q->{table} || 'Record'), buffer => [], + dbh => $dbh, }; bless $self, $class; @@ -55,8 +58,8 @@ sub new { $self->{id} = sprintf('cursor%08x', refaddr($self)); my $statement = "DECLARE ".$self->{id}." CURSOR FOR ".$q->{statement}; - my $sth = dbh->prepare($statement) - or die dbh->errstr; + my $sth = $dbh->prepare($statement) + or die $dbh->errstr; my $bind = 1; foreach my $value ( @{ $q->{value} } ) { my $bind_type = shift @{ $q->{bind_type} }; @@ -65,7 +68,7 @@ sub new { $sth->execute or die $sth->errstr; - $self->{fetch} = dbh->prepare("FETCH FORWARD $buffer FROM ".$self->{id}); + $self->{fetch} = $dbh->prepare("FETCH FORWARD $buffer FROM ".$self->{id}); $self; } @@ -105,7 +108,10 @@ sub refill { sub DESTROY { my $self = shift; return unless $self->{pid} eq $$; - dbh->do('CLOSE '. $self->{id}) or die dbh->errstr; # clean-up the cursor in Pg + $self->{dbh}->do('CLOSE '. $self->{id}) + or die $self->{dbh}->errstr; # clean-up the cursor in Pg + $self->{dbh}->rollback; + $self->{dbh}->disconnect; } =back diff --git a/FS/FS/cust_bill_pkg.pm b/FS/FS/cust_bill_pkg.pm index ef9c01a32..034601db8 100644 --- a/FS/FS/cust_bill_pkg.pm +++ b/FS/FS/cust_bill_pkg.pm @@ -970,7 +970,13 @@ sub tax_locationnum { sub tax_location { my $self = shift; - FS::cust_location->by_key($self->tax_locationnum); + if ( $self->pkgnum ) { # normal sales + return $self->cust_pkg->tax_location; + } elsif ( $self->feepart ) { # fees + return $self->cust_bill->cust_main->ship_location; + } else { # taxes + return; + } } =item part_X @@ -1576,6 +1582,14 @@ sub _upgrade_data { }); # call it kind of like a class method, not that it matters much $job->insert($class, 's' => str2time('2012-01-01')); + # if there's a customer location upgrade queued also, wait for it to + # finish + my $location_job = qsearchs('queue', { + job => 'FS::cust_main::Location::process_upgrade_location' + }); + if ( $location_job ) { + $job->depend_insert($location_job->jobnum); + } # Then mark the upgrade as done, so that we don't queue the job twice # and somehow run two of them concurrently. FS::upgrade_journal->set_done($upgrade); diff --git a/FS/FS/cust_main/Location.pm b/FS/FS/cust_main/Location.pm index 9899f7230..560736d4f 100644 --- a/FS/FS/cust_main/Location.pm +++ b/FS/FS/cust_main/Location.pm @@ -70,7 +70,11 @@ Returns an L object for the customer's billing address. sub bill_location { my $self = shift; $self->hashref->{bill_location} - ||= FS::cust_location->by_key($self->bill_locationnum); + ||= FS::cust_location->by_key($self->bill_locationnum) + # degraded mode--let the system keep running during upgrades + || FS::cust_location->new({ + map { $_ => $self->get($_) } @location_fields + }) } =item ship_location @@ -82,7 +86,11 @@ Returns an L object for the customer's service address. sub ship_location { my $self = shift; $self->hashref->{ship_location} - ||= FS::cust_location->by_key($self->ship_locationnum); + ||= FS::cust_location->by_key($self->ship_locationnum) + || FS::cust_location->new({ + map { $_ => $self->get('ship_'.$_) || $self->get($_) } @location_fields + }) + } =item location TYPE @@ -127,11 +135,6 @@ sub _upgrade_data { local $DEBUG = 0; my $error; - my $tax_prefix = 'bill_'; - if ( FS::Conf->new->exists('tax-ship_address') ) { - $tax_prefix = 'ship_'; - } - # Step 0: set up contact classes and phone types my $service_contact_class = qsearchs('contact_class', { classname => 'Service'}) @@ -160,147 +163,19 @@ sub _upgrade_data { } } - warn "Migrating customer locations.\n"; - my $search = FS::Cursor->new('cust_main', - { bill_locationnum => '', - address1 => { op=>'!=', value=>'' } - }); - while (my $cust_main = $search->fetch) { - # 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(), - } - ); - $bill_location->set('censustract', ''); - $bill_location->set('censusyear', ''); - # properly goes with ship_location; if they're the same, will be set - # on ship_location before inserting either one - my $ship_location = $bill_location; # until proven otherwise - - if ( $cust_main->get('ship_address1') ) { - # detect duplicates - my $same = 1; - foreach (location_fields()) { - if ( length($cust_main->get("ship_$_")) and - $cust_main->get($_) ne $cust_main->get("ship_$_") ) { - $same = 0; - } - } - - if ( !$same ) { - $ship_location = FS::cust_location->new( - { - custnum => $custnum, - map { $_ => $cust_main->get("ship_$_") } location_fields() - } - ); - } # else it stays equal to $bill_location - - # 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 ( !$cust_main->get('ship_last') or !$cust_main->get('ship_first') ) - { - warn "customer $custnum has no service contact name; substituting ". - "customer name\n"; - $contact->set('last' => $cust_main->get('last')); - $contact->set('first' => $cust_main->get('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 - - # special case: should go with whichever location is used to calculate - # taxes, because that's the one it originally came from - if ( my $geocode = $cust_main->get('geocode') ) { - $bill_location->set('geocode' => ''); - $ship_location->set('geocode' => ''); - - if ( $tax_prefix eq 'bill_' ) { - $bill_location->set('geocode', $geocode); - } elsif ( $tax_prefix eq 'ship_' ) { - $ship_location->set('geocode', $geocode); - } + my $num_to_upgrade = FS::cust_main->count('bill_locationnum is null or ship_locationnum is null'); + my $num_jobs = FS::queue->count('job = \'FS::cust_main::Location::process_upgrade_location\' and status != \'failed\''); + if ( $num_to_upgrade > 0 ) { + warn "Need to migrate $num_to_upgrade customer locations.\n"; + if ( $num_jobs > 0 ) { + warn "Upgrade already queued.\n"; + } else { + warn "Scheduling upgrade.\n"; + my $job = FS::queue->new({ job => 'FS::cust_main::Location::process_upgrade_location' }); + $job->insert; } - # this always goes with the ship_location (whether it's the same as - # bill_location or not) - $ship_location->set('censustract', $cust_main->get('censustract')); - $ship_location->set('censusyear', $cust_main->get('censusyear')); - - $error = $bill_location->insert; - die "error migrating billing address for customer $custnum: $error" - if $error; - - $cust_main->set(bill_locationnum => $bill_location->locationnum); - - if (!$ship_location->locationnum) { - $error = $ship_location->insert; - die "error migrating service address for customer $custnum: $error" - if $error; - } - - $cust_main->set(ship_locationnum => $ship_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 - my $pkg_search = - FS::Cursor->new('cust_pkg', { custnum => $custnum, locationnum => '' }); - while (my $cust_pkg = $pkg_search->fetch) { - # 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; - } - - } #while (my $cust_main...) + } # repair an error in earlier upgrades if (!FS::upgrade_journal->is_done('cust_location_censustract_repair') @@ -332,9 +207,198 @@ sub _upgrade_data { } # foreach $cust_location FS::upgrade_journal->set_done('cust_location_censustract_repair'); } +} + +sub process_upgrade_location { + my $class = shift; + + my $dbh = dbh; + local $FS::cust_location::import = 1; + local $FS::UID::AutoCommit = 0; + + my $tax_prefix = 'bill_'; + if ( FS::Conf->new->exists('tax-ship_address') ) { + $tax_prefix = 'ship_'; + } + + # load some records that were created during the initial upgrade + my $service_contact_class = + qsearchs('contact_class', { classname => 'Service'}); + + my %phone_type = ( + daytime => 'Work', + night => 'Home', + mobile => 'Mobile', + fax => 'Fax' + ); + foreach (keys %phone_type) { + $phone_type{$_} = qsearchs('phone_type', { typename => $phone_type{$_}}); + } + my %opt = ( + tax_prefix => $tax_prefix, + service_contact_class => $service_contact_class, + phone_type => \%phone_type, + ); + + my $search = FS::Cursor->new('cust_main', + { bill_locationnum => '', + address1 => { op=>'!=', value=>'' } + }); + while (my $cust_main = $search->fetch) { + my $error = $cust_main->upgrade_location(%opt); + if ( $error ) { + warn "cust#".$cust_main->custnum.": $error\n"; + $dbh->rollback; + } else { + # commit as we go + $dbh->commit; + } + } } +sub upgrade_location { # instance method + my $cust_main = shift; + my %opt = @_; + my $error; + + # 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(), + } + ); + $bill_location->set('censustract', ''); + $bill_location->set('censusyear', ''); + # properly goes with ship_location; if they're the same, will be set + # on ship_location before inserting either one + my $ship_location = $bill_location; # until proven otherwise + + if ( $cust_main->get('ship_address1') ) { + # detect duplicates + my $same = 1; + foreach (location_fields()) { + if ( length($cust_main->get("ship_$_")) and + $cust_main->get($_) ne $cust_main->get("ship_$_") ) { + $same = 0; + } + } + + if ( !$same ) { + $ship_location = FS::cust_location->new( + { + custnum => $custnum, + map { $_ => $cust_main->get("ship_$_") } location_fields() + } + ); + } # else it stays equal to $bill_location + + # 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' => $opt{service_contact_class}->classnum, + 'locationnum' => $ship_location->locationnum, + 'last' => $cust_main->get('ship_last'), + 'first' => $cust_main->get('ship_first'), + }); + if ( !$cust_main->get('ship_last') or !$cust_main->get('ship_first') ) + { + warn "customer $custnum has no service contact name; substituting ". + "customer name\n"; + $contact->set('last' => $cust_main->get('last')); + $contact->set('first' => $cust_main->get('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; + return "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' => $opt{phone_type}->{$_}->phonetypenum, + FS::contact::_parse_phonestring( $phone ) + }); + $error = $contact_phone->insert; + return "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 + + # special case: should go with whichever location is used to calculate + # taxes, because that's the one it originally came from + if ( my $geocode = $cust_main->get('geocode') ) { + $bill_location->set('geocode' => ''); + $ship_location->set('geocode' => ''); + + if ( $opt{tax_prefix} eq 'bill_' ) { + $bill_location->set('geocode', $geocode); + } elsif ( $opt{tax_prefix} eq 'ship_' ) { + $ship_location->set('geocode', $geocode); + } + } + + # this always goes with the ship_location (whether it's the same as + # bill_location or not) + $ship_location->set('censustract', $cust_main->get('censustract')); + $ship_location->set('censusyear', $cust_main->get('censusyear')); + + $error = $bill_location->insert; + return "error migrating billing address for customer $custnum: $error" + if $error; + + $cust_main->set(bill_locationnum => $bill_location->locationnum); + + if (!$ship_location->locationnum) { + $error = $ship_location->insert; + return "error migrating service address for customer $custnum: $error" + if $error; + } + + $cust_main->set(ship_locationnum => $ship_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; + return "error migrating addresses for customer $custnum: $error" + if $error; + + # Step 4: set packages at the "default service location" to ship_location + my $pkg_search = + FS::Cursor->new('cust_pkg', { custnum => $custnum, locationnum => '' }); + while (my $cust_pkg = $pkg_search->fetch) { + # not a location change + $cust_pkg->set('locationnum', $cust_main->ship_locationnum); + $error = $cust_pkg->replace; + return "error migrating package ".$cust_pkg->pkgnum.": $error" + if $error; + } + ''; + +} + + =back =cut diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index d546e555d..cf9e3244f 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -3416,7 +3416,16 @@ Returns the L object for tax_locationnum. sub tax_location { my $self = shift; - FS::cust_location->by_key( $self->tax_locationnum ) + my $conf = FS::Conf->new; + if ( $conf->exists('tax-pkg_address') and $self->locationnum ) { + return FS::cust_location->by_key($self->locationnum); + } + elsif ( $conf->exists('tax-ship_address') ) { + return $self->cust_main->ship_location; + } + else { + return $self->cust_main->bill_location; + } } =item seconds_since TIMESTAMP