<link field="name" reltype="has_a" key="name" map="" class="coust"/>
</links>
</class>
+ <class id="aoushd" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::org_unit_shipping_hub_distance" oils_persist:tablename="actor.org_unit_shipping_hub_distance" reporter:label="Organizational Unit Shipping Hub Distance">
+ <fields oils_persist:primary="id" oils_persist:sequence="actor.org_unit_shipping_hub_distance_id_seq">
+ <field name="id" />
+ <field name="orig_hub" reporter:datatype="org_unit"/>
+ <field name="dest_hub" reporter:datatype="org_unit"/>
+ <field name="distance" reporter:datatype="int"/>
+ </fields>
+ <links>
+ <link field="orig_hub" reltype="has_a" key="id" map="" class="aou"/>
+ <link field="dest_hub" reltype="has_a" key="id" map="" class="aou"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="UPDATE_ORG_UNIT" context_field="orig_hub"/>
+ <retrieve/>
+ <update permission="UPDATE_ORG_UNIT" context_field="orig_hub"/>
+ <delete permission="UPDATE_ORG_UNIT" context_field="orig_hub"/>
+ </actions>
+ </permacrud>
+ </class>
<class id="acpn" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::copy_note" oils_persist:tablename="asset.copy_note" reporter:label="Copy Note">
<fields oils_persist:primary="id" oils_persist:sequence="asset.copy_note_id_seq">
<field reporter:label="Note Creation Date/Time" name="create_date" reporter:datatype="timestamp"/>
<field name="id" reporter:datatype="id" />
<field name="target_copy" reporter:datatype="link"/>
<field name="proximity" reporter:datatype="number"/>
+ <field name="shipping_distance" reporter:datatype="number"/>
</fields>
<links>
<link field="hold" reltype="has_a" key="id" map="" class="ahr"/>
<field reporter:label="Name" name="name" reporter:datatype="text" oils_persist:i18n="true" oils_obj:required="true"/>
<field reporter:label="Organizational Unit Type" name="ou_type" reporter:datatype="link" oils_obj:required="true"/>
<field reporter:label="Parent Organizational Unit" name="parent_ou" reporter:datatype="link"/>
+ <field reporter:label="Shipping Hub Unit" name="shipping_hub_ou" reporter:datatype="link"/>
<field reporter:label="Short (Policy) Name" name="shortname" reporter:datatype="text" oils_obj:required="true" oils_obj:validate="^.+$"/>
<field reporter:label="Email Address" name="email" reporter:datatype="text"/>
<field reporter:label="Phone Number" name="phone" reporter:datatype="text"/>
<link field="ou_type" reltype="has_a" key="id" map="" class="aout"/>
<link field="mailing_address" reltype="has_a" key="id" map="" class="aoa"/>
<link field="parent_ou" reltype="has_a" key="id" map="" class="aou"/>
+ <link field="shipping_hub_ou" reltype="has_a" key="id" map="" class="aou"/>
<link field="ill_address" reltype="has_a" key="id" map="" class="aoa"/>
<link field="fiscal_calendar" reltype="has_a" key="id" map="" class="acqfc"/>
<link field="users" reltype="has_many" key="home_ou" map="" class="au"/>
<appname>open-ils.vandelay</appname>
<appname>open-ils.serial</appname>
<appname>open-ils.hold-targeter</appname>
+ <appname>open-ils.vicinity-calculator</appname>
<appname>open-ils.ebook_api</appname>
<appname>open-ils.courses</appname>
<appname>open-ils.curbside</appname>
routerLink="/staff/admin/server/config/marc_field"></eg-link-table-link>
<eg-link-table-link i18n-label label="Org Unit Proximity Adjustments"
routerLink="/staff/admin/server/actor/org_unit_proximity_adjustment"></eg-link-table-link>
+ <eg-link-table-link i18n-label label="Org Unit Shipping Hub Distances"
+ routerLink="/staff/admin/server/actor/org_unit_shipping_hub_distance"></eg-link-table-link>
<eg-link-table-link i18n-label label="Org Unit Setting Types"
routerLink="/staff/admin/server/config/org_unit_setting_type"></eg-link-table-link>
<eg-link-table-link i18n-label label="Organization Types"
import {AdminCommonModule} from '@eg/staff/admin/common.module';
import {AdminServerRoutingModule} from './routing.module';
import {AdminServerSplashComponent} from './admin-server-splash.component';
+import {OrgUnitShippingHubDistanceComponent} from './org-unit-shipping-hub-distance.component';
import {OrgUnitTypeComponent} from './org-unit-type.component';
import {PrintTemplateComponent} from './print-template.component';
import {SampleDataService} from '@eg/share/util/sample-data.service';
declarations: [
AdminServerSplashComponent,
OrgUnitTypeComponent,
+ OrgUnitShippingHubDistanceComponent,
PrintTemplateComponent,
PermGroupTreeComponent,
PermGroupMapDialogComponent
--- /dev/null
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+
+/**
+ * Generic IDL class editor page.
+ */
+
+@Component({
+ template: `
+ <eg-title i18n-prefix prefix="{{classLabel}} Administration">
+ </eg-title>
+ <eg-staff-banner bannerText="{{classLabel}} Configuration" i18n-bannerText>
+ </eg-staff-banner>
+ <div class="alert alert-info" i18n>
+ 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. <b>Recalculating the distances will clear out any existing data.</b>.
+ </div>
+ <button [disabled]="calculating" class="btn btn-outline-dark" (click)="calculateDistances()">Calculate with Geolocation Service</button>
+ <eg-progress-inline *ngIf="calculating" ></eg-progress-inline>
+ <br>
+ <br>
+ <eg-admin-page persistKeyPfx="{{persistKeyPfx}}" idlClass="{{idlClass}}"
+ configLinkBasePath="{{configLinkBasePath}}"
+ readonlyFields="{{readonlyFields}}"
+ [disableOrgFilter]="disableOrgFilter"></eg-admin-page>
+ `
+})
+
+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;}
+ );
+ }
+}
+
+
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';
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',
export CPAN_MODULES = \
Geo::Coder::Google \
+ Geo::Coder::Bing \
Business::OnlinePayment::PayPal \
String::KeyboardDistance \
Text::Levenshtein::Damerau::XS \
export CPAN_MODULES = \
Geo::Coder::Google \
+ Geo::Coder::Bing \
Business::OnlinePayment::PayPal \
String::KeyboardDistance \
Text::Levenshtein::Damerau::XS \
export CPAN_MODULES = \
Geo::Coder::Google \
+ Geo::Coder::Bing \
Business::OnlinePayment::PayPal \
String::KeyboardDistance \
Text::Levenshtein::Damerau::XS \
export CPAN_MODULES = \
Geo::Coder::OSM \
Geo::Coder::Google \
+ Geo::Coder::Bing \
Excel::Writer::XLSX \
String::KeyboardDistance \
Text::Levenshtein::Damerau::XS \
export CPAN_MODULES = \
Geo::Coder::Google \
+ Geo::Coder::Bing \
Business::OnlinePayment::PayPal \
Email::Send \
MARC::Charset \
export CPAN_MODULES = \
Geo::Coder::Google \
+ Geo::Coder::Bing \
Business::OnlinePayment::PayPal \
Email::Send \
MARC::Charset \
'File::Spec' => '0',
'File::stat' => '0',
'File::Temp' => '0',
+ 'Geo::Coder::Bing' => '0',
'Getopt::Long' => '0',
'IO::Scalar' => '0',
'List::Util' => '0',
'Unicode::Normalize' => '0',
'UNIVERSAL::require' => '0',
'UUID::Tiny' => '0',
+ 'WWW::REST' => '0',
'XML::LibXML' => '0',
'XML::LibXML::XPathContext' => '0',
'XML::LibXSLT' => '0',
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
}
__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"
);
$logger->info("circulator: un-targeting hold ".$hold->id.
" because copy ".$copy->id." is getting checked out");
-
+ try {
+ $U->simplereq('open-ils.circ',
+ 'open-ils.circ.hold_reset_reason_entry.create',
+ $e->authtoken,
+ $hold->id,
+ OILS_HOLD_CHECK_OUT,
+ "Checked out to patron #".$patron->id);
+ } catch Error with {
+ $logger->error("circulate: create reset reason failed with ".shift());
+ };
$hold->clear_prev_check_time;
$hold->clear_current_copy;
$hold->clear_capture_time;
$hold->clear_shelf_time;
$hold->clear_shelf_expire_time;
$hold->clear_current_shelf_lib;
- $U->simplereq('open-ils.circ',
- 'open-ils.circ.hold_reset_reason_entry.create',$e->authtoken,$hold->id,OILS_HOLD_CHECK_OUT,"Checked out to patron #".$patron->id);
return $self->bail_on_event($e->event)
unless $e->update_action_hold_request($hold);
next if ($_->{hold_type} eq 'P');
}
# So much for easy stuff, attempt a retarget!
- $U->simplereq('open-ils.circ',
- 'open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $_->{id},OILS_HOLD_BETTER_HOLD);
+ try{
+ $U->simplereq('open-ils.circ',
+ 'open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $_->{id},OILS_HOLD_BETTER_HOLD);
+ }
+ catch Error with{
+ $logger->error("circulate: create reset reason failed with ".shift());
+ };
my $tresult = $U->simplereq(
'open-ils.hold-targeter',
'open-ils.hold-targeter.target',
$hold->clear_cancel_time;
$hold->clear_prev_check_time unless $hold->prev_check_time;
- $U->simplereq('open-ils.circ',
- 'open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $hold->id, OILS_HOLD_CHECK_IN);
+ try{
+ $U->simplereq('open-ils.circ',
+ 'open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $hold->id, OILS_HOLD_CHECK_IN);
+ }
+ catch Error with{
+ $logger->error("circulate: create reset reason failed with ".shift());
+ };
$self->bail_on_events($self->editor->event)
unless $self->editor->update_action_hold_request($hold);
$self->hold($hold);
my $self = shift;
$logger->info("circulator: retargeting holds @{$self->retarget} after opportunistic capture");
my $ses = OpenSRF::AppSession->create('open-ils.hold-targeter');
- my $cses = OpenSRF::AppSession->create('open-ils.circ');
- $cses->request('open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $self->retarget,OILS_HOLD_BETTER_HOLD);
- $ses->request('open-ils.hold-targeter.target', {hold => $self->retarget});
+ $ses->request('open-ils.hold-targeter.target', {hold => $self->retarget});
+ try{
+ my $cses = OpenSRF::AppSession->create('open-ils.circ');
+ $cses->request('open-ils.circ.hold_reset_reason_entry.create',$self->editor->authtoken, $self->retarget,OILS_HOLD_BETTER_HOLD);
+ }
+ catch Error with{
+ $logger->error("circulate: create reset reason failed with ".shift());
+ };
# no reason to wait for the return value
return;
}
$hold->cancel_time('now');
$hold->cancel_cause($cause);
$hold->cancel_note($note);
- my $note_body = "";
- if($cause){
- my $cancel_reason = "ID $cause";
- my $cancel_cause = $e->retrieve_action_hold_request_cancel_cause($cause);
- if($cancel_cause){
- $cancel_reason = $cancel_cause->label;
- }
- $note_body .= "Cancel Cause: $cancel_reason";
- }
- else{
- $note_body .= "Cancel reason unknown";
- }
- $note_body .= "," unless $note_body eq "" || $note eq "";
- $note_body .= " Cancel Note: \"$note\"" unless $note eq "";
+ my $note_body = "";
+ if($cause){
+ my $cancel_reason = "ID $cause";
+ my $cancel_cause = $e->retrieve_action_hold_request_cancel_cause($cause);
+ if($cancel_cause){
+ $cancel_reason = $cancel_cause->label;
+ }
+ $note_body .= "Cancel Cause: $cancel_reason";
+ }
+ else{
+ $note_body .= "Cancel reason unknown";
+ }
+ $note_body .= "," unless $note_body eq "" || $note eq "";
+ $note_body .= " Cancel Note: \"$note\"" unless $note eq "";
_create_reset_reason_entry($e,$hold,OILS_HOLD_CANCELED,$note_body);
$e->update_action_hold_request($hold)
or return $e->die_event;
if($U->is_true($hold->frozen)) {
$logger->info("clearing current_copy and check_time for frozen hold ".$hold->id);
- _create_reset_reason_entry($e,$hold,OILS_HOLD_FROZEN,$note_body) unless $U->is_true($orig_hold->frozen);
+ _create_reset_reason_entry($e,$hold,OILS_HOLD_FROZEN,$note_body) unless $U->is_true($orig_hold->frozen);
$hold->clear_current_copy;
$hold->clear_prev_check_time;
# Clear expire_time to prevent frozen holds from expiring.
{
my($self, $conn, $auth, $hold, $reset_reason, $note) = @_;
my $e = new_editor(authtoken => $auth, xact => 1);
- #checkauth to set the requestor (if available)
- $e->checkauth;
+ #checkauth to set the requestor (if available)
+ $e->checkauth;
my @holds;
if(ref $hold eq 'ARRAY'){
@holds = @{$hold};
@holds = ($hold);
}
for my $holdid (@holds){
- my ($hold, $evt) = $U->fetch_hold($holdid);
- return $evt if $evt;
- _create_reset_reason_entry($e, $hold, $reset_reason, $note);
+ try{
+ my ($hold, $evt) = $U->fetch_hold($holdid);
+ _create_reset_reason_entry($e, $hold, $reset_reason, $note) unless $evt;
+ }
+ catch Error with{
+ $logger->error("holds: create reset reason failed with ".shift());
+ };
}
$e->commit;
return 1;
sub _create_reset_reason_entry
{
- my($e, $hold, $reset_reason,$note) = @_;
+ my($e, $hold, $reset_reason,$note,$previous_copy) = @_;
my $ts = DateTime->now;
my $entry = Fieldmapper::action::hold_request_reset_reason_entry->new;
$logger->info("Creating reset reason entry for hold #" . $hold->id);
- my $last_copy = $hold->current_copy;
+ my $last_copy = defined $previous_copy ? $previous_copy : $hold->current_copy;
$entry->hold($hold->id);
$entry->reset_reason($reset_reason);
$entry->reset_time('now');
- $entry->requestor($e->requestor->id) if defined $e->requestor;
- $entry->requestor_workstation($e->requestor->wsid) if defined $e->requestor;
$entry->previous_copy($last_copy);
$entry->note($note);
+ $entry->requestor($e->requestor->id) if defined $e->requestor;
+ $entry->requestor_workstation($e->requestor->wsid) if defined $e->requestor;
$e->create_action_hold_request_reset_reason_entry($entry) or return $e->die_event;
return 1;
}
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/;
};
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);
$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) = @_;
}
);
+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) = @_;
}
);
+# 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) = @_;
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;
} 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};
use OpenILS::Utils::HoldTargeter;
use OpenILS::Const qw/:const/;
use OpenSRF::Utils::Logger qw(:logger);
+use OpenSRF::EX qw(:try);
__PACKAGE__->register_method(
method => 'hold_targeter',
my $single =
OpenILS::Utils::HoldTargeter::Single->new(parent => $targeter);
- # If targeter is issued without a hold
- # it will retarget all of the holds that need it
- # so we shoot off a RRE for all them.
- $hold_ses->request(
- "open-ils.circ.hold_reset_reason_entry.create",
- $single->editor()->authtoken,
- $hold_id,
- OILS_HOLD_TIMED_OUT)
- unless defined $args->{hold};
-
# Don't let an explosion on a single hold stop processing
eval { $single->target($hold_id) };
$logger->error($msg);
$single->message($msg) unless $single->message;
}
+ else{
+ try{
+ # create a TIMED_OUT reset reason
+ # other types of resets are handled
+ # at their sources.
+ $hold_ses->request(
+ "open-ils.circ.hold_reset_reason_entry.create",
+ $single->editor()->authtoken,
+ $hold_id,
+ OILS_HOLD_TIMED_OUT,
+ $single->{previous_copy_id}
+ ) unless defined $args->{hold};
+ } catch Error with {
+ $logger->error(
+ "hold-targeter: create reset reason failed with ".shift()
+ );
+ }
+ }
if (($count % $throttle) == 0) {
# Time to reply to the caller. Return either the number
#-------------------------------------------------------------------------------
+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/;
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' );
use strict;
use warnings;
use DateTime;
+use Data::Dumper;
use OpenSRF::AppSession;
use OpenSRF::Utils::Logger qw(:logger);
use OpenSRF::Utils::JSON;
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::Const qw/:const/;
use OpenILS::Application::AppUtils;
use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use List::Util qw(shuffle max);
sub new {
my ($class, %args) = @_;
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}) {
};
}
+sub retarget_previous_targets_interval {
+ my ($self) = @_;
+ if (!defined($self->{retarget_previous_targets_interval}) || !$self->{retarget_previous_targets_interval}) {
+ # See if we have a global flag value for the interval
+ my $rri = $self->editor->search_config_global_flag({
+ name => 'circ.holds.retarget_previous_targets_interval',
+ enabled => 't'
+ })->[0];
+ # If no flag is present, default to 0 so feature is disabled
+ $self->{retarget_previous_targets_interval} = $rri ? $rri->value : 0;
+ }
+ return $self->{retarget_previous_targets_interval};
+}
+
# List of potential copies in the form of slim hashes. This list
# evolves as copies are filtered as they are deemed non-targetable.
sub copies {
sub compile_weighted_proximity_map {
my $self = shift;
+ my %copy_reset_map = {};
+ my %copy_timeout_map = {};
+ eval{
+ my $pt_interval = $self->retarget_previous_targets_interval();
+ if($pt_interval){
+ my $reset_cutoff_time = DateTime->now(time_zone => 'local')
+ ->subtract(days => $pt_interval);
+
+ # Collect reset reason info and previous copies.
+ # for this hold within the last time interval
+ my $reset_entries = $self->editor->json_query({
+ select => {ahrrre => ['reset_reason','reset_time','previous_copy']},
+ from => 'ahrrre',
+ where => {
+ hold => $self->hold_id,
+ previous_copy => {'!=' => undef},
+ reset_time => {'>=' => $reset_cutoff_time->strftime('%F %T%z')},
+ reset_reason => [OILS_HOLD_TIMED_OUT, OILS_HOLD_MANUAL_RESET]
+ }
+ });
+
+ # count how many times each copy
+ # was reset or timed out
+ for(@$reset_entries){
+ my $pc = $_->{previous_copy};
+ my $rr = $_->{reset_reason};
+ if($rr == OILS_HOLD_MANUAL_RESET){
+ $copy_reset_map{$pc} += 1;
+ }
+ elsif($rr == OILS_HOLD_TIMED_OUT){
+ $copy_timeout_map{$pc} += 1;
+ }
+ }
+ }
+ };
+
# 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;
+
+ # calculate the maximum proximity to make adjustments
+ my $max_prox = 0;
+ foreach(@$hold_copy_maps){
+ my $mp = $_->{proximity} + 1;
+ $max_prox = $mp unless $max_prox >= $mp;
+ }
+
# Pre-fetch the org setting value for all circ libs so that
# later calls can reference the cached value.
$self->parent->precache_batch_ou_settings($self->get_copy_circ_libs,
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 $reset_count = $copy_reset_map{$copy_id};
+ my $shipping_distance = $copy_dist_map{$copy_id};
+ my $timeout_count = $copy_timeout_map{$copy_id};
+
+ # make adjustments to proximity based on reset reason.
+ # manual resets get +max_prox each time
+ # this moves them to the end of the hold copy map.
+ # timeout resets only add one level of proximity
+ # so that copies can be inspected again later.
+ $prox += (($reset_count || 0) * $max_prox) + (($timeout_count || 0));
+
$copy_hash->{proximity} = $prox;
+ $copy_hash->{shipping_distance} = $shipping_distance;
$prox_map{$prox} ||= [];
my $weight = $self->parent->get_ou_setting(
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".
# 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;
);
last if ($prox > 0); # No point in looking further "out".
}
+
- my $rand = int(rand(scalar(@copies)));
+ 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}};
- 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;
+
+ 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)));
- return $c if $self->copy_is_permitted($c);
- $seen{$c->{id}} = 1;
+ while (my ($c) = splice(@copies, $rand, 1)) {
+ $rand = int(rand(scalar(@copies)));
+ next if $seen{$c->{id}};
- last unless(@copies);
+ return $c if $self->copy_is_permitted($c);
+ $seen{$c->{id}} = 1;
+
+ last unless(@copies);
+
+ }
}
}
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.
--- /dev/null
+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
);
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;
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);
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);
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,
'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
--- /dev/null
+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
--- /dev/null
+CREATE TABLE action.hold_request_reset_reason
+(
+ id serial NOT NULL,
+ manual boolean,
+ name text,
+CONSTRAINT hold_request_reset_reason_pkey PRIMARY KEY (id),
+CONSTRAINT hold_request_reset_reason_name_key UNIQUE (name)
+) WITH (
+ OIDS=FALSE
+);
+
+INSERT INTO action.hold_request_reset_reason (id, name, manual) VALUES
+(1,'HOLD_TIMED_OUT',false),
+(2,'HOLD_MANUAL_RESET',true),
+(3,'HOLD_BETTER_HOLD',false),
+(4,'HOLD_FROZEN',true),
+(5,'HOLD_UNFROZEN',true),
+(6,'HOLD_CANCELED',true),
+(7,'HOLD_UNCANCELED',true),
+(8,'HOLD_UPDATED',true),
+(9,'HOLD_CHECKED_OUT',true),
+(10,'HOLD_CHECKED_IN',true);
+
+CREATE TABLE action.hold_request_reset_reason_entry
+(
+ id serial NOT NULL,
+ hold int,
+ reset_reason int,
+ note text,
+ reset_time timestamp with time zone,
+ previous_copy bigint,
+ requestor int,
+ requestor_workstation int,
+ CONSTRAINT hold_request_reset_reason_entry_pkey PRIMARY KEY (id),
+ CONSTRAINT action_hold_request_reset_reason_entry_reason_fkey FOREIGN KEY (reset_reason)
+ REFERENCES action.hold_request_reset_reason (id) MATCH SIMPLE
+ ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
+CONSTRAINT action_hold_request_reset_reason_entry_previous_copy_fkey FOREIGN KEY (previous_copy)
+ REFERENCES asset.copy (id) MATCH SIMPLE
+ ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
+ CONSTRAINT action_hold_request_reset_reason_entry_requestor_fkey FOREIGN KEY (requestor)
+ REFERENCES actor.usr (id) MATCH SIMPLE
+ ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
+ CONSTRAINT action_hold_request_reset_reason_entry_requestor_workstation_fkey FOREIGN KEY (requestor_workstation)
+ REFERENCES actor.workstation (id) MATCH SIMPLE
+ ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED,
+ CONSTRAINT action_hold_request_reset_reason_entry_hold_fkey FOREIGN KEY (hold)
+ REFERENCES action.hold_request (id) MATCH SIMPLE
+ ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED
+)
+
+WITH (
+ OIDS=FALSE
+);
+
+INSERT INTO config.global_flag (name, label, enabled)
+ VALUES (
+ 'circ.holds.retarget_previous_targets_interval',
+ oils_i18n_gettext(
+ 'circ.holds.retarget_previous_targets_interval',
+ 'Hold targeter will create proximity adjustments for previously targeted copies within this time interval (in days).',
+ 'cgf',
+ 'label'
+ ),
+ TRUE
+ );
--- /dev/null
+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
[%- IF ctx.library.mailing_address; %]
<div id="addresses">
+ [%- IF ctx.library.mailing_address -%]
<div id="mailing" property="location address" typeof="PostalAddress">
<h3 property="contactType">[% l('Mailing address') %]</h3>
<span property="streetAddress">[% ctx.mailing_address.street1 | html %]
<span property="addressRegion">[% ctx.mailing_address.state | html %]</span><br />
<span property="addressCountry">[% ctx.mailing_address.country | html %]</span><br />
<span property="postalCode">[% ctx.mailing_address.post_code | html %]</span><br />
+ [%- IF ctx.mailing_address.latitude AND ctx.mailing_address.longitude -%]
+ <div>
+ <iframe width="500" height="400" frameborder="0" src="https://www.bing.com/maps/embed?h=400&w=500&cp=[% ctx.mailing_address.latitude %]~[% ctx.mailing_address.longitude %]&sp=point.[% ctx.mailing_address.latitude %]_[% ctx.mailing_address.longitude %]_[% ctx.library.name %]&lvl=16&typ=o&sty=r&src=SHELL&FORM=MBEDV8" scrolling="no">
+ </iframe>
+ <div style="white-space: nowrap; text-align: center; width: 500px; padding: 6px 0;">
+ <a id="largeMapLink" target="_blank" href="https://www.bing.com/maps?cp=[% ctx.mailing_address.latitude %]~[% ctx.mailing_address.longitude %]&sp=point.[% ctx.mailing_address.latitude %]_[% ctx.mailing_address.longitude %]_[% ctx.library.name %]&sty=r&lvl=16&FORM=MBEDLD">View Larger Map with Pin</a>
+ </div>
+ </div>
+ [%- END -%]
</div>
+ [%- END; -%]
</div>
[%- END; %]
</div>
<div class="row">
<div class="col-md-2">
+ <span>[% l('Shipping Hub OU') %]</span>
+ </div>
+ <div class="col-md-9">
+ <select class="form-control" ng-model="selectedOUType">
+ <option ng-repeat="t in outypes" value="{{t}}" ng-selected="t == selectedOUType">
+ {{getOUTypeLabel(t)}}
+ </option>
+ </select>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-2">
<button class="form-control" ng-click="reset()">[% l('Reset Form') %]</button>
</div>
<div class="col-md-9">
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
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
* 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
-- 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();
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'));
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,
(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';
'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;
'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 $$;
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 );
</div>
</td>
</tr>
+ <tr>
+ <th>&conify.org_unit.editor_pane.shipping_hub;</th>
+ <td>
+ <div
+ id="editor_pane_shipping_hub_ou"
+ dojoType="dijit.form.FilteringSelect"
+ jsId="editor_pane_shipping_hub_ou"
+ store="ou_list_store"
+ searchAttr="shortname"
+ ignoreCase="true"
+ >
+ <script type="dojo/method" event="onChange">
+<![CDATA[
+ if (current_ou && this.getValue()) this.store.setValue( current_ou, "shipping_hub_ou", this.getValue() );
+
+]]>
+ </script>
+ </div>
+ </td>
+ </tr>
<tr>
<th>&conify.org_unit.editor_pane.opac_visible;</th>
<td>
<!ENTITY conify.org_unit.editor_pane.main_phone "Main Phone Number">
<!ENTITY conify.org_unit.editor_pane.org_unit_type "Organization Unit Type">
<!ENTITY conify.org_unit.editor_pane.parent "Parent Organization Unit">
+<!ENTITY conify.org_unit.editor_pane.shipping_hub "Shipping Hub Organization Unit">
<!ENTITY conify.org_unit.editor_pane.opac_visible "OPAC Visible">
<!-- Hours of operation -->
<!ENTITY conify.org_unit.hoo_pane.title "Hours of Operation">