From: Llewellyn Marshall Date: Wed, 11 Jan 2023 21:28:43 +0000 (-0500) Subject: geosort X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=351d401cb14114071f44fa60869652d1001588c4;p=working%2FEvergreen.git geosort merge vincinity stuff into geo app, revert changes to admin page component, fix syntax error in statement to create SQL add call to geo from actor like how the retrieve coordinates works get distance divisor from config global flags and divide shipping distance by it to get copy score. --- diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 609a596628..db8c27dee3 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -4339,6 +4339,26 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + + + + + + + + + + + + + + + + + + + @@ -5765,6 +5785,7 @@ SELECT usr, + @@ -7244,6 +7265,7 @@ SELECT usr, + @@ -7273,6 +7295,7 @@ SELECT usr, + diff --git a/Open-ILS/examples/opensrf.xml.example b/Open-ILS/examples/opensrf.xml.example index 05d945fdeb..eeec62f664 100644 --- a/Open-ILS/examples/opensrf.xml.example +++ b/Open-ILS/examples/opensrf.xml.example @@ -1470,6 +1470,7 @@ vim:et:ts=4:sw=4: open-ils.vandelay open-ils.serial open-ils.hold-targeter + open-ils.vicinity-calculator open-ils.ebook_api open-ils.courses open-ils.curbside diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html index c6a4108031..1c161ec424 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html @@ -79,6 +79,8 @@ routerLink="/staff/admin/server/config/marc_field"> + + + + +
+ Entries in this table contain the distance in kilometers between shipping hub locations. If configured for your service, driving distance will be used instead of as-the-crow-flies. Shipping hubs are configured from the Orginizational Units page. These numbers are used for sorting hold targets during inter-library lending. Entries can be created manually or calculated using your geolocation service. Recalculating the distances will clear out any existing data.. +
+ + +
+
+ + ` +}) + +export class OrgUnitShippingHubDistanceComponent implements OnInit { + + idlClass: string; + classLabel: string; + persistKeyPfx: string; + readonlyFields = ''; + configLinkBasePath = '/staff/admin'; + + // API is currently calculating + calculating : boolean; + // Tell the admin page to disable and hide the automagic org unit filter + disableOrgFilter: boolean; + + constructor( + private route: ActivatedRoute, + private idl: IdlService, + private net: NetService, + private auth: AuthService + ) { + } + + ngOnInit() { + let schema = this.route.snapshot.paramMap.get('schema'); + if (!schema) { + // Allow callers to pass the schema via static route data + const data = this.route.snapshot.data[0]; + if (data) { schema = data.schema; } + } + let table = this.route.snapshot.paramMap.get('table'); + if (!table) { + const data = this.route.snapshot.data[0]; + if (data) { table = data.table; } + } + const fullTable = schema + '.' + table; + + // Set the prefix to "server", "local", "workstation", + // extracted from the URL path. + // For admin pages that use none of these, avoid setting + // the prefix because that will cause it to double-up. + // e.g. eg.grid.acq.acq.cancel_reason + this.persistKeyPfx = this.route.snapshot.parent.url[0].path; + const selfPrefixers = ['acq', 'booking']; + if (selfPrefixers.indexOf(this.persistKeyPfx) > -1) { + // ACQ is a special case, because unlike 'server', 'local', + // 'workstation', the schema ('acq') is the root of the path. + this.persistKeyPfx = ''; + } else { + this.configLinkBasePath += '/' + this.persistKeyPfx; + } + + // Pass the readonlyFields param if available + if (this.route.snapshot.data && this.route.snapshot.data[0]) { + // snapshot.data is a HASH. + const data = this.route.snapshot.data[0]; + + if (data.readonlyFields) { + this.readonlyFields = data.readonlyFields; + } + + if (data.disableOrgFilter) { + this.disableOrgFilter = true; + } + } + + Object.keys(this.idl.classes).forEach(class_ => { + const classDef = this.idl.classes[class_]; + if (classDef.table === fullTable) { + this.idlClass = class_; + this.classLabel = classDef.label; + } + }); + + if (!this.idlClass) { + throw new Error('Unable to find IDL class for table ' + fullTable); + } + this.calculating = false; + } + + calculateDistances(){ + this.calculating = true; + this.net.request( + 'open-ils.actor', + 'open-ils.actor.geo.build_distance_matrix', + this.auth.token() + ).subscribe( + n => {this.calculating = false; location.reload();}, + err => {alert('API failed to calculate ' + err);this.calculating = false;} + ); + } +} + + diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts index caadbcb897..309270b441 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts @@ -2,6 +2,7 @@ import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; import {AdminServerSplashComponent} from './admin-server-splash.component'; import {BasicAdminPageComponent} from '@eg/staff/admin/basic-admin-page.component'; +import {OrgUnitShippingHubDistanceComponent} from './org-unit-shipping-hub-distance.component'; import {OrgUnitTypeComponent} from './org-unit-type.component'; import {PrintTemplateComponent} from './print-template.component'; import {PermGroupTreeComponent} from './perm-group-tree.component'; @@ -60,6 +61,11 @@ const routes: Routes = [{ data: [{schema: 'actor', table: 'org_unit_proximity_adjustment', disableOrgFilter: true}] }, { + path: 'actor/org_unit_shipping_hub_distance', + component: OrgUnitShippingHubDistanceComponent, + data: [{schema: 'actor', + table: 'org_unit_shipping_hub_distance', readonlyFields: 'id'}] +}, { path: 'asset/call_number_prefix', component: BasicAdminPageComponent, data: [{schema: 'asset', diff --git a/Open-ILS/src/extras/install/Makefile.debian-bullseye b/Open-ILS/src/extras/install/Makefile.debian-bullseye index 0cc634d8fb..03d727ba77 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-bullseye +++ b/Open-ILS/src/extras/install/Makefile.debian-bullseye @@ -98,6 +98,7 @@ export DEB_APACHE_DISCONF = \ export CPAN_MODULES = \ Geo::Coder::Google \ + Geo::Coder::Bing \ Business::OnlinePayment::PayPal \ String::KeyboardDistance \ Text::Levenshtein::Damerau::XS \ diff --git a/Open-ILS/src/extras/install/Makefile.debian-buster b/Open-ILS/src/extras/install/Makefile.debian-buster index 57fae8496c..873543b850 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-buster +++ b/Open-ILS/src/extras/install/Makefile.debian-buster @@ -99,6 +99,7 @@ export DEB_APACHE_DISCONF = \ export CPAN_MODULES = \ Geo::Coder::Google \ + Geo::Coder::Bing \ Business::OnlinePayment::PayPal \ String::KeyboardDistance \ Text::Levenshtein::Damerau::XS \ diff --git a/Open-ILS/src/extras/install/Makefile.debian-stretch b/Open-ILS/src/extras/install/Makefile.debian-stretch index d242aa64cc..a8d5fddf27 100644 --- a/Open-ILS/src/extras/install/Makefile.debian-stretch +++ b/Open-ILS/src/extras/install/Makefile.debian-stretch @@ -98,6 +98,7 @@ export DEB_APACHE_DISCONF = \ export CPAN_MODULES = \ Geo::Coder::Google \ + Geo::Coder::Bing \ Business::OnlinePayment::PayPal \ String::KeyboardDistance \ Text::Levenshtein::Damerau::XS \ diff --git a/Open-ILS/src/extras/install/Makefile.fedora b/Open-ILS/src/extras/install/Makefile.fedora index 907bd99b85..2d1bb9900b 100644 --- a/Open-ILS/src/extras/install/Makefile.fedora +++ b/Open-ILS/src/extras/install/Makefile.fedora @@ -74,6 +74,7 @@ FEDORA_RPMS = \ export CPAN_MODULES = \ Geo::Coder::OSM \ Geo::Coder::Google \ + Geo::Coder::Bing \ Excel::Writer::XLSX \ String::KeyboardDistance \ Text::Levenshtein::Damerau::XS \ diff --git a/Open-ILS/src/extras/install/Makefile.ubuntu-bionic b/Open-ILS/src/extras/install/Makefile.ubuntu-bionic index 11a2ff5d46..2d479e58d3 100644 --- a/Open-ILS/src/extras/install/Makefile.ubuntu-bionic +++ b/Open-ILS/src/extras/install/Makefile.ubuntu-bionic @@ -94,6 +94,7 @@ export DEB_APACHE_DISCONF = \ export CPAN_MODULES = \ Geo::Coder::Google \ + Geo::Coder::Bing \ Business::OnlinePayment::PayPal \ Email::Send \ MARC::Charset \ diff --git a/Open-ILS/src/extras/install/Makefile.ubuntu-focal b/Open-ILS/src/extras/install/Makefile.ubuntu-focal index 74badd855b..804cdfcaa5 100644 --- a/Open-ILS/src/extras/install/Makefile.ubuntu-focal +++ b/Open-ILS/src/extras/install/Makefile.ubuntu-focal @@ -94,6 +94,7 @@ export DEB_APACHE_DISCONF = \ export CPAN_MODULES = \ Geo::Coder::Google \ + Geo::Coder::Bing \ Business::OnlinePayment::PayPal \ Email::Send \ MARC::Charset \ diff --git a/Open-ILS/src/perlmods/Build.PL b/Open-ILS/src/perlmods/Build.PL index 5c323085b9..9ca2161e4c 100644 --- a/Open-ILS/src/perlmods/Build.PL +++ b/Open-ILS/src/perlmods/Build.PL @@ -40,6 +40,7 @@ my $build = Module::Build->new( 'File::Spec' => '0', 'File::stat' => '0', 'File::Temp' => '0', + 'Geo::Coder::Bing' => '0', 'Getopt::Long' => '0', 'IO::Scalar' => '0', 'List::Util' => '0', @@ -87,6 +88,7 @@ my $build = Module::Build->new( 'Unicode::Normalize' => '0', 'UNIVERSAL::require' => '0', 'UUID::Tiny' => '0', + 'WWW::REST' => '0', 'XML::LibXML' => '0', 'XML::LibXML::XPathContext' => '0', 'XML::LibXSLT' => '0', diff --git a/Open-ILS/src/perlmods/MANIFEST b/Open-ILS/src/perlmods/MANIFEST index 37fb0903da..83970b43c3 100644 --- a/Open-ILS/src/perlmods/MANIFEST +++ b/Open-ILS/src/perlmods/MANIFEST @@ -153,6 +153,7 @@ lib/OpenILS/Utils/Normalize.pm lib/OpenILS/Utils/OfflineStore.pm lib/OpenILS/Utils/Penalty.pm lib/OpenILS/Utils/PermitHold.pm +lib/OpenILS/Utils/VicinityCalculator.pm lib/OpenILS/Utils/RemoteAccount.pm lib/OpenILS/Utils/ZClient.pm lib/OpenILS/WWW/AddedContent.pm diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm index c0d9851f02..8f1030e352 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm @@ -1508,6 +1508,27 @@ sub retrieve_coordinates { } __PACKAGE__->register_method( + method => "build_distance_matrix", + api_name => "open-ils.actor.geo.build_distance_matrix", + signature => { + params => [ + {desc => 'Authentication token', type => 'string' } + ] + } +); + +sub build_distance_matrix { + my( $self, $client, $auth) = @_; + my $e = new_editor(authtoken=>$auth); + return $e->event unless $e->checkauth; + + return $apputils->simplereq( + "open-ils.geo", + "open-ils.geo.build_distance_matrix", + $auth ); +} + +__PACKAGE__->register_method( method => "get_my_org_ancestor_at_depth", api_name => "open-ils.actor.org_unit.ancestor_at_depth.retrieve" ); diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Geo.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Geo.pm index 3f735e5547..a6988201e9 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Geo.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Geo.pm @@ -6,12 +6,17 @@ use warnings; use OpenSRF::AppSession; use OpenILS::Application; use base qw/OpenILS::Application/; +use List::MoreUtils qw(natatime); +use List::Util qw(min); use OpenSRF::Utils::SettingsClient; use OpenILS::Utils::CStoreEditor qw/:funcs/; use OpenILS::Utils::Fieldmapper; use OpenSRF::Utils::Cache; +use OpenILS::Utils::VicinityCalculator; use OpenILS::Application::AppUtils; +use Data::Dumper; +use JSON::XS; my $U = "OpenILS::Application::AppUtils"; use OpenSRF::Utils::Logger qw/$logger/; @@ -23,6 +28,7 @@ my $have_geocoder_free = eval { }; use Geo::Coder::OSM; use Geo::Coder::Google; +use Geo::Coder::Bing; use Math::Trig qw(great_circle_distance deg2rad); use Digest::SHA qw(sha256_base64); @@ -40,6 +46,61 @@ sub child_init { $cache = OpenSRF::Utils::Cache->new('global'); } +sub calculate_driving_distance { + my ($self, $conn, $auth, $pointA, $pointB) = @_; + + return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing coordinates") unless $pointA; + return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing coordinates") unless $pointB; + return new OpenILS::Event("BAD_PARAMS", "desc" => "Malformed coordinates") unless scalar(@{ $pointA }) == 2; + return new OpenILS::Event("BAD_PARAMS", "desc" => "Malformed coordinates") unless scalar(@{ $pointB }) == 2; + + my $e = new_editor(xact => 1, authtoken=>$auth); + return $e->die_event unless $e->checkauth; + # get the requestor's org unit + my $org = $e->requestor->ws_ou; + my $use_geo = $e->retrieve_config_global_flag('opac.use_geolocation'); + $use_geo = ($use_geo and $U->is_true($use_geo->enabled)); + return new OpenILS::Event("GEOCODING_NOT_ENABLED") unless ($U->is_true($use_geo)); + + return new OpenILS::Event("BAD_PARAMS", "desc" => "No org ID supplied") unless $org; + my $service_id = $U->ou_ancestor_setting_value($org, 'opac.geographic_location_service_for_address'); + return new OpenILS::Event("GEOCODING_NOT_ALLOWED") unless ($U->is_true($service_id)); + + my $service = $e->retrieve_config_geolocation_service($service_id); + return new OpenILS::Event("GEOCODING_NOT_ALLOWED") unless ($U->is_true($service)); + + my $geo_coder = _create_geocoder($service); + if (!$geo_coder) { + return OpenILS::Event->new('GEOCODING_LOCATION_NOT_FOUND'); + } + + if ($service->service_code eq 'Bing') { + my $origin_coord = join(',',@{ $pointA }); + my $dest_coord = join(',',@{ $pointB }); + my $uri = URI->new("https://dev.virtualearth.net/REST/v1/Routes/DistanceMatrix?origins=$origin_coord&destinations=$dest_coord&distanceUnit=km&travelMode=driving&key=".$service->api_key); + my $results = $geo_coder->_rest_request($uri)->{results}; + return $results->[0]->{travelDistance}; + } else { + $logger->info($service->service_code." can not get driving distance. Reverting to as-the-crow-flies."); + # if geocoder can't do driving distance just get as-the-crow-flies + return calculate_distance($self, $conn, $pointA, $pointB); + } + return 0; +} + +__PACKAGE__->register_method( + method => "calculate_driving_distance", + api_name => "open-ils.geo.calculate_driving_distance", + signature => { + params => [ + {type => 'string', desc => 'User\'s authorization token'}, + {type => 'array', desc => 'An array containing latitude and longitude for point A'}, + {type => 'array', desc => 'An array containing latitude and longitude for point B'} + ], + return => { desc => 'Driving distance between points A and B in kilometers'} + } +); + sub calculate_distance { my ($self, $conn, $pointA, $pointB) = @_; @@ -67,6 +128,222 @@ __PACKAGE__->register_method( } ); +sub _post_request { + my ($bing, $uri, $form, $json_coder) = @_; + my $json = $json_coder->encode($form); + return unless $uri; + #$logger->info($uri); + #$logger->info($form); + my $res = $bing->{response} = $bing->ua->post($uri,'Content-Length' => 3500,'Content-Type' => 'application/json',Content => $json); + unless($res->is_success){ + $logger->error("API ERROR\n"); + my @error = split /\n/, $res->decoded_content; + foreach(@error){ + $logger->error($_); + } + return; + } + # Change the content type of the response from 'application/json' so + # HTTP::Message will decode the character encoding. + $res->content_type('text/plain'); + + my $content = $res->decoded_content; + return unless $content; + my $data= eval { $json_coder->decode($res->decoded_content) }; + return unless $data; + my @results = @{ $data->{resourceSets}[0]{resources} || [] }; + return wantarray ? @results : $results[0]; +} + +sub calculate_bulk_driving_distance { + my ($self, $conn, $auth, $origin_array, $destination_array) = @_; + + return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing coordinates") unless $origin_array; + return new OpenILS::Event("BAD_PARAMS", "desc" => "Missing coordinates") unless $destination_array; + + my $e = new_editor(xact => 1, authtoken=>$auth); + return $e->die_event unless $e->checkauth; + # get the requestor's org unit + my $org = $e->requestor->ws_ou; + my $use_geo = $e->retrieve_config_global_flag('opac.use_geolocation'); + $use_geo = ($use_geo and $U->is_true($use_geo->enabled)); + return new OpenILS::Event("GEOCODING_NOT_ENABLED") unless ($U->is_true($use_geo)); + + return new OpenILS::Event("BAD_PARAMS", "desc" => "No org ID supplied") unless $org; + my $service_id = $U->ou_ancestor_setting_value($org, 'opac.geographic_location_service_for_address'); + return new OpenILS::Event("GEOCODING_NOT_ALLOWED") unless ($U->is_true($service_id)); + + my $service = $e->retrieve_config_geolocation_service($service_id); + return new OpenILS::Event("GEOCODING_NOT_ALLOWED") unless ($U->is_true($service)); + + my $geo_coder = _create_geocoder($service); + if (!$geo_coder) { + return OpenILS::Event->new('GEOCODING_LOCATION_NOT_FOUND'); + } + + my @results; + + if ($service->service_code eq 'Bing') { + my @origins; + my @destinations; + my $uri = URI->new("https://dev.virtualearth.net/REST/v1/Routes/DistanceMatrix?key=".$service->api_key); + + # get the data into the right form for our request + foreach(@{$origin_array}){ + my %ocoord; + $ocoord{'latitude'} = $_->[0]; + $ocoord{'longitude'} = $_->[1]; + push(@origins, \%ocoord); + } + $logger->info(Dumper(\@origins)); + + foreach(@{$destination_array}){ + my %dcoord; + $dcoord{'latitude'} = $_->[0]; + $dcoord{'longitude'} = $_->[1]; + push(@destinations, \%dcoord); + } + + # find out how many coords we can process per request. + my $budget = int(2500 / scalar(@origins)); + $logger->debug("data being chunked into ".$budget.".\n"); + my $it = natatime $budget, @origins; + my $real_index = 0; + my $json_coder = JSON::XS->new->convert_blessed; + + while (my @coords = $it->()) + { + my %content = ( + origins => \@coords, + destinations => \@destinations, + travelMode => "driving", + timeUnit => "minute", + distanceUnit => "km" + ); + $logger->info("Hash for JSON: ".Dumper(\%content)); + my $rest_req = _post_request($geo_coder,$uri,\%content,$json_coder); + + #calculate the distance matrix for this chunk of origins. + $logger->info("results from post: ".Dumper($rest_req)); + my @distance_matrix = eval{$rest_req->{results}}; + if(@distance_matrix){ + for my $ref (@distance_matrix) { + for (@$ref){ + my %dist; + $dist{origin} = $_->{originIndex} + $real_index; + $dist{destination} = $_->{destinationIndex}; + $dist{distance} = $_->{travelDistance}; + push(@results,\%dist); + } + } + + $logger->info("Information from server: ".Dumper(\@results)); + $real_index += min($budget,scalar(@coords)); + print("setting index to ".$real_index."\n"); + } + } + + return \@results; + } else { + $logger->info($service->service_code." can not get driving distance. Reverting to as-the-crow-flies."); + # if geocoder can't do driving distance just get as-the-crow-flies + my $index = 0; + foreach(@{$origin_array}){ + my $pointA = $_; + my $dindex = 0; + foreach(@{$destination_array}){ + my $pointB = $_; + my $d = calculate_distance($self, $conn, $pointA, $pointB); + my %dist; + $dist{origin} = $index; + $dist{destination} = $dindex; + $dist{distance} = $d; + push(@results,\%dist); + $dindex++; + } + $index++; + } + + return \@results; + } + return \@results; +} + +__PACKAGE__->register_method( + method => "calculate_bulk_driving_distance", + api_name => "open-ils.geo.calculate_bulk_driving_distance", + signature => { + params => [ + {type => 'string', desc => 'User\'s authorization token'}, + {type => 'array', desc => 'An array containing latitude and longitude origin points as an array.'}, + {type => 'array', desc => 'An array containing latitude and longitude destination points as an array.'} + ], + return => { desc => 'Driving distance between origin points and destinations in kilometers'} + } +); + +__PACKAGE__->register_method( + method => 'build_distance_matrix', + api_name => 'open-ils.geo.build_distance_matrix', + signature => { + desc => q/Batch calculation of shipping hub distance matrix./, + return => {desc => 'See API Options for return types'} + } +); + +sub build_distance_matrix{ + my ($self, $conn, $auth) = @_; + my $calculator = OpenILS::Utils::VicinityCalculator->new($auth); + $calculator->calculate_distance_matrix(); + return 1; +} + + +__PACKAGE__->register_method( + method => 'get_all_hubs', + api_name => 'open-ils.geo.shipping-hubs.retrieve', + signature => { + desc => q/Retrieve a list of all shipping hubs/, + } +); + +sub get_all_hubs{ + my ($self, $conn, $auth) = @_; + my $calculator = OpenILS::Utils::VicinityCalculator->new(); + $logger->info("retreiving org unit shipping hubs"); + return $calculator->get_all_hubs(); +} + +__PACKAGE__->register_method( + method => 'get_hub_from_ou', + api_name => 'open-ils.geo.shipping-hub.retrieve', + signature => { + desc => q/Retrieve a shipping hub from a given OU/, + } +); + +sub get_hub_from_ou{ + my ($self, $org_unit) = @_; + my $calculator = OpenILS::Utils::VicinityCalculator::Matrix->new(); + $logger->info("retreiving org unit shipping hubs"); + return $calculator->get_hub_from_ou($org_unit); +} + +__PACKAGE__->register_method( + method => 'get_distance_between_shipping_hubs', + api_name => 'open-ils.geo.shipping-hubs.distance', + signature => { + desc => q/Retrieve the distance between two shipping hubs/, + } +); + +sub get_distance_between_shipping_hubs { + my ($self, $origin_hub, $dest_hub) = @_; + my $calculator = OpenILS::Utils::VicinityCalculator::Matrix->new(); + $logger->info("calculating org unit shipping hub distances"); + return $calculator->distance_between_hubs($origin_hub,$dest_hub); +} + sub sort_orgs_by_distance_from_coordinate { my ($self, $conn, $pointA, $orgs) = @_; @@ -131,6 +408,35 @@ __PACKAGE__->register_method( } ); +# creates a Geo::Coder object +sub _create_geocoder { + my $service = shift; + my $service_id = $service->id; + my $geo_coder; + eval { + if ($service->service_code eq 'Free') { + if ($have_geocoder_free) { + $logger->debug("Using Geo::Coder::Free (service id $service_id)"); + $geo_coder = Geo::Coder::Free->new(); + } else { + $logger->error("geosort: Geo::Coder::Free not installed but referenced."); + } + } elsif ($service->service_code eq 'Google') { + $logger->debug("Using Geo::Coder::Google (service id $service_id)"); + $geo_coder = Geo::Coder::Google->new(key => $service->api_key); + } elsif ($service->service_code eq 'Bing') { + $logger->debug("Using Geo::Coder::Bing (service id $service_id)"); + $geo_coder = Geo::Coder::Bing->new(key => $service->api_key); + } else { + $logger->debug("Using Geo::Coder::OSM (service id $service_id)"); + $geo_coder = Geo::Coder::OSM->new(); + } + }; + if ($@ || !$geo_coder) { + $logger->error("geosort: problem creating Geo::Coder instance : $@"); + } + return $geo_coder; +} sub retrieve_coordinates { # invoke 3rd party API for latitude/longitude lookup my ($self, $conn, $org, $address) = @_; @@ -165,26 +471,8 @@ sub retrieve_coordinates { # invoke 3rd party API for latitude/longitude lookup my $coords = OpenSRF::Utils::JSON->JSON2perl($cache->get_cache($cache_key)); return $coords if $coords; - my $geo_coder; - eval { - if ($service->service_code eq 'Free') { - if ($have_geocoder_free) { - $logger->debug("Using Geo::Coder::Free (service id $service_id)"); - $geo_coder = Geo::Coder::Free->new(); - } else { - $logger->error("geosort: Geo::Coder::Free not installed but referenced."); - return OpenILS::Event->new('GEOCODING_LOCATION_NOT_FOUND'); - } - } elsif ($service->service_code eq 'Google') { - $logger->debug("Using Geo::Coder::Google (service id $service_id)"); - $geo_coder = Geo::Coder::Google->new(key => $service->api_key); - } else { - $logger->debug("Using Geo::Coder::OSM (service id $service_id)"); - $geo_coder = Geo::Coder::OSM->new(); - } - }; - if ($@ || !$geo_coder) { - $logger->error("geosort: problem creating Geo::Coder instance : $@"); + my $geo_coder = _create_geocoder($service); + if (!$geo_coder) { return OpenILS::Event->new('GEOCODING_LOCATION_NOT_FOUND'); } my $location; @@ -204,6 +492,9 @@ sub retrieve_coordinates { # invoke 3rd party API for latitude/longitude lookup } elsif ($service->service_code eq 'Google') { $latitude = $location->{'geometry'}->{'location'}->{'lat'}; $longitude = $location->{'geometry'}->{'location'}->{'lng'}; + } elsif ($service->service_code eq 'Bing') { + $latitude = $location->{point}{coordinates}[0]; + $longitude = $location->{point}{coordinates}[1]; } else { $latitude = $location->{lat}; $longitude = $location->{lon}; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/actor.pm index 6bf0481662..19c42a0433 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/actor.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/actor.pm @@ -113,6 +113,24 @@ __PACKAGE__->columns( Essential => qw/org_unit name value/); #------------------------------------------------------------------------------- +package actor::org_unit_shipping_hub; +use base qw/actor/; + +__PACKAGE__->table( 'actor_org_unit_shipping_hub' ); +__PACKAGE__->columns( Primary => qw/id/); +__PACKAGE__->columns( Essential => qw/org_unit hub/); + + +#------------------------------------------------------------------------------- +package actor::org_unit_shipping_hub_distance; +use base qw/actor/; + +__PACKAGE__->table( 'actor_org_unit_shipping_hub_distance' ); +__PACKAGE__->columns( Primary => qw/id/); +__PACKAGE__->columns( Essential => qw/orig_hub dest_hub distance/); + + +#------------------------------------------------------------------------------- package actor::stat_cat; use base qw/actor/; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/dbi.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/dbi.pm index 0d2b49ccd5..62aaed025b 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/dbi.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/dbi.pm @@ -460,6 +460,12 @@ actor::org_unit::closed_date->sequence( 'actor.org_unit_closed_id_seq' ); #--------------------------------------------------------------------- + package actor::org_unit_shipping_hub; + + actor::org_unit_shipping_hub->table( 'actor.org_unit_shipping_hub' ); + actor::org_unit_shipping_hub->sequence( 'actor.org_unit_shipping_hub_id_seq' ); + + #--------------------------------------------------------------------- package actor::org_unit_setting; actor::org_unit_setting->table( 'actor.org_unit_setting' ); diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm b/Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm index 31b8852cb0..4d2c3b19af 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm @@ -16,6 +16,7 @@ package OpenILS::Utils::HoldTargeter; use strict; use warnings; use DateTime; +use Data::Dumper; use OpenSRF::AppSession; use OpenSRF::Utils::Logger qw(:logger); use OpenSRF::Utils::JSON; @@ -262,11 +263,14 @@ package OpenILS::Utils::HoldTargeter::Single; use strict; use warnings; use DateTime; +use Data::Dumper; +use OpenILS::Utils::VicinityCalculator; use OpenSRF::AppSession; use OpenILS::Utils::DateTime qw/:datetime/; use OpenSRF::Utils::Logger qw(:logger); use OpenILS::Application::AppUtils; use OpenILS::Utils::CStoreEditor qw/:funcs/; +use List::Util qw(shuffle max); sub new { my ($class, %args) = @_; @@ -298,6 +302,22 @@ sub hold { return $self->{hold}; } +# return the number by which shipping distances are divided +# during hold copy scoring. Defaults to 10. +sub distance_divisor { + my ($self) = @_; + if (!defined($self->{distance_divisor}) || !$self->{distance_divisor}) { + my $dd = $self->editor->search_config_global_flag({ + name => 'circ.holds.distance_divisor', + enabled => 't' + })->[0]; + + # If no flag is present, default to 10.0 + $self->{distance_divisor} = $dd ? $dd->value : 10.0; + } + return $self->{distance_divisor}; +} + sub inside_hard_stall_interval { my ($self) = @_; if (defined $self->{inside_hard_stall_interval}) { @@ -738,13 +758,16 @@ sub compile_weighted_proximity_map { # Collect copy proximity info (generated via DB trigger) # from our newly create copy maps. my $hold_copy_maps = $self->editor->json_query({ - select => {ahcm => ['target_copy', 'proximity']}, + select => {ahcm => ['target_copy', 'proximity','shipping_distance']}, from => 'ahcm', where => {hold => $self->hold_id} }); my %copy_prox_map = map {$_->{target_copy} => $_->{proximity}} @$hold_copy_maps; + + my %copy_dist_map = + map {$_->{target_copy} => $_->{shipping_distance}} @$hold_copy_maps; # Pre-fetch the org setting value for all circ libs so that # later calls can reference the cached value. @@ -753,8 +776,11 @@ sub compile_weighted_proximity_map { my %prox_map; for my $copy_hash (@{$self->copies}) { - my $prox = $copy_prox_map{$copy_hash->{id}}; + my $copy_id = $copy_hash->{id}; + my $prox = $copy_prox_map{$copy_id}; + my $shipping_distance = $copy_dist_map{$copy_id}; $copy_hash->{proximity} = $prox; + $copy_hash->{shipping_distance} = $shipping_distance; $prox_map{$prox} ||= []; my $weight = $self->parent->get_ou_setting( @@ -1147,12 +1173,18 @@ sub attempt_prev_copy_retarget { return undef; } -# Returns the closest copy by proximity that is a confirmed valid +# Returns the closest copy by proximity and shipping distance that is a confirmed valid # targetable copy. sub find_nearest_copy { my $self = shift; my %prox_map = %{$self->{weighted_prox_map}}; my $hold = $self->hold; + my $req_hub; + my $geo_sort = $self->editor->retrieve_config_global_flag('opac.use_geolocation'); + my $geo_sort_for_holds = $self->parent->get_ou_setting( + $hold->pickup_lib, + 'circ.holds.target_sort_by_geographic_proximity', $self->editor); + my $do_geosort = $geo_sort && $U->is_true($geo_sort_for_holds); my %seen; # See if there are in-use (targeted) copies "here". @@ -1170,7 +1202,7 @@ sub find_nearest_copy { # copy is found that is suitable for targeting. my $no_copies = 1; for my $prox (sort {$a <=> $b} keys %prox_map) { - my @copies = @{$prox_map{$prox}}; + my @copies = shuffle(@{$prox_map{$prox}}); next unless @copies; $no_copies = 0; @@ -1188,17 +1220,37 @@ sub find_nearest_copy { ); last if ($prox > 0); # No point in looking further "out". } + + + if($do_geosort){ + # sort all of the copies by shipping hub distance. + # two copies with equal shipping distance are sorted at random. + # pick copy with the lowest score + for my $c (sort { $self->score_copy($a) <=> $self->score_copy($b) } @copies){ + next if $seen{$c->{id}}; + + return $c if $self->copy_is_permitted($c); + $seen{$c->{id}} = 1; + + last unless(@copies); + } + } + else{ + # Pick a copy at random from each tier of the proximity map, + # starting at the lowest proximity and working up, until a + # copy is found that is suitable for targeting. + my $rand = int(rand(scalar(@copies))); - my $rand = int(rand(scalar(@copies))); + while (my ($c) = splice(@copies, $rand, 1)) { + $rand = int(rand(scalar(@copies))); + next if $seen{$c->{id}}; - while (my ($c) = splice(@copies, $rand, 1)) { - $rand = int(rand(scalar(@copies))); - next if $seen{$c->{id}}; + return $c if $self->copy_is_permitted($c); + $seen{$c->{id}} = 1; - return $c if $self->copy_is_permitted($c); - $seen{$c->{id}} = 1; + last unless(@copies); - last unless(@copies); + } } } @@ -1213,6 +1265,19 @@ sub find_nearest_copy { return undef; } +# calculates a value for the copy based on the shipping distance +# shipping distance is divided by the distance_divisor and +# decimal part is removed. This provides for cases where +# shipping distance between two branches are negiligibly far apart. +# golf rules, larger scores make less valuable targets. +sub score_copy { + my ($self, $copy) = @_; + return 0 unless $copy; + my $distance_divisor = $self->distance_divisor(); + my $shipping_distance = $copy->{shipping_distance}; + return int($shipping_distance/$distance_divisor); +} + # Returns true if the provided copy passes the hold permit test for our # hold and can be used for targeting. # When a copy fails the test, it is removed from $self->copies. diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Utils/VicinityCalculator.pm b/Open-ILS/src/perlmods/lib/OpenILS/Utils/VicinityCalculator.pm new file mode 100644 index 0000000000..d4d46a4b53 --- /dev/null +++ b/Open-ILS/src/perlmods/lib/OpenILS/Utils/VicinityCalculator.pm @@ -0,0 +1,250 @@ +package OpenILS::Utils::VicinityCalculator; +use strict; use warnings; +use Geo::Coder::Bing; +use JSON; +use Data::Dumper; +use URI; +use OpenSRF::System; +use OpenILS::Application::Actor; +use OpenSRF::Utils::Logger qw($logger); +use OpenSRF::AppSession; +use OpenILS::Utils::Fieldmapper; +use OpenSRF::Utils::SettingsClient; +use OpenILS::Application::AppUtils; +use OpenILS::Utils::CStoreEditor qw/:funcs/; + +our $U = "OpenILS::Application::AppUtils"; +my $actor; + +sub new { + my ($class, $auth) = @_; + my $self = { + editor => new_editor(authtoken => $auth), + auth => $auth, + hub_cache => {}, + coord_cache => {}, + + }; + $self->{editor}->init; + return bless($self, $class); +} + +sub uniq { + my %seen; + grep !$seen{$_}++, @_; +} + +# Use Bing maps API to calculate the distances between all shipping hubs +sub calculate_distance_matrix { + my $self = shift; + # find hubs for all OUs + my @hubs = $self->get_all_hubs(); + # find addresses of all hub OUs + $logger->info("Getting shipping hub addresses"); + my %hub_coord = $self->get_coord_from_ou(uniq(@hubs)); + my @origins = values(%hub_coord); + my @destinations = values(%hub_coord); + my @hub_ids = keys(%hub_coord); + # make one giant request to our geolocation service to calculate our distance matrix + my $geo = OpenSRF::AppSession->create('open-ils.geo'); + my $geo_request = $geo->request('open-ils.geo.calculate_bulk_driving_distance', + $self->{auth}, \@origins, \@destinations); + my $result = $geo_request->recv(); + my $content = $result->content(); + $logger->info("content from API: ".Dumper($content)); + my @distance_matrix = @{$content}; + if(@distance_matrix){ + $self->{editor}->xact_begin; + # clear out existing matrix + $logger->info("Old distance matrix is being cleared out."); + $self->clear_hub_distances(); + foreach(@distance_matrix) { + print(Dumper $_); + # create our AOUSHD objects for the data returned + my $dist = Fieldmapper::actor::org_unit_shipping_hub_distance->new; + $dist->orig_hub($hub_ids[$_->{origin}]); + $dist->dest_hub($hub_ids[$_->{destination}]); + $dist->distance($_->{distance}); + # place AOUSHD into the DB + $self->{editor}->runmethod('create', 'actor.org_unit_shipping_hub_distance', 'aoushd', $dist); + + } + # commit to DB + $self->{editor}->xact_commit; + } + else{ + $logger->error("API failed to calculate distance matrix"); + } +} + +sub get_coord_from_ou { + my($self,@org_ids) = @_; + my @ma = $self->{editor}->json_query({ + select => { + 'aoa' => ['org_unit','latitude','longitude','address_type'] + }, + from => {'aou' => {'aoa' => {'field' => 'id', 'fkey' => 'mailing_address'}}}, + where => {'id' => [@org_ids]} + }); + my %coords; + + for my $ref (@ma) { + for (@$ref){ + $coords{$_->{org_unit}} = [$_->{latitude}, $_->{longitude}]; + } + } + return %coords; +} + +# remove all existing distance calculations. +# TODO make this all happen in one query +# what could the analog to DELETE FROM TABLE be? +sub clear_hub_distances { + my($self,@org_ids) = @_; + my @ma = $self->{editor}->json_query({ + select => { + aoushd => [ + { + column => 'id', + } + ] + }, + from => 'aoushd' + }); + + for my $ref (@ma) { + for (@$ref){ + my $dist = Fieldmapper::actor::org_unit_shipping_hub_distance->new; + $dist->id($_->{id}); + $self->{editor}->runmethod('delete', 'actor.org_unit_shipping_hub_distance', 'aoushd', $dist); + } + } +} + +sub get_all_hubs { +my($self) = @_; +my @sh = $self->{editor}->json_query({ + select => { + aou => ['shipping_hub_ou'], + }, + from => 'aou' + }); + my @hubs; + for my $ref (@sh) { + for (@$ref){ + my $hub = $_->{shipping_hub_ou}; + if($hub && $hub != 0 && !($hub eq '')){ + push @hubs, $hub; + } + } + } + return @hubs; +} + +package OpenILS::Utils::VicinityCalculator::Matrix; +use OpenSRF::System; +use OpenILS::Application::Actor; +use OpenSRF::Utils::Logger qw(:logger); +use OpenSRF::AppSession; +use OpenILS::Utils::Fieldmapper; +use OpenSRF::Utils::SettingsClient; +use OpenILS::Application::AppUtils; +use OpenILS::Utils::CStoreEditor qw/:funcs/; +use Data::Dumper; + +our $U = "OpenILS::Application::AppUtils"; +sub new { + my ($class) = @_; + my $self = { editor => new_editor() }; + $self->{editor}->init; + return bless($self, $class); +} + +sub hub_matrix { + my ($self, $origin_hub, $dest_hubs_ref) = @_; + my @dest_hubs = @{$dest_hubs_ref}; + my @d = $self->{editor}->json_query({ + select => {'aoushd' => [{column => 'dest_hub'},{column => 'distance'}]}, + from => 'aoushd', + where => {'orig_hub'=>[$origin_hub],'dest_hub'=>[@dest_hubs]}, + order_by => [ + {class => 'aoushd', field => 'distance', direction => 'ASC'}, + ] + }); + + my %matrix; + for my $ref (@d) { + for (@$ref){ + $matrix{$_->{'dest_hub'}}=$_->{distance}; + } + } + # hub matrix will be undefined if any destination hubs are missing from the return list. + for my $hub (@dest_hubs){ + next if $matrix{$hub}; + $logger->error("OU $origin_hub has no calculation to OU $hub. open-ils.geo.build-distance-matrix must be run before vicinity based hold targeting can continue!"); + return undef; + } + return %matrix; +} + +sub distance_between_hubs { + my ($self, $origin_hub, $dest_hub) = @_; + my @d = $self->{editor}->json_query({ + select => {'aoushd' => [{column => 'distance'}]}, + from => 'aoushd', + where => {'orig_hub'=>[$origin_hub],'dest_hub'=>[$dest_hub]} + }); + for my $ref (@d) { + for (@$ref){ + return $_->{distance}; + } + } + $logger->error("OU $origin_hub has no calculation to OU $dest_hub. open-ils.geo.build-distance-matrix must be run!"); + return undef; +} + +sub get_target_hubs{ + my $self = shift; + my $copies_ref = shift; + my @target_copies = @{ $copies_ref }; + my @h = $self->{editor}->json_query({ + select => {'acp' => ['id','circ_lib']}, + from => 'acp', + where => {'+acp'=>{id => [@target_copies]}} + }); + my %circ_libs; + for my $ref (@h) { + for (@$ref){ + $circ_libs{$_->{id}} = $_->{circ_lib}; + } + } + + my %circ_hubs; + my %hubs; + my @sh = $self->{editor}->json_query({ + select => [{column=>'org_unit'},{column=>'hub'}], + from => [ + 'actor.list_org_unit_ancestor_shipping_hub',values(%circ_libs)] + }); + for my $ref (@sh) { + for (@$ref){ + $circ_hubs{$_->{org_unit}} = $_->{hub}; + } + } + foreach my $copy(@target_copies){ + $hubs{$copy} = $circ_hubs{$circ_libs{$copy}}; + } + + return %hubs; +} + +sub get_hub_from_ou { +my($self,@org_ids) = @_; +my @sh = $self->{editor}->json_query({ + select => [{column=>'org_unit'},{column=>'hub'}], + from => [ + 'actor.list_org_unit_ancestor_shipping_hub',@org_ids] + }); + return $sh[0][0]->{'hub'}; +} +1; \ No newline at end of file diff --git a/Open-ILS/src/sql/Pg/005.schema.actors.sql b/Open-ILS/src/sql/Pg/005.schema.actors.sql index b18a33cf6f..a26bd3bf7a 100644 --- a/Open-ILS/src/sql/Pg/005.schema.actors.sql +++ b/Open-ILS/src/sql/Pg/005.schema.actors.sql @@ -1310,4 +1310,41 @@ CREATE TABLE actor.usr_privacy_waiver ( ); CREATE INDEX actor_usr_privacy_waiver_usr_idx ON actor.usr_privacy_waiver (usr); +ALTER TABLE actor.org_unit +ADD COLUMN shipping_hub_ou BIGINT REFERENCES actor.org_unit(id) ON DELETE SET NULL; + +CREATE OR REPLACE FUNCTION actor.list_org_unit_ancestor_shipping_hub(VARIADIC orgs NUMERIC[]) RETURNS TABLE(org_unit INT,hub INT) + AS +$func$ +DECLARE + rec record; + cur_org INT; + next_hub INT; + org_id INT; +BEGIN + FOREACH org_id IN ARRAY orgs LOOP + cur_org := org_id; + org_unit := cur_org; + LOOP + SELECT INTO next_hub actor.org_unit.shipping_hub_ou FROM actor.org_unit WHERE actor.org_unit.id = cur_org; + IF FOUND AND next_hub IS NOT NULL THEN + hub := next_hub; + return next; + EXIT; + END IF; + SELECT INTO cur_org parent_ou FROM actor.org_unit WHERE actor.org_unit.id = cur_org; + EXIT WHEN cur_org IS NULL; + END LOOP; + END LOOP; + RETURN; +END; +$func$ LANGUAGE PLPGSQL; + +CREATE TABLE actor.org_unit_shipping_hub_distance ( + id SERIAL PRIMARY KEY, + orig_hub BIGINT NOT NULL REFERENCES actor.org_unit(id) ON DELETE CASCADE DEFERRABLE, + dest_hub BIGINT NOT NULL REFERENCES actor.org_unit(id) ON DELETE CASCADE DEFERRABLE, + distance INT NOT NULL +); + COMMIT; diff --git a/Open-ILS/src/sql/Pg/090.schema.action.sql b/Open-ILS/src/sql/Pg/090.schema.action.sql index 711269e245..ccfc246ddc 100644 --- a/Open-ILS/src/sql/Pg/090.schema.action.sql +++ b/Open-ILS/src/sql/Pg/090.schema.action.sql @@ -562,6 +562,7 @@ CREATE TABLE action.hold_copy_map ( hold INT NOT NULL REFERENCES action.hold_request (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, target_copy BIGINT NOT NULL, -- REFERENCES asset.copy (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, -- XXX could be an serial.issuance proximity NUMERIC, + shipping_distance NUMERIC, CONSTRAINT copy_once_per_hold UNIQUE (hold,target_copy) ); -- CREATE INDEX acm_hold_idx ON action.hold_copy_map (hold); @@ -1607,6 +1608,35 @@ BEGIN END; $f$ LANGUAGE PLPGSQL; +CREATE OR REPLACE FUNCTION action.hold_copy_calculated_shipping_distance( + ahr_id INT, + acp_id BIGINT +) RETURNS NUMERIC AS $f$ +DECLARE + ahr action.hold_request%ROWTYPE; + acp asset.copy%ROWTYPE; + + dist NUMERIC; + o_hub NUMERIC; + d_hub NUMERIC; +BEGIN + + SELECT * INTO ahr FROM action.hold_request WHERE id = ahr_id; + SELECT * INTO acp FROM asset.copy WHERE id = acp_id; + + + SELECT hub from actor.list_org_unit_ancestor_shipping_hub(ahr.pickup_lib) + INTO o_hub; + SELECT hub from actor.list_org_unit_ancestor_shipping_hub(acp.circ_lib) + INTO d_hub; + SELECT distance from actor.org_unit_shipping_hub_distance aoushd + where aoushd.orig_hub = o_hub and aoushd.dest_hub = d_hub + INTO dist; + + RETURN dist; +END; +$f$ LANGUAGE PLPGSQL; + CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity_update () RETURNS TRIGGER AS $f$ BEGIN NEW.proximity := action.hold_copy_calculated_proximity(NEW.hold,NEW.target_copy); @@ -1614,7 +1644,15 @@ BEGIN END; $f$ LANGUAGE PLPGSQL; +CREATE OR REPLACE FUNCTION action.hold_copy_calculated_shipping_distance_update () RETURNS TRIGGER AS $f$ +BEGIN + NEW.shipping_distance := action.hold_copy_calculated_shipping_distance(NEW.hold,NEW.target_copy); + RETURN NEW; +END; +$f$ LANGUAGE PLPGSQL; + CREATE TRIGGER hold_copy_proximity_update_tgr BEFORE INSERT OR UPDATE ON action.hold_copy_map FOR EACH ROW EXECUTE PROCEDURE action.hold_copy_calculated_proximity_update (); +CREATE TRIGGER hold_copy_shipping_distance_update_tgr BEFORE INSERT OR UPDATE ON action.hold_copy_map FOR EACH ROW EXECUTE PROCEDURE action.hold_copy_calculated_shipping_distance_update (); CREATE TABLE action.usr_circ_history ( id BIGSERIAL PRIMARY KEY, diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index c3d2745c1f..8fef92ab9f 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -21435,6 +21435,98 @@ VALUES ( 'integer' ); +-- Hold Targeter Geosort + +ALTER TABLE actor.org_unit +ADD COLUMN shipping_hub_ou BIGINT REFERENCES actor.org_unit(id) ON DELETE SET NULL; + +CREATE OR REPLACE FUNCTION actor.list_org_unit_ancestor_shipping_hub(VARIADIC orgs NUMERIC[]) RETURNS TABLE(org_unit INT,hub INT) + AS +$func$ +DECLARE + rec record; + cur_org INT; + next_hub INT; + org_id INT; +BEGIN + FOREACH org_id IN ARRAY orgs LOOP + cur_org := org_id; + org_unit := cur_org; + LOOP + SELECT INTO next_hub actor.org_unit.shipping_hub_ou FROM actor.org_unit WHERE actor.org_unit.id = cur_org; + IF FOUND AND next_hub IS NOT NULL THEN + hub := next_hub; + return next; + EXIT; + END IF; + SELECT INTO cur_org parent_ou FROM actor.org_unit WHERE actor.org_unit.id = cur_org; + EXIT WHEN cur_org IS NULL; + END LOOP; + END LOOP; + RETURN; +END; +$func$ LANGUAGE PLPGSQL; + +CREATE TABLE actor.org_unit_shipping_hub_distance ( + id SERIAL PRIMARY KEY, + orig_hub BIGINT NOT NULL REFERENCES actor.org_unit(id) ON DELETE CASCADE DEFERRABLE, + dest_hub BIGINT NOT NULL REFERENCES actor.org_unit(id) ON DELETE CASCADE DEFERRABLE, + distance INT NOT NULL +); + +INSERT into config.org_unit_setting_type +( name, grp, label, description, datatype, fm_class ) VALUES +( 'circ.holds.target_sort_by_geographic_proximity', 'holds', + oils_i18n_gettext('circ.holds.target_sort_by_geographic_proximity', + 'Use shipping hub distance based hold targeting', + 'coust', 'label'), + oils_i18n_gettext('circ.holds.target_sort_by_geographic_proximity', + 'Use shipping hub distance based hold targeting', + 'coust', 'description'), + 'bool', null) +; + +ALTER TABLE action.hold_copy_map +ADD COLUMN shipping_distance NUMERIC; + +CREATE OR REPLACE FUNCTION action.hold_copy_calculated_shipping_distance( + ahr_id INT, + acp_id BIGINT +) RETURNS NUMERIC AS $f$ +DECLARE + ahr action.hold_request%ROWTYPE; + acp asset.copy%ROWTYPE; + + dist NUMERIC; + o_hub NUMERIC; + d_hub NUMERIC; +BEGIN + + SELECT * INTO ahr FROM action.hold_request WHERE id = ahr_id; + SELECT * INTO acp FROM asset.copy WHERE id = acp_id; + + + SELECT hub from actor.list_org_unit_ancestor_shipping_hub(ahr.pickup_lib) + INTO o_hub; + SELECT hub from actor.list_org_unit_ancestor_shipping_hub(acp.circ_lib) + INTO d_hub; + SELECT distance from actor.org_unit_shipping_hub_distance aoushd + where aoushd.orig_hub = o_hub and aoushd.dest_hub = d_hub + INTO dist; + + RETURN dist; +END; +$f$ LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION action.hold_copy_calculated_shipping_distance_update () RETURNS TRIGGER AS $f$ +BEGIN + NEW.shipping_distance := action.hold_copy_calculated_shipping_distance(NEW.hold,NEW.target_copy); + RETURN NEW; +END; +$f$ LANGUAGE PLPGSQL; + +CREATE TRIGGER hold_copy_shipping_distance_update_tgr BEFORE INSERT OR UPDATE ON action.hold_copy_map FOR EACH ROW EXECUTE PROCEDURE action.hold_copy_calculated_shipping_distance_update (); + ------------------- Disabled example A/T defintions ------------------------------ -- Create a "dummy" slot when applicable, and trigger the "offer curbside" events diff --git a/Open-ILS/src/sql/Pg/upgrade/xxxx.function.ahcm_shipping_distance.sql b/Open-ILS/src/sql/Pg/upgrade/xxxx.function.ahcm_shipping_distance.sql new file mode 100644 index 0000000000..24ffea9fdc --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/xxxx.function.ahcm_shipping_distance.sql @@ -0,0 +1,44 @@ +BEGIN; + +ALTER TABLE action.hold_copy_map +ADD COLUMN shipping_distance NUMERIC; + +CREATE OR REPLACE FUNCTION action.hold_copy_calculated_shipping_distance( + ahr_id INT, + acp_id BIGINT +) RETURNS NUMERIC AS $f$ +DECLARE + ahr action.hold_request%ROWTYPE; + acp asset.copy%ROWTYPE; + + dist NUMERIC; + o_hub NUMERIC; + d_hub NUMERIC; +BEGIN + + SELECT * INTO ahr FROM action.hold_request WHERE id = ahr_id; + SELECT * INTO acp FROM asset.copy WHERE id = acp_id; + + + SELECT hub from actor.list_org_unit_ancestor_shipping_hub(ahr.pickup_lib) + INTO o_hub; + SELECT hub from actor.list_org_unit_ancestor_shipping_hub(acp.circ_lib) + INTO d_hub; + SELECT distance from actor.org_unit_shipping_hub_distance aoushd + where aoushd.orig_hub = o_hub and aoushd.dest_hub = d_hub + INTO dist; + + RETURN dist; +END; +$f$ LANGUAGE PLPGSQL; + +CREATE OR REPLACE FUNCTION action.hold_copy_calculated_shipping_distance_update () RETURNS TRIGGER AS $f$ +BEGIN + NEW.shipping_distance := action.hold_copy_calculated_shipping_distance(NEW.hold,NEW.target_copy); + RETURN NEW; +END; +$f$ LANGUAGE PLPGSQL; + +CREATE TRIGGER hold_copy_shipping_distance_update_tgr BEFORE INSERT OR UPDATE ON action.hold_copy_map FOR EACH ROW EXECUTE PROCEDURE action.hold_copy_calculated_shipping_distance_update (); + +COMMIT; \ No newline at end of file diff --git a/Open-ILS/src/sql/Pg/upgrade/xxxx.schema.actor_org_unit_shipping_hub.sql b/Open-ILS/src/sql/Pg/upgrade/xxxx.schema.actor_org_unit_shipping_hub.sql new file mode 100644 index 0000000000..900793dea8 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/xxxx.schema.actor_org_unit_shipping_hub.sql @@ -0,0 +1,64 @@ +BEGIN; + +INSERT into config.org_unit_setting_type +( name, grp, label, description, datatype, fm_class ) VALUES +( 'circ.holds.target_sort_by_geographic_proximity', 'holds', + oils_i18n_gettext('circ.holds.target_sort_by_geographic_proximity', + 'Use shipping hub distance based hold targeting', + 'coust', 'label'), + oils_i18n_gettext('circ.holds.target_sort_by_geographic_proximity', + 'Use shipping hub distance based hold targeting', + 'coust', 'description'), + 'bool', null) +; + +INSERT INTO config.global_flag (name, label, enabled) + VALUES ( + 'circ.holds.distance_divisor', + oils_i18n_gettext( + 'circ.holds.distance_divisor', + 'Hold targeter will divide shipping distances by this number when scoring copies.', + 'cgf', + 'label' + ), + TRUE + ); + +ALTER TABLE actor.org_unit +ADD COLUMN shipping_hub_ou BIGINT REFERENCES actor.org_unit(id) ON DELETE SET NULL; + +CREATE OR REPLACE FUNCTION actor.list_org_unit_ancestor_shipping_hub(VARIADIC orgs NUMERIC[]) RETURNS TABLE(org_unit INT,hub INT) + AS +$func$ +DECLARE + rec record; + cur_org INT; + next_hub INT; + org_id INT; +BEGIN + FOREACH org_id IN ARRAY orgs LOOP + cur_org := org_id; + org_unit := cur_org; + LOOP + SELECT INTO next_hub actor.org_unit.shipping_hub_ou FROM actor.org_unit WHERE actor.org_unit.id = cur_org; + IF FOUND AND next_hub IS NOT NULL THEN + hub := next_hub; + return next; + EXIT; + END IF; + SELECT INTO cur_org parent_ou FROM actor.org_unit WHERE actor.org_unit.id = cur_org; + EXIT WHEN cur_org IS NULL; + END LOOP; + END LOOP; + RETURN; +END; +$func$ LANGUAGE PLPGSQL; + +CREATE TABLE actor.org_unit_shipping_hub_distance ( + id SERIAL PRIMARY KEY, + orig_hub BIGINT NOT NULL REFERENCES actor.org_unit(id) ON DELETE CASCADE DEFERRABLE, + dest_hub BIGINT NOT NULL REFERENCES actor.org_unit(id) ON DELETE CASCADE DEFERRABLE, + distance INT NOT NULL +); + +COMMIT; \ No newline at end of file diff --git a/Open-ILS/src/templates/opac/parts/library/core_info.tt2 b/Open-ILS/src/templates/opac/parts/library/core_info.tt2 index cdaf0473e4..9e45c43eb0 100644 --- a/Open-ILS/src/templates/opac/parts/library/core_info.tt2 +++ b/Open-ILS/src/templates/opac/parts/library/core_info.tt2 @@ -31,6 +31,7 @@ [%- IF ctx.library.mailing_address; %]
+ [%- IF ctx.library.mailing_address -%]

[% l('Mailing address') %]

[% ctx.mailing_address.street1 | html %] @@ -40,7 +41,17 @@ [% ctx.mailing_address.state | html %]
[% ctx.mailing_address.country | html %]
[% ctx.mailing_address.post_code | html %]
+ [%- IF ctx.mailing_address.latitude AND ctx.mailing_address.longitude -%] + + [%- END -%]
+ [%- END; -%]
[%- END; %] diff --git a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2 b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2 index daba717614..cbd29ab5e0 100644 --- a/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2 +++ b/Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2 @@ -47,6 +47,18 @@
+ [% l('Shipping Hub OU') %] +
+
+ +
+
+
+
diff --git a/Open-ILS/tests/datasets/sql/assets_concerto.sql b/Open-ILS/tests/datasets/sql/assets_concerto.sql index 82e050fe03..2dfb087a02 100644 --- a/Open-ILS/tests/datasets/sql/assets_concerto.sql +++ b/Open-ILS/tests/datasets/sql/assets_concerto.sql @@ -8,6 +8,7 @@ SELECT evergreen.populate_call_number(4, '780 B', 'IMPORT CONCERTO', 2); -- BR1 SELECT evergreen.populate_call_number(5, '780 A', 'IMPORT CONCERTO', 2); -- BR2 SELECT evergreen.populate_call_number(6, '781 D', 'IMPORT CONCERTO', 2); -- BR3 SELECT evergreen.populate_call_number(7, '781 G', 'IMPORT CONCERTO', 2); -- BR4 +SELECT evergreen.populate_call_number(11, 'LB 782 G', 'IMPORT CONCERTO', 2); -- BR5 SELECT evergreen.populate_call_number(9, '780 R', 'IMPORT CONCERTO', 2); -- BM1 -- Create copies @@ -15,6 +16,7 @@ SELECT evergreen.populate_copy(4, 4, 'CONC40000', 'M'); -- BR1 SELECT evergreen.populate_copy(5, 5, 'CONC50000', 'M'); -- BR2 SELECT evergreen.populate_copy(6, 6, 'CONC60000', 'M'); -- BR3 SELECT evergreen.populate_copy(7, 7, 'CONC70000', 'M'); -- BR4 +SELECT evergreen.populate_copy(11, 11, 'CONC80000', 'M'); -- BR5 SELECT evergreen.populate_copy(9, 9, 'CONC90000', 'M'); -- BM1 SELECT evergreen.populate_copy(4, 4, 'CONC41000', 'M'); -- BR1 diff --git a/Open-ILS/tests/datasets/sql/env_create.sql b/Open-ILS/tests/datasets/sql/env_create.sql index fe9dbea0e5..9bd5b6f402 100644 --- a/Open-ILS/tests/datasets/sql/env_create.sql +++ b/Open-ILS/tests/datasets/sql/env_create.sql @@ -16,26 +16,26 @@ CREATE TABLE marcxml_import (id SERIAL PRIMARY KEY, marc TEXT, tag TEXT); * This will happily create duplicate addresses if given duplicate info. */ CREATE FUNCTION evergreen.create_aou_address - (owning_lib INTEGER, street1 TEXT, street2 TEXT, city TEXT, state TEXT, country TEXT, + (owning_lib INTEGER, street1 TEXT, street2 TEXT, city TEXT, state TEXT, county TEXT, country TEXT, post_code TEXT, address_type TEXT) RETURNS void AS $$ BEGIN - INSERT INTO actor.org_address (org_unit, street1, street2, city, state, country, post_code) - VALUES ($1, $2, $3, $4, $5, $6, $7); + INSERT INTO actor.org_address (org_unit, street1, street2, city, state, county, country, post_code) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8); - IF $8 IS NULL THEN + IF $9 IS NULL THEN UPDATE actor.org_unit SET holds_address = currval('actor.org_address_id_seq'), ill_address = currval('actor.org_address_id_seq'), billing_address = currval('actor.org_address_id_seq'), mailing_address = currval('actor.org_address_id_seq') WHERE id = $1; END IF; - IF $8 ~ 'holds' THEN + IF $9 ~ 'holds' THEN UPDATE actor.org_unit SET holds_address = currval('actor.org_address_id_seq') WHERE id = $1; END IF; - IF $8 ~ 'interlibrary' THEN + IF $9 ~ 'interlibrary' THEN UPDATE actor.org_unit SET ill_address = currval('actor.org_address_id_seq') WHERE id = $1; END IF; - IF $8 ~ 'billing' THEN + IF $9 ~ 'billing' THEN UPDATE actor.org_unit SET billing_address = currval('actor.org_address_id_seq') WHERE id = $1; END IF; - IF $8 ~ 'mailing' THEN + IF $9 ~ 'mailing' THEN UPDATE actor.org_unit SET mailing_address = currval('actor.org_address_id_seq') WHERE id = $1; END IF; END diff --git a/Open-ILS/tests/datasets/sql/env_destroy.sql b/Open-ILS/tests/datasets/sql/env_destroy.sql index 61691943c8..e3b42fa71e 100644 --- a/Open-ILS/tests/datasets/sql/env_destroy.sql +++ b/Open-ILS/tests/datasets/sql/env_destroy.sql @@ -1,7 +1,7 @@ -- clean up our temp tables / functions DROP TABLE marcxml_import; -DROP FUNCTION evergreen.create_aou_address(INTEGER, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT); +DROP FUNCTION evergreen.create_aou_address(INTEGER, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT, TEXT,TEXT); DROP FUNCTION evergreen.populate_call_number(INTEGER, TEXT, TEXT); DROP FUNCTION evergreen.populate_call_number(INTEGER, TEXT, TEXT, INTEGER); DROP FUNCTION evergreen.generate_price(); diff --git a/Open-ILS/tests/datasets/sql/libraries.sql b/Open-ILS/tests/datasets/sql/libraries.sql index 513dc2b7f2..34b548e523 100644 --- a/Open-ILS/tests/datasets/sql/libraries.sql +++ b/Open-ILS/tests/datasets/sql/libraries.sql @@ -3,6 +3,10 @@ INSERT INTO actor.org_unit (id, parent_ou, ou_type, shortname, name) VALUES INSERT INTO actor.org_unit (id, parent_ou, ou_type, shortname, name) VALUES (3, 1, 2, 'SYS2', oils_i18n_gettext(3, 'Example System 2', 'aou', 'name')); INSERT INTO actor.org_unit (id, parent_ou, ou_type, shortname, name) VALUES + (10, 1, 2, 'SYS3', oils_i18n_gettext(10, 'Example System 3', 'aou', 'name')); +INSERT INTO actor.org_unit (id, parent_ou, ou_type, shortname, name) VALUES + (12, 1, 2, 'SYS4', oils_i18n_gettext(12, 'Example System 4', 'aou', 'name')); +INSERT INTO actor.org_unit (id, parent_ou, ou_type, shortname, name) VALUES (4, 2, 3, 'BR1', oils_i18n_gettext(4, 'Example Branch 1', 'aou', 'name')); INSERT INTO actor.org_unit (id, parent_ou, ou_type, shortname, name) VALUES (5, 2, 3, 'BR2', oils_i18n_gettext(5, 'Example Branch 2', 'aou', 'name')); @@ -11,41 +15,53 @@ INSERT INTO actor.org_unit (id, parent_ou, ou_type, shortname, name) VALUES INSERT INTO actor.org_unit (id, parent_ou, ou_type, shortname, name) VALUES (7, 3, 3, 'BR4', oils_i18n_gettext(7, 'Example Branch 4', 'aou', 'name')); INSERT INTO actor.org_unit (id, parent_ou, ou_type, shortname, name) VALUES + (11, 10, 3, 'BR5', oils_i18n_gettext(11, 'Example Branch 5', 'aou', 'name')); +INSERT INTO actor.org_unit (id, parent_ou, ou_type, shortname, name) VALUES + (13, 12, 3, 'BR6', oils_i18n_gettext(13, 'Example Branch 6', 'aou', 'name')); +INSERT INTO actor.org_unit (id, parent_ou, ou_type, shortname, name) VALUES (8, 4, 4, 'SL1', oils_i18n_gettext(8, 'Example Sub-library 1', 'aou', 'name')); INSERT INTO actor.org_unit (id, parent_ou, ou_type, shortname, name) VALUES (9, 6, 5, 'BM1', oils_i18n_gettext(9, 'Example Bookmobile 1', 'aou', 'name')); -INSERT INTO actor.org_lasso (id, name, global) VALUES (1000001, 'Even Branches', FALSE); -INSERT INTO actor.org_lasso_map (lasso, org_unit) VALUES (1000001, 5), (1000001, 7); -INSERT INTO actor.org_lasso (id, name, global) VALUES (1000002, 'Non-branches', TRUE); -INSERT INTO actor.org_lasso_map (lasso, org_unit) VALUES (1000002, 8), (1000002, 9); -- Address for the Consortium -SELECT evergreen.create_aou_address(1, '123 Main St.', NULL, 'Anywhere', 'GA', 'US', '30303', NULL); +SELECT evergreen.create_aou_address(1, '250 Georgia Ave SE #103', NULL, 'Atlanta', 'GA', 'Fulton', 'US', '30312', NULL); -- Addresses for System 1 -SELECT evergreen.create_aou_address(2, '234 Side St.', NULL, 'Anywhere', 'GA', 'US', '30304', NULL); +SELECT evergreen.create_aou_address(2, '1721 Waters Ave', NULL, 'Savannah', 'GA','Chatham', 'US', '31404', NULL); -- Addresses for System 2 -SELECT evergreen.create_aou_address(3, '345 Corner Crescent', NULL, 'Elsewhere', 'GA', 'US', '30335', NULL); +SELECT evergreen.create_aou_address(3, '831 Adams St', NULL, 'Macon', 'GA','Bibb', 'US', '31201', NULL); + +-- Addresses for System 3 +SELECT evergreen.create_aou_address(10, '215 N Lumpkin St', NULL, 'Athens', 'GA', 'Clarke', 'US', '30601', NULL); + +-- Addresses for System 4 +SELECT evergreen.create_aou_address(12, '625 Academy St NE', NULL, 'Gainesville', 'GA', 'Hall', 'US', '30501', NULL); -- Addresses for Branch 1 -SELECT evergreen.create_aou_address(4, 'BR1', '123 Main St.', 'Anywhere', 'GA', 'US', '30303', 'billing mailing'); -SELECT evergreen.create_aou_address(4, 'Holds and ILL', '125 Main St.', 'Anywhere', 'GA', 'US', '30303', 'interlibrary holds'); +SELECT evergreen.create_aou_address(4, '250 Georgia Ave SE #103', NULL, 'Atlanta', 'GA', 'Hall','US', '30312', 'billing mailing'); +SELECT evergreen.create_aou_address(4, '250 Georgia Ave SE #103', NULL, 'Atlanta', 'GA', 'Hall','US', '30312', 'interlibrary holds'); -- Addresses for Branch 2 -SELECT evergreen.create_aou_address(5, 'BR2', '234 Side St.', 'Anywhere', 'GA', 'US', '30304', 'mailing'); -SELECT evergreen.create_aou_address(5, 'BR2 - Billing', '234 Side St.', 'Anywhere', 'GA', 'US', '30304', 'billing'); -SELECT evergreen.create_aou_address(5, 'BR2 - Holds and ILL', '234 Side St.', 'Anywhere', 'GA', 'US', '30304', 'interlibrary holds'); +SELECT evergreen.create_aou_address(5, '1721 Waters Ave', NULL, 'Savannah', 'GA','Chatham', 'US', '31404', 'mailing'); +SELECT evergreen.create_aou_address(5, '1721 Waters Ave', NULL, 'Savannah', 'GA','Chatham', 'US', '31404', 'billing'); +SELECT evergreen.create_aou_address(5, '1721 Waters Ave', NULL, 'Savannah', 'GA','Chatham', 'US', '31404', 'interlibrary holds'); -- Addresses for Branch 3 -SELECT evergreen.create_aou_address(6, 'BR3', '347 Corner Crescent', 'Elsewhere', 'GA', 'US', '30335', NULL); +SELECT evergreen.create_aou_address(6, '831 Adams St', NULL, 'Macon', 'GA','Bibb', 'US', '31201', NULL); -- Addresses for Branch 4 -SELECT evergreen.create_aou_address(7, 'BR4', '446 Nowhere Road', 'Elsewhere', 'GA', 'US', '30404', 'mailing'); -SELECT evergreen.create_aou_address(7, 'BR4 - Billing Dept', '446 Nowhere Road', 'Elsewhere', 'GA', 'US', '30404', 'billing'); -SELECT evergreen.create_aou_address(7, 'BR4 - Holds and ILL', '756 Industrial Lane', 'Elsewhere', 'GA', 'US', '30304', 'interlibrary holds'); +SELECT evergreen.create_aou_address(7, '419 7th St', NULL, 'Augusta', 'GA', 'Richmond', 'US', '30901', 'mailing'); +SELECT evergreen.create_aou_address(7, '419 7th St', NULL, 'Augusta', 'GA', 'Richmond','US', '30901', 'billing'); +SELECT evergreen.create_aou_address(7, '419 7th St', NULL, 'Augusta', 'GA', 'Richmond','US', '30901', 'interlibrary holds'); + +-- Addresses for Branch 5 +SELECT evergreen.create_aou_address(11, '215 N Lumpkin St', NULL, 'Athens', 'GA', 'Clarke', 'US', '30601', NULL); + +-- Addresses for Branch 6 +SELECT evergreen.create_aou_address(13, '625 Academy St NE', NULL, 'Gainesville', 'GA', 'Hall', 'US', '30501', NULL); -- Hours for branches INSERT INTO actor.hours_of_operation (id, dow_0_open, dow_0_close, dow_1_open, dow_1_close, @@ -67,8 +83,19 @@ INSERT INTO actor.org_unit_setting(org_unit, name, value) VALUES (6, 'lib.info_url', '"http://br3.example.com"'), -- BR3 (7, 'lib.info_url', '"http://br4.example.com/info"'); -- BR4 +UPDATE actor.org_unit SET shipping_hub_ou = 4 WHERE id = 2; +UPDATE actor.org_unit SET shipping_hub_ou = 7 WHERE id = 3; +UPDATE actor.org_unit SET shipping_hub_ou = 11 WHERE id = 10; +UPDATE actor.org_unit SET shipping_hub_ou = 13 WHERE id = 12; +INSERT INTO actor.org_unit_shipping_hub_distance(orig_hub, dest_hub, distance) VALUES (4,4,0),(4,7,25),(4,11,50),(4,13,80), + (7,7,0),(7,4,25),(7,11,65),(7,13,35), + (11,7,65),(11,4,50),(11,11,0),(11,13,25), + (13,7,35),(13,4,80),(13,11,25),(13,13,0); + UPDATE actor.org_unit SET email = 'br1@example.com', phone = '(555) 555-0271' WHERE shortname = 'BR1'; UPDATE actor.org_unit SET email = 'br2@example.com', phone = '(555) 555-0272' WHERE shortname = 'BR2'; UPDATE actor.org_unit SET email = 'br3@example.com', phone = '(555) 555-0273' WHERE shortname = 'BR3'; UPDATE actor.org_unit SET email = 'br4@example.com', phone = '(555) 555-0274' WHERE shortname = 'BR4'; +UPDATE actor.org_unit SET email = 'br5@example.com', phone = '(555) 555-0275' WHERE shortname = 'BR5'; +UPDATE actor.org_unit SET email = 'br6@example.com', phone = '(555) 555-0276' WHERE shortname = 'BR6'; diff --git a/Open-ILS/tests/datasets/sql/transactions.sql b/Open-ILS/tests/datasets/sql/transactions.sql index cb9a73c699..3a73ff2f49 100644 --- a/Open-ILS/tests/datasets/sql/transactions.sql +++ b/Open-ILS/tests/datasets/sql/transactions.sql @@ -106,7 +106,9 @@ BEGIN 'T', bre.id, recipient.id, recipient.id, recipient.home_ou, FALSE, NULL ); + + -- title hold, circulator-placed bre := evergreen.next_bib(bre.id); EXIT WHEN bre IS NULL; @@ -143,6 +145,11 @@ BEGIN 'M', 42, 2, 2, 9, FALSE, NULL, '{"0":[{"_attr":"mr_hold_format","_val":"score"}]}' ); + -- title hold, resource sharing + PERFORM evergreen.populate_hold( + 'T', 9, 9, 9, + 13, FALSE, NULL + ); END $$; diff --git a/Open-ILS/web/conify/global/actor/org_unit.html b/Open-ILS/web/conify/global/actor/org_unit.html index faaac24e7f..2a3b59a5ed 100644 --- a/Open-ILS/web/conify/global/actor/org_unit.html +++ b/Open-ILS/web/conify/global/actor/org_unit.html @@ -193,6 +193,11 @@ editor_pane_parent_ou.validate(true); editor_pane_parent_ou.setValue( this.store.getValue( current_ou, 'parent_ou' ) ); } + + editor_pane_shipping_hub_ou.disabled = false; + editor_pane_shipping_hub_ou.required = false; + editor_pane_shipping_hub_ou.validate(true); + editor_pane_shipping_hub_ou.setValue( this.store.getValue( current_ou, 'shipping_hub_ou' ) ); editor_pane_opac_visible.setChecked( this.store.getValue( current_ou, 'opac_visible' ) == 't' ? true : false ); @@ -302,6 +307,26 @@
+ + &conify.org_unit.editor_pane.shipping_hub; + +
+ +
+ + &conify.org_unit.editor_pane.opac_visible; diff --git a/Open-ILS/web/opac/locale/en-US/conify.dtd b/Open-ILS/web/opac/locale/en-US/conify.dtd index c0cf5a7992..128637c441 100644 --- a/Open-ILS/web/opac/locale/en-US/conify.dtd +++ b/Open-ILS/web/opac/locale/en-US/conify.dtd @@ -32,6 +32,7 @@ +