try-catching any calls to hold reset reasons in circulation module so that it'll... user/lew/retargeter_hold_notes_geosort_squash
authorLlewellyn Marshall <llewellyn.marshall@ncdcr.gov>
Mon, 26 Sep 2022 18:38:32 +0000 (14:38 -0400)
committerLlewellyn Marshall <llewellyn.marshall@ncdcr.gov>
Fri, 3 Mar 2023 14:36:38 +0000 (09:36 -0500)
sql for reset reasons

make proximity adjustments based on reset reasons if circ.holds.retarget_previous_targets_interval greater than 0. For each previous copy on a hold, reset reasons with MANUAL_RESET will increase the proximity by an amount equal to the maximum proximity while TIMED_OUT will apply a +1 prox adjustment per occurence within that interval.

log retarget only if it was successful

Don't create reset reason in hold targeter if hold arg is defined. Try catch any errors from hold reset note in hold-targeter application. run reset reason entry search within eval in case of failure.

fix tab issues.

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.

change from max_prox to max_prox+1 so retargeted copies always appear outside of initial list even if prox is 0.

38 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/examples/opensrf.xml.example
Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
Open-ILS/src/eg2/src/app/staff/admin/server/admin-server.module.ts
Open-ILS/src/eg2/src/app/staff/admin/server/org-unit-shipping-hub-distance.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts
Open-ILS/src/extras/install/Makefile.debian-bullseye
Open-ILS/src/extras/install/Makefile.debian-buster
Open-ILS/src/extras/install/Makefile.debian-stretch
Open-ILS/src/extras/install/Makefile.fedora
Open-ILS/src/extras/install/Makefile.ubuntu-bionic
Open-ILS/src/extras/install/Makefile.ubuntu-focal
Open-ILS/src/perlmods/Build.PL
Open-ILS/src/perlmods/MANIFEST
Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Geo.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/HoldTargeter.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/actor.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Driver/Pg/dbi.pm
Open-ILS/src/perlmods/lib/OpenILS/Utils/HoldTargeter.pm
Open-ILS/src/perlmods/lib/OpenILS/Utils/VicinityCalculator.pm [new file with mode: 0644]
Open-ILS/src/sql/Pg/005.schema.actors.sql
Open-ILS/src/sql/Pg/090.schema.action.sql
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/xxxx.function.ahcm_shipping_distance.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/xxxx.hold_reset_reasons.sql [new file with mode: 0644]
Open-ILS/src/sql/Pg/upgrade/xxxx.schema.actor_org_unit_shipping_hub.sql [new file with mode: 0644]
Open-ILS/src/templates/opac/parts/library/core_info.tt2
Open-ILS/src/templates/staff/admin/actor/org_unit/t_main_tab.tt2
Open-ILS/tests/datasets/sql/assets_concerto.sql
Open-ILS/tests/datasets/sql/env_create.sql
Open-ILS/tests/datasets/sql/env_destroy.sql
Open-ILS/tests/datasets/sql/libraries.sql
Open-ILS/tests/datasets/sql/transactions.sql
Open-ILS/web/conify/global/actor/org_unit.html
Open-ILS/web/opac/locale/en-US/conify.dtd

index 0f22734..669ffea 100644 (file)
@@ -4339,6 +4339,26 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
                        <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"/>
@@ -5765,6 +5785,7 @@ SELECT  usr,
                        <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"/>
@@ -7244,6 +7265,7 @@ SELECT  usr,
                        <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"/>
@@ -7273,6 +7295,7 @@ SELECT  usr,
                        <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"/>
index 05d945f..eeec62f 100644 (file)
@@ -1470,6 +1470,7 @@ vim:et:ts=4:sw=4:
                 <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>
index c6a4108..1c161ec 100644 (file)
@@ -79,6 +79,8 @@
       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"  
index 9f49a7d..8e80c8f 100644 (file)
@@ -3,6 +3,7 @@ import {TreeModule} from '@eg/share/tree/tree.module';
 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';
@@ -18,6 +19,7 @@ generated UI's into lazy-loadable sub-mobules. */
   declarations: [
       AdminServerSplashComponent,
       OrgUnitTypeComponent,
+      OrgUnitShippingHubDistanceComponent,
       PrintTemplateComponent,
       PermGroupTreeComponent,
       PermGroupMapDialogComponent
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/server/org-unit-shipping-hub-distance.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/server/org-unit-shipping-hub-distance.component.ts
new file mode 100644 (file)
index 0000000..522b666
--- /dev/null
@@ -0,0 +1,122 @@
+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;}
+            );
+    }
+}
+
+
index caadbcb..309270b 100644 (file)
@@ -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',
index 0cc634d..03d727b 100644 (file)
@@ -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 \
index 57fae84..873543b 100644 (file)
@@ -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 \
index d242aa6..a8d5fdd 100644 (file)
@@ -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 \
index 907bd99..2d1bb99 100644 (file)
@@ -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 \
index 11a2ff5..2d479e5 100644 (file)
@@ -94,6 +94,7 @@ export DEB_APACHE_DISCONF = \
 
 export CPAN_MODULES = \
        Geo::Coder::Google \
+       Geo::Coder::Bing \
        Business::OnlinePayment::PayPal \
        Email::Send \
        MARC::Charset \
index 74badd8..804cdfc 100644 (file)
@@ -94,6 +94,7 @@ export DEB_APACHE_DISCONF = \
 
 export CPAN_MODULES = \
        Geo::Coder::Google \
+       Geo::Coder::Bing \
        Business::OnlinePayment::PayPal \
        Email::Send \
        MARC::Charset \
index 5c32308..9ca2161 100644 (file)
@@ -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',
index 37fb090..83970b4 100644 (file)
@@ -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
index c0d9851..8f1030e 100644 (file)
@@ -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"
 );
index 29dec09..1a8e2a9 100644 (file)
@@ -1868,15 +1868,22 @@ sub handle_checkout_holds {
     
         $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);
 
@@ -2651,8 +2658,13 @@ sub checkin_retarget {
                 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', 
@@ -3306,8 +3318,13 @@ sub attempt_checkin_hold_capture {
     $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);
@@ -3451,9 +3468,14 @@ sub retarget_holds {
     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;
 }
index 0dd2ad4..e16f44e 100644 (file)
@@ -1107,20 +1107,20 @@ sub cancel_hold {
     $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;
@@ -1350,7 +1350,7 @@ sub update_hold_impl {
    
     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.
@@ -2247,8 +2247,8 @@ sub create_reset_reason_entry
 {
     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};
@@ -2257,9 +2257,13 @@ sub create_reset_reason_entry
         @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;
@@ -2267,18 +2271,18 @@ sub create_reset_reason_entry
 
 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;
 }
index 3f735e5..a698820 100644 (file)
@@ -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};
index 7b54b39..7305015 100644 (file)
@@ -6,6 +6,7 @@ use base qw/OpenILS::Application/;
 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',
@@ -89,16 +90,6 @@ sub 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) };
 
@@ -108,6 +99,24 @@ sub hold_targeter {
             $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
index 6bf0481..19c42a0 100644 (file)
@@ -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/;
 
index 0d2b49c..62aaed0 100644 (file)
     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' );
index 31b8852..08b94a2 100644 (file)
@@ -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,15 @@ 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::Const qw/:const/;
 use OpenILS::Application::AppUtils;
 use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use List::Util qw(shuffle max);
 
 sub new {
     my ($class, %args) = @_;
@@ -298,6 +303,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}) {
@@ -367,6 +388,20 @@ sub result {
     };
 }
 
+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 {
@@ -735,17 +770,63 @@ sub get_copy_circ_libs {
 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, 
@@ -753,8 +834,21 @@ 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 $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(
@@ -1147,12 +1241,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 +1270,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 +1288,37 @@ sub find_nearest_copy {
             );
             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);
+
+            }
         }
     }
 
@@ -1213,6 +1333,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 (file)
index 0000000..d4d46a4
--- /dev/null
@@ -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
index b18a33c..a26bd3b 100644 (file)
@@ -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;
index 711269e..ccfc246 100644 (file)
@@ -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,
index c3d2745..8fef92a 100644 (file)
@@ -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 (file)
index 0000000..24ffea9
--- /dev/null
@@ -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.hold_reset_reasons.sql b/Open-ILS/src/sql/Pg/upgrade/xxxx.hold_reset_reasons.sql
new file mode 100644 (file)
index 0000000..1de4750
--- /dev/null
@@ -0,0 +1,66 @@
+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
+    );
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 (file)
index 0000000..900793d
--- /dev/null
@@ -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
index cdaf047..9e45c43 100644 (file)
@@ -31,6 +31,7 @@
 
     [%- 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 %]&amp;sp=point.[% ctx.mailing_address.latitude %]_[% ctx.mailing_address.longitude %]_[% ctx.library.name %]&amp;sty=r&amp;lvl=16&amp;FORM=MBEDLD">View Larger Map with Pin</a>
+                </div>
+            </div>
+            [%- END -%]
         </div>
+        [%- END; -%]
     </div>
     [%- END; %]
 
index daba717..cbd29ab 100644 (file)
     </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">
index 82e050f..2dfb087 100644 (file)
@@ -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
index fe9dbea..9bd5b6f 100644 (file)
@@ -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
index 6169194..e3b42fa 100644 (file)
@@ -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();
index 513dc2b..34b548e 100644 (file)
@@ -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';
index cb9a73c..3a73ff2 100644 (file)
@@ -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 $$;
 
index faaac24..2a3b59a 100644 (file)
                                                        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>
index c0cf5a7..128637c 100644 (file)
@@ -32,6 +32,7 @@
 <!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">