[open-ils-commits] [GIT] Evergreen ILS branch master updated. cbf3c8636645af6b5831a40ff3f75c2fc5cf987f

Evergreen Git git at git.evergreen-ils.org
Wed Feb 27 11:03:45 EST 2013


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  cbf3c8636645af6b5831a40ff3f75c2fc5cf987f (commit)
       via  6b53189a9134d7366d4fd9bb7f0a8b29ee304df5 (commit)
       via  fc048034c0a45795057dcea75ad2e876b6d5785a (commit)
      from  e1337996b6a6c743997be58f8070d49010845271 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit cbf3c8636645af6b5831a40ff3f75c2fc5cf987f
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Feb 27 11:02:47 2013 -0500

    Stamping upgrade scripts for Org Unit Proximity Adjustment and Custom Best-Hold Sort Order
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 4f84162..a686ea5 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -87,7 +87,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0758', :eg_version); -- jeff davis / miker
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('0760', :eg_version); -- senator/miker
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.org_prox_adjust.sql b/Open-ILS/src/sql/Pg/upgrade/0759.schema.org_prox_adjust.sql
similarity index 98%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.org_prox_adjust.sql
rename to Open-ILS/src/sql/Pg/upgrade/0759.schema.org_prox_adjust.sql
index 43876d8..a83a844 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.org_prox_adjust.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0759.schema.org_prox_adjust.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('0759', :eg_version);
+
 CREATE TABLE actor.org_unit_proximity_adjustment (
     id                  SERIAL   PRIMARY KEY,
     item_circ_lib       INT         REFERENCES actor.org_unit (id),
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXYY.schema.custom-best-hold-selection.sql b/Open-ILS/src/sql/Pg/upgrade/0760.schema.custom-best-hold-selection.sql
similarity index 98%
rename from Open-ILS/src/sql/Pg/upgrade/XXYY.schema.custom-best-hold-selection.sql
rename to Open-ILS/src/sql/Pg/upgrade/0760.schema.custom-best-hold-selection.sql
index 55e3906..3cacb3d 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXYY.schema.custom-best-hold-selection.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/0760.schema.custom-best-hold-selection.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('0760', :eg_version);
+
 CREATE TABLE config.best_hold_order(
     id          SERIAL      PRIMARY KEY,    -- (metadata)
     name        TEXT        UNIQUE,   -- i18n (metadata)

commit 6b53189a9134d7366d4fd9bb7f0a8b29ee304df5
Author: Lebbeous Fogle-Weekley <lebbeous at esilibrary.com>
Date:   Thu Dec 13 14:45:41 2012 -0500

    Custom best-hold selection sort order
    
    The ranking algorithm that chooses the best hold to target a copy in
    hand at a capture time used to be fairly simple.  It had two modes, FIFO
    and not-FIFO, and that was it.
    
    This change allows full configuration of that algorithm.  In other
    words, when the system captures a copy and sets out to evaluate what
    hold, if any, that copy might best fulfull, site staff of sufficient
    permission level are now empowered to choose exactly which comparisons
    the systems makes in what order.  This gives said staff much greater
    flexibililty than they have today over holds policy.
    
    For more information, see the included tech spec documents.
    
    Signed-off-by: Lebbeous Fogle-Weekley <lebbeous at esilibrary.com>
    
    Conflicts:
    	Open-ILS/src/sql/Pg/002.schema.config.sql
    	Open-ILS/src/sql/Pg/950.data.seed-values.sql
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 771ab0b..f656573 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -2358,6 +2358,32 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             </actions>
         </permacrud>
 	</class>
+	<class id="cbho" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::best_hold_order" oils_persist:tablename="config.best_hold_order" reporter:label="Best-Hold Sort Order">
+		<fields oils_persist:primary="id" oils_persist:sequence="config.best_hold_order_id_seq">
+			<field reporter:label="ID" name="id" reporter:datatype="id" reporter:selector="name" />
+			<field reporter:label="Name" name="name" reporter:datatype="text"/>
+			<field reporter:label="Capture Lib to Pickup Lib Proximity" name="pprox" reporter:datatype="int" />
+			<field reporter:label="Circ Lib to Request Lib Proximity" name="hprox" reporter:datatype="int" />
+			<field reporter:label="Adjusted Circ Lib to Pickup Lib Proximity" name="aprox" reporter:datatype="int" />
+			<field reporter:label="Adjusted Capture Location to Pickup Lib Proximity" name="approx" reporter:datatype="int" />
+			<field reporter:label="Hold Priority" name="priority" reporter:datatype="int" />
+			<field reporter:label="Hold Cut-in-line State" name="cut" reporter:datatype="int" />
+			<field reporter:label="Hold Selection Depth" name="depth" reporter:datatype="int" />
+			<field reporter:label="Copy Has Circulated From Home Lately" name="htime" reporter:datatype="int" />
+			<field reporter:label="Hold Request Time" name="rtime" reporter:datatype="int" />
+			<field reporter:label="Copy Has Been Home At All Lately" name="shtime" reporter:datatype="int" />
+		</fields>
+		<links>
+		</links>
+		<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+			<actions>
+				<create permission="ADMIN_HOLD_CAPTURE_SORT" global_required="true"/>
+				<retrieve permission="ADMIN_HOLD_CAPTURE_SORT" global_required="true"/>
+				<update permission="ADMIN_HOLD_CAPTURE_SORT" global_required="true"/>
+				<delete permission="ADMIN_HOLD_CAPTURE_SORT" global_required="true"/>
+			</actions>
+		</permacrud>
+	</class>
 	<class id="cbfp" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::biblio_fingerprint" oils_persist:tablename="config.biblio_fingerprint" reporter:label="Fingerprint Definition">
 		<fields oils_persist:primary="id" oils_persist:sequence="config.biblio_fingerprint_id_seq">
 			<field name="id" reporter:datatype="id" />
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
index 40cf63e..03f6c98 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Holds.pm
@@ -3000,10 +3000,10 @@ sub find_nearest_permitted_hold {
 
     my $fifo = $U->ou_ancestor_setting_value($user->ws_ou, 'circ.holds_fifo');
 
-    # search for what should be the best holds for this copy to fulfill
-    my $best_holds = $U->storagereq(
-        "open-ils.storage.action.hold_request.nearest_hold.atomic",
-        $user->ws_ou, $copy->id, 100, $hold_stall_interval, $fifo );
+	# search for what should be the best holds for this copy to fulfill
+	my $best_holds = $U->storagereq(
+        "open-ils.storage.action.hold_request.nearest_hold.atomic", 
+		$user->ws_ou, $copy, 100, $hold_stall_interval, $fifo );
 
     # Add any pre-targeted holds to the list too? Unless they are already there, anyway.
     if ($old_holds) {
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
index 60fe6b7..05dedcf 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
@@ -17,6 +17,32 @@ use OpenILS::Application::Circ::CircCommon;
 use OpenILS::Application::AppUtils;
 my $U = "OpenILS::Application::AppUtils";
 
+# used in build_hold_sort_clause()
+my %HOLD_SORT_ORDER_BY = (
+    pprox => 'p.prox',
+    hprox => 'actor.org_unit_proximity(%d, h.request_lib)',  # $cp->circ_lib
+    aprox => 'COALESCE(hm.proximity, p.prox)',
+    approx => 'action.hold_copy_calculated_proximity(h.id, %d, %d)', # $cp,$here
+    priority => 'pgt.hold_priority',
+    cut => 'CASE WHEN h.cut_in_line IS TRUE THEN 0 ELSE 1 END',
+    depth => 'h.selection_depth',
+    rtime => 'h.request_time',
+    htime => q!
+        CASE WHEN
+            copy_has_not_been_home.result
+        THEN actor.org_unit_proximity(%d, h.request_lib)
+        ELSE 999
+        END
+    !,
+    shtime => q!
+        CASE WHEN
+            copy_has_not_been_home_even_to_idle.result
+        THEN actor.org_unit_proximity(%d, h.request_lib)
+        ELSE 999
+        END
+    !,
+);
+
 
 sub isTrue {
 	my $v = shift;
@@ -282,22 +308,183 @@ __PACKAGE__->register_method(
 	method          => 'grab_overdue',
 );
 
+sub get_hold_sort_order {
+    my ($ou) = @_;
+
+    my $dbh = action::hold_request->db_Main;
+
+    # The purpose of this function is to return column names in a DB-configured
+    # order, so it won't do to add columns here or change column names unless
+    # you also change the expectation of anything calling this function.
+
+    my $row = $dbh->selectrow_hashref(
+        q!
+        SELECT
+            cbho.pprox, cbho.hprox, cbho.aprox, cbho.approx, cbho.priority,
+            cbho.cut, cbho.depth, cbho.htime, cbho.shtime, cbho.rtime
+        FROM config.best_hold_order cbho
+        WHERE id = (
+            SELECT oils_json_to_text(value)::INT
+            FROM actor.org_unit_ancestor_setting('circ.hold_capture_order', ?)
+        )
+        !, undef, $ou
+    ) || {
+        pprox => 1, hprox => 8, aprox => 2, priority => 3,
+        cut => 4, depth => 5, htime => 7, rtime => 6
+    };
+
+    # Return only the keys of our hash, sorted by value,
+    # keys for null values omitted.
+    return [
+        grep { defined $row->{$_} } (
+            sort {$row->{$a} cmp $row->{$b}} keys %$row
+        )
+    ];
+}
+
+# Returns an ORDER BY clause
+# *and* a string with a CTE expression to precede the nearest-hold SQL query
+# *and* a string with extra JOIN statements needed
+sub build_hold_sort_clause {
+    my ($columns, $cp, $here) = @_;
+
+    my %order_by_sprintf_args = (
+        hprox => [$cp->circ_lib],
+        approx => [$cp->id, $here],
+        htime => [$cp->circ_lib],
+        shtime => [$cp->circ_lib]
+    );
+
+    my @clauses;
+    my $ctes_needed = 0;
+    foreach my $col (@$columns) {
+        if ($col eq 'htime' and not $ctes_needed) {
+            $ctes_needed = 1;
+        } elsif ($col eq 'shtime') {
+            $ctes_needed = 2;
+        }
+
+        my @args;
+        @args = @{$order_by_sprintf_args{$col}} if
+            exists $order_by_sprintf_args{$col};
+
+        push @clauses, sprintf($HOLD_SORT_ORDER_BY{$col}, @args);
+
+        last if $col eq 'rtime';    # rtime is effectively unique, no need for
+                                    # more order-by clauses after that.
+    }
+
+    my ($ctes, $joins);
+    if ($ctes_needed >= 1) {
+        # For our first auxiliary query, the question we seek to answer is, "has
+        # our copy been circulating away from home too long?" Two parts to
+        # answer this question.
+        #
+        # part 1: Have their been no checkouts at the copy's circ_lib since the
+        # beginning of our go-home interval?
+        # part 2: Was the last transit to affect our copy before the beginning
+        # of our go-home interval an outbound transit? i.e. away from circ-lib
+
+        # [We use sprintf because the outer function that's going to send one
+        # big query through DBI is blind to our process of dynamically building
+        # these CTEs, and it wouldn't know what bind parameters to pass unless
+        # we did a lot more work here. This is injection-safe because we only
+        # use the %d formatter.]
+        $ctes .= sprintf(q!
+, copy_has_not_been_home AS (
+    SELECT (
+        -- part 1
+        SELECT circ.id FROM action.circulation circ
+        JOIN go_home_interval ON (true)
+        WHERE
+            circ.target_copy = %d AND
+            circ.circ_lib = %d AND
+            circ.xact_start >= NOW() - go_home_interval.value
+    ) IS NULL AND (
+        -- part 2
+        SELECT atc.dest <> %d FROM action.transit_copy atc
+        JOIN go_home_interval ON (true)
+        WHERE
+            atc.id = (
+                SELECT MAX(id) FROM action.transit_copy atc_inner
+                WHERE
+                    atc_inner.target_copy = %d AND
+                    atc_inner.source_send_time < NOW() - go_home_interval.value
+            )
+    ) AS result
+) !, $cp->id, $cp->circ_lib, $cp->circ_lib, $cp->id);
+        $joins .= " JOIN copy_has_not_been_home ON (true) ";
+    }
+
+    if ($ctes_needed == 2) {
+        # In this auxiliary query, we ask the question, "has our copy come home
+        # by any means that we can determine, even if it didn't circulate once
+        # it came home, in the time defined by the go-home-interval?"
+        # answer this question. Two parts to this too (besides including the
+        # previous auxiliary query).
+        #
+        # 1: there have been no homebound transits for this copy since the
+        # beginning of the go-home interval.
+        # 2: there have been no checkins at home since the beginning of
+        # the go-home interval for this copy
+
+        $ctes .= sprintf(q!
+, copy_has_not_been_home_even_to_idle AS (
+    SELECT
+        copy_has_not_been_home.response AND (
+            -- part 1
+            SELECT atc.id FROM action.transit_copy atc
+            JOIN go_home_interval ON (true)
+            WHERE
+                atc.target_copy = %d AND
+                atc.dest = %d AND
+                atc.dest_recv_time >= NOW() - go_home_interval.value
+        ) IS NULL AND (
+            -- part 2
+            SELECT circ.id FROM action.circulation circ
+            JOIN go_home_interval ON (true)
+            WHERE
+                circ.target_copy = %d AND
+                circ.checkin_lib = %d AND
+                circ.checkin_time >= NOW() - go_home_interval.value
+        ) IS NULL
+    AS result
+) !, $cp->id, $cp->circ_lib, $cp->id, $cp->circ_lib);
+        $joins .= " JOIN copy_has_not_been_home_even_to_idle ON (true) ";
+    }
+
+    return (
+        join(", ", @clauses),
+        $ctes,
+        $joins
+    );
+}
+
 sub nearest_hold {
 	my $self = shift;
 	my $client = shift;
-	my $here = shift;
-	my $cp = shift;
+	my $here = shift;   # just the ID
+	my $cp = shift;     # now an object, formerly just the ID
 	my $limit = int(shift()) || 10;
 	my $age = shift() || '0 seconds';
 	my $fifo = shift();
 
-	local $OpenILS::Application::Storage::WRITE = 1;
+    $log->info("deprecated 'fifo' param true, but ignored") if isTrue $fifo;
 
-	my $holdsort = isTrue($fifo) ?
-			"pgt.hold_priority, CASE WHEN h.cut_in_line IS TRUE THEN 0 ELSE 1 END, h.request_time, h.selection_depth DESC, COALESCE(hm.proximity, h.prox) " :
-			"COALESCE(hm.proximity, h.prox), pgt.hold_priority, CASE WHEN h.cut_in_line IS TRUE THEN 0 ELSE 1 END, h.selection_depth DESC, h.request_time ";
+    my ($holdsort, $addl_cte, $addl_join) =
+        build_hold_sort_clause(get_hold_sort_order($here), $cp, $here);
 
-	my $ids = action::hold_request->db_Main->selectcol_arrayref(<<"	SQL", {}, $here, $cp, $age);
+	local $OpenILS::Application::Storage::WRITE = 1;
+
+	my $ids = action::hold_request->db_Main->selectcol_arrayref(<<"	SQL", {}, $cp->circ_lib, $here, $cp->id, $age);
+        WITH go_home_interval AS (
+            SELECT OILS_JSON_TO_TEXT(
+                (SELECT value FROM actor.org_unit_ancestor_setting(
+                    'circ.hold_go_home_interval', ?
+                )
+            ))::INTERVAL AS value
+        )
+        $addl_cte
 		SELECT	h.id
 		  FROM	action.hold_request h
 			JOIN actor.org_unit_proximity p ON (p.from_org = ? AND p.to_org = h.pickup_lib)
@@ -308,6 +495,7 @@ sub nearest_hold {
 				ON ( au.id = ausp.usr AND ( ausp.stop_date IS NULL OR ausp.stop_date > NOW() ) )
 		  	LEFT JOIN config.standing_penalty csp
 				ON ( csp.id = ausp.standing_penalty AND csp.block_list LIKE '%CAPTURE%' )
+            $addl_join
 		  WHERE hm.target_copy = ?
 		  	AND (AGE(NOW(),h.request_time) >= CAST(? AS INTERVAL) OR p.prox = 0)
 			AND h.capture_time IS NULL
diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 10ad215..4f84162 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -979,7 +979,6 @@ CREATE TABLE config.usr_activity_type (
 CREATE UNIQUE INDEX unique_wwh ON config.usr_activity_type 
     (COALESCE(ewho,''), COALESCE (ewhat,''), COALESCE(ehow,''));
 
-
 CREATE TABLE config.filter_dialog_interface (
     key         TEXT                        PRIMARY KEY,
     description TEXT
@@ -996,5 +995,31 @@ CREATE TABLE config.filter_dialog_filter_set (
     CONSTRAINT cfdfs_name_once_per_lib UNIQUE (name, owning_lib)
 );
 
+CREATE TABLE config.best_hold_order(
+    id          SERIAL      PRIMARY KEY,
+    name        TEXT        UNIQUE,   -- i18n
+    pprox       INT, -- copy capture <-> pickup lib prox
+    hprox       INT, -- copy circ lib <-> request lib prox
+    aprox       INT, -- copy circ lib <-> pickup lib ADJUSTED prox on ahcm
+    approx      INT, -- copy capture <-> pickup lib ADJUSTED prox from function
+    priority    INT, -- group hold priority
+    cut         INT, -- cut-in-line
+    depth       INT, -- selection depth
+    htime       INT, -- time since last home-lib circ exceeds org-unit setting
+    rtime       INT, -- request time
+    shtime      INT  -- time since copy last trip home exceeds org-unit setting
+);
+
+-- At least one of these columns must contain a non-null value
+ALTER TABLE config.best_hold_order ADD CHECK ((
+    pprox IS NOT NULL OR
+    hprox IS NOT NULL OR
+    aprox IS NOT NULL OR
+    priority IS NOT NULL OR
+    cut IS NOT NULL OR
+    depth IS NOT NULL OR
+    htime IS NOT NULL OR
+    rtime IS NOT NULL
+));
 
 COMMIT;
diff --git a/Open-ILS/src/sql/Pg/005.schema.actors.sql b/Open-ILS/src/sql/Pg/005.schema.actors.sql
index 176f465..e6d8cd6 100644
--- a/Open-ILS/src/sql/Pg/005.schema.actors.sql
+++ b/Open-ILS/src/sql/Pg/005.schema.actors.sql
@@ -384,7 +384,7 @@ CREATE TABLE actor.org_unit_proximity_adjustment (
     id                  SERIAL   PRIMARY KEY,
     item_circ_lib       INT         REFERENCES actor.org_unit (id),
     item_owning_lib     INT         REFERENCES actor.org_unit (id),
-    copy_location       INT         REFERENCES asset.copy_location (id),
+    copy_location       INT,        -- REFERENCES asset.copy_location (id),
     hold_pickup_lib     INT         REFERENCES actor.org_unit (id),
     hold_request_lib    INT         REFERENCES actor.org_unit (id),
     pos                 INT         NOT NULL DEFAULT 0,
diff --git a/Open-ILS/src/sql/Pg/090.schema.action.sql b/Open-ILS/src/sql/Pg/090.schema.action.sql
index 3f7bb6f..fd82cf0 100644
--- a/Open-ILS/src/sql/Pg/090.schema.action.sql
+++ b/Open-ILS/src/sql/Pg/090.schema.action.sql
@@ -973,7 +973,16 @@ query-based fieldsets.
 Returns NULL if successful, or an error message if not.
 $$;
 
-CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity(ahr_id INT, acp_id BIGINT, context_ou INT DEFAULT NULL) RETURNS NUMERIC AS $f$
+CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity(
+    ahr_id INT,
+    acp_id BIGINT,
+    copy_context_ou INT DEFAULT NULL
+    -- TODO maybe? hold_context_ou INT DEFAULT NULL.  This would optionally
+    -- support an "ahprox" measurement: adjust prox between copy circ lib and
+    -- hold request lib, but I'm unsure whether to use this theoretical
+    -- argument only in the baseline calculation or later in the other
+    -- queries in this function.
+) RETURNS NUMERIC AS $f$
 DECLARE
     aoupa           actor.org_unit_proximity_adjustment%ROWTYPE;
     ahr             action.hold_request%ROWTYPE;
@@ -995,17 +1004,17 @@ BEGIN
     SELECT * INTO acn FROM asset.call_number WHERE id = acp.call_number;
     SELECT * INTO acl FROM asset.copy_location WHERE id = acp.location;
 
-    IF context_ou IS NULL THEN
-        context_ou := acp.circ_lib;
+    IF copy_context_ou IS NULL THEN
+        copy_context_ou := acp.circ_lib;
     END IF;
 
     -- First, gather the baseline proximity of "here" to pickup lib
-    SELECT prox INTO baseline_prox FROM actor.org_unit_proximity WHERE from_org = context_ou AND to_org = ahr.pickup_lib;
+    SELECT prox INTO baseline_prox FROM actor.org_unit_proximity WHERE from_org = copy_context_ou AND to_org = ahr.pickup_lib;
 
     -- Find any absolute adjustments, and set the baseline prox to that
     SELECT  adj.* INTO aoupa
       FROM  actor.org_unit_proximity_adjustment adj
-            LEFT JOIN actor.org_unit_ancestors_distance(context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
+            LEFT JOIN actor.org_unit_ancestors_distance(copy_context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
             LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
             LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acn_ol.id = adj.copy_location)
             LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
@@ -1030,7 +1039,7 @@ BEGIN
     FOR aoupa IN
         SELECT  adj.* 
           FROM  actor.org_unit_proximity_adjustment adj
-                LEFT JOIN actor.org_unit_ancestors_distance(context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
+                LEFT JOIN actor.org_unit_ancestors_distance(copy_context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
                 LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
                 LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acn_ol.id = adj.copy_location)
                 LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
diff --git a/Open-ILS/src/sql/Pg/800.fkeys.sql b/Open-ILS/src/sql/Pg/800.fkeys.sql
index 36e9438..fb7b07d 100644
--- a/Open-ILS/src/sql/Pg/800.fkeys.sql
+++ b/Open-ILS/src/sql/Pg/800.fkeys.sql
@@ -40,6 +40,7 @@ ALTER TABLE actor.org_unit ADD CONSTRAINT actor_org_unit_holds_address_fkey FORE
 ALTER TABLE actor.org_unit ADD CONSTRAINT actor_org_unit_ill_address_fkey FOREIGN KEY (ill_address) REFERENCES actor.org_address (id) DEFERRABLE INITIALLY DEFERRED;
 
 ALTER TABLE actor.org_unit_proximity_adjustment ADD CONSTRAINT actor_org_unit_proximity_adjustment_circ_mod_fkey FOREIGN KEY (circ_mod) REFERENCES config.circ_modifier (code) DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE actor.org_unit_proximity_adjustment ADD CONSTRAINT actor_org_unit_proximity_copy_location_fkey FOREIGN KEY (copy_location) REFERENCES asset.copy_location (id) DEFERRABLE INITIALLY DEFERRED;
 
 ALTER TABLE acq.provider ADD CONSTRAINT acq_provider_edi_default_fkey FOREIGN KEY (edi_default) REFERENCES acq.edi_account (id) DEFERRABLE INITIALLY DEFERRED;
 
diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
index 37e0d32..51d608f 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -1582,9 +1582,9 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES
  ( 544, 'URL_VERIFY_UPDATE_SETTINGS', oils_i18n_gettext( 544, 
     'Allows a user to configure URL verification org unit settings', 'ppl', 'description')),
  ( 545, 'SAVED_FILTER_DIALOG_FILTERS', oils_i18n_gettext( 545,
-    'Allows users to save and load sets of filters for filter dialogs, available in certain staff interfaces', 'ppl', 'description'))
-
-
+    'Allows users to save and load sets of filters for filter dialogs, available in certain staff interfaces', 'ppl', 'description')),
+ ( 546, 'ADMIN_HOLD_CAPTURE_SORT', oils_i18n_gettext( 546,
+        'Allows a user to make changes to best-hold selection sort order', 'ppl', 'description'))
 ;
 
 
@@ -12412,3 +12412,104 @@ INSERT INTO config.metabib_class_ts_map(field_class, ts_config, index_weight, al
     ('subject','simple','A',true),
     ('subject','english_nostop','C',true),
     ('identifier','simple','A',true);
+
+INSERT INTO config.org_unit_setting_type (
+    name, label, description, datatype, fm_class, update_perm, grp
+) VALUES (
+    'circ.hold_capture_order',
+    oils_i18n_gettext(
+        'circ.hold_capture_order',
+        'Best-hold selection sort order',
+        'coust',
+        'label'
+    ),
+    oils_i18n_gettext(
+        'circ.hold_capture_order',
+        'Defines the sort order of holds when selecting a hold to fill using a given copy at capture time',
+        'coust',
+        'description'
+    ),
+    'link',
+    'cbho',
+    543,
+    'holds'
+);
+
+INSERT INTO config.org_unit_setting_type (
+    name, label, description, datatype, update_perm, grp
+) VALUES (
+    'circ.hold_go_home_interval',
+    oils_i18n_gettext(
+        'circ.hold_go_home_interval',
+        'Max foreign-circulation time',
+        'coust',
+        'label'
+    ),
+    oils_i18n_gettext(
+        'circ.hold_go_home_interval',
+        'Time a copy can spend circulating away from its circ lib before returning there to fill a hold (if one exists there)',
+        'coust',
+        'description'
+    ),
+    'interval',
+    543,
+    'holds'
+);
+
+
+INSERT INTO config.best_hold_order (
+    name,
+    pprox, aprox, priority, cut, depth, rtime, htime, hprox
+) VALUES (
+    'Traditional',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    hprox, pprox, aprox, priority, cut, depth, rtime, htime
+) VALUES (
+    'Traditional with Holds-always-go-home',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    htime, hprox, pprox, aprox, priority, cut, depth, rtime
+) VALUES (
+    'Traditional with Holds-go-home',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    priority, cut, rtime, depth, pprox, hprox, aprox, htime
+) VALUES (
+    'FIFO',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    hprox, priority, cut, rtime, depth, pprox, aprox, htime
+) VALUES (
+    'FIFO with Holds-always-go-home',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    htime, priority, cut, rtime, depth, pprox, aprox, hprox
+) VALUES (
+    'FIFO with Holds-go-home',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO actor.org_unit_setting (
+    org_unit, name, value
+) VALUES (
+    (SELECT id FROM actor.org_unit WHERE parent_ou IS NULL),
+    'circ.hold_go_home_interval',
+    '"6 months"'
+);
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.org_prox_adjust.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.org_prox_adjust.sql
index d9eb082..43876d8 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.org_prox_adjust.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.org_prox_adjust.sql
@@ -32,7 +32,16 @@ CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABL
     SELECT * FROM org_unit_ancestors_distance;
 $$ LANGUAGE SQL STABLE ROWS 1;
 
-CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity(ahr_id INT, acp_id BIGINT, context_ou INT DEFAULT NULL) RETURNS NUMERIC AS $f$
+CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity(
+    ahr_id INT,
+    acp_id BIGINT,
+    copy_context_ou INT DEFAULT NULL
+    -- TODO maybe? hold_context_ou INT DEFAULT NULL.  This would optionally
+    -- support an "ahprox" measurement: adjust prox between copy circ lib and
+    -- hold request lib, but I'm unsure whether to use this theoretical
+    -- argument only in the baseline calculation or later in the other
+    -- queries in this function.
+) RETURNS NUMERIC AS $f$
 DECLARE
     aoupa           actor.org_unit_proximity_adjustment%ROWTYPE;
     ahr             action.hold_request%ROWTYPE;
@@ -54,17 +63,17 @@ BEGIN
     SELECT * INTO acn FROM asset.call_number WHERE id = acp.call_number;
     SELECT * INTO acl FROM asset.copy_location WHERE id = acp.location;
 
-    IF context_ou IS NULL THEN
-        context_ou := acp.circ_lib;
+    IF copy_context_ou IS NULL THEN
+        copy_context_ou := acp.circ_lib;
     END IF;
 
     -- First, gather the baseline proximity of "here" to pickup lib
-    SELECT prox INTO baseline_prox FROM actor.org_unit_proximity WHERE from_org = context_ou AND to_org = ahr.pickup_lib;
+    SELECT prox INTO baseline_prox FROM actor.org_unit_proximity WHERE from_org = copy_context_ou AND to_org = ahr.pickup_lib;
 
     -- Find any absolute adjustments, and set the baseline prox to that
     SELECT  adj.* INTO aoupa
       FROM  actor.org_unit_proximity_adjustment adj
-            LEFT JOIN actor.org_unit_ancestors_distance(context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
+            LEFT JOIN actor.org_unit_ancestors_distance(copy_context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
             LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
             LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acn_ol.id = adj.copy_location)
             LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
@@ -89,7 +98,7 @@ BEGIN
     FOR aoupa IN
         SELECT  adj.* 
           FROM  actor.org_unit_proximity_adjustment adj
-                LEFT JOIN actor.org_unit_ancestors_distance(context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
+                LEFT JOIN actor.org_unit_ancestors_distance(copy_context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
                 LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
                 LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acn_ol.id = adj.copy_location)
                 LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXYY.schema.custom-best-hold-selection.sql b/Open-ILS/src/sql/Pg/upgrade/XXYY.schema.custom-best-hold-selection.sql
new file mode 100644
index 0000000..55e3906
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXYY.schema.custom-best-hold-selection.sql
@@ -0,0 +1,148 @@
+BEGIN;
+
+CREATE TABLE config.best_hold_order(
+    id          SERIAL      PRIMARY KEY,    -- (metadata)
+    name        TEXT        UNIQUE,   -- i18n (metadata)
+    pprox       INT, -- copy capture <-> pickup lib prox
+    hprox       INT, -- copy circ lib <-> request lib prox
+    aprox       INT, -- copy circ lib <-> pickup lib ADJUSTED prox on ahcm
+    approx      INT, -- copy capture <-> pickup lib ADJUSTED prox from function
+    priority    INT, -- group hold priority
+    cut         INT, -- cut-in-line
+    depth       INT, -- selection depth
+    htime       INT, -- time since last home-lib circ exceeds org-unit setting
+    rtime       INT, -- request time
+    shtime      INT  -- time since copy last trip home exceeds org-unit setting
+);
+
+-- At least one of these columns must contain a non-null value
+ALTER TABLE config.best_hold_order ADD CHECK ((
+    pprox IS NOT NULL OR
+    hprox IS NOT NULL OR
+    aprox IS NOT NULL OR
+    priority IS NOT NULL OR
+    cut IS NOT NULL OR
+    depth IS NOT NULL OR
+    htime IS NOT NULL OR
+    rtime IS NOT NULL
+));
+
+INSERT INTO config.best_hold_order (
+    name,
+    pprox, aprox, priority, cut, depth, rtime, htime, hprox
+) VALUES (
+    'Traditional',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    hprox, pprox, aprox, priority, cut, depth, rtime, htime
+) VALUES (
+    'Traditional with Holds-always-go-home',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    htime, hprox, pprox, aprox, priority, cut, depth, rtime
+) VALUES (
+    'Traditional with Holds-go-home',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    priority, cut, rtime, depth, pprox, hprox, aprox, htime
+) VALUES (
+    'FIFO',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    hprox, priority, cut, rtime, depth, pprox, aprox, htime
+) VALUES (
+    'FIFO with Holds-always-go-home',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    htime, priority, cut, rtime, depth, pprox, aprox, hprox
+) VALUES (
+    'FIFO with Holds-go-home',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO permission.perm_list (
+    id, code, description
+) VALUES (
+    543,
+    'ADMIN_HOLD_CAPTURE_SORT',
+    oils_i18n_gettext(
+        543,
+        'Allows a user to make changes to best-hold selection sort order',
+        'ppl',
+        'description'
+    )
+);
+
+INSERT INTO config.org_unit_setting_type (
+    name, label, description, datatype, fm_class, update_perm, grp
+) VALUES (
+    'circ.hold_capture_order',
+    oils_i18n_gettext(
+        'circ.hold_capture_order',
+        'Best-hold selection sort order',
+        'coust',
+        'label'
+    ),
+    oils_i18n_gettext(
+        'circ.hold_capture_order',
+        'Defines the sort order of holds when selecting a hold to fill using a given copy at capture time',
+        'coust',
+        'description'
+    ),
+    'link',
+    'cbho',
+    543,
+    'holds'
+);
+
+INSERT INTO config.org_unit_setting_type (
+    name, label, description, datatype, update_perm, grp
+) VALUES (
+    'circ.hold_go_home_interval',
+    oils_i18n_gettext(
+        'circ.hold_go_home_interval',
+        'Max foreign-circulation time',
+        'coust',
+        'label'
+    ),
+    oils_i18n_gettext(
+        'circ.hold_go_home_interval',
+        'Time a copy can spend circulating away from its circ lib before returning there to fill a hold (if one exists there)',
+        'coust',
+        'description'
+    ),
+    'interval',
+    543,
+    'holds'
+);
+
+INSERT INTO actor.org_unit_setting (
+    org_unit, name, value
+) VALUES (
+    (SELECT id FROM actor.org_unit WHERE parent_ou IS NULL),
+    'circ.hold_go_home_interval',
+    '"6 months"'
+);
+
+UPDATE actor.org_unit_setting SET
+    name = 'circ.hold_capture_order',
+    value = (SELECT id FROM config.best_hold_order WHERE name = 'FIFO')
+WHERE
+    name = 'circ.holds_fifo' AND value ILIKE '%true%';
+
+COMMIT;
diff --git a/Open-ILS/src/templates/conify/global/config/best_hold_order.tt2 b/Open-ILS/src/templates/conify/global/config/best_hold_order.tt2
new file mode 100644
index 0000000..14d8bd7
--- /dev/null
+++ b/Open-ILS/src/templates/conify/global/config/best_hold_order.tt2
@@ -0,0 +1,119 @@
+[% WRAPPER base.tt2 %]
+<style type="text/css">
+    h1 { margin-bottom: 0.5ex; }
+    #cbho-loading { text-align: center; }
+    #cbho-edit-space { padding: 0.25ex 0.5em; }
+    #cbho-name { width: 30em; }
+    #cbho-field-order { width: 30em; }
+    #cbho-field-order-space > div { float: left; padding-right: 1em; }
+    #cbho-needs-saved { color: #c00; font-weight: bold; }
+    option.post-rtime, option.post-rtime:focus {
+        font-style: italic; color: #999;
+    }
+    .body-part { margin: 1ex 0; }
+    .clear-both { clear: both; }
+    .show-access-key { font-weight: bold; border-bottom: 1px dashed black; }
+</style>
+<h1>[% l('Best-Hold Selection Sort Order') %]</h1>
+
+<!-- Hidden after JS load. Prevents early clicks from breaking anything. -->
+<div id="cbho-loading">
+    <img src="[% ctx.media_prefix %]/opac/images/progressbar_green-old.gif"
+        alt="[% l('Interface loading') %]" />
+    <!-- <audio src="knight rider theme ;)" /> -->
+</div>
+
+<div id="cbho-main-body" class="hidden"><!-- main body -->
+    <div class="body-part">
+        <span dojoType="dijit.form.Button" onClick="module.new_cbho()">[% l('Create New') %]</span>
+        [% l('or') %]
+        <span dojoType="dijit.form.Button" onClick="module.edit_cbho()">[% l('Edit Existing') %]</span>
+    </div>
+
+    <div class="body-part hidden" id="cbho-edit-space"><!-- editing space -->
+        <p>
+            <span id="cbho-editing"></span>
+            <span id="cbho-needs-saved">[% l('You have unsaved changes.') %]</span>
+        </p>
+        <div id="cbho-name-edit-space">
+            <label for="cbho-name">[% l('Name:') %]</label>
+            <input id="cbho-name" type="text" onchange="module.editor_changed(true);" />
+        </div>
+        <div id="cbho-field-order-space" class="body-part">
+            <div>
+                <label for="cbho-field-order">[% l('Order:') %]</label>
+            </div>
+            <div>
+                <select id="cbho-field-order" size="10"> </select>
+            </div>
+            <div>
+                <input type="button"
+                    onclick="module.editor_move(-1); return false;"
+                    accesskey="[% l('k') %]"
+                    value="&uarr; [% l('Move Up') %]" />
+                <span class="show-access-key">[% l('k') %]</span>
+                <br />
+                <input type="button"
+                    onclick="module.editor_move(1); return false;"
+                    accesskey="[% l('j') %]"
+                    value="&darr; [% l('Move Down') %]" />
+                <span class="show-access-key">[% l('j') %]</span>
+            </div>
+        </div>
+
+        <div class="clear-both"></div>
+
+        <div class="body-part"><!-- save changes -->
+            <p><em>[% l('Because rtime, a high-precision timestamp, is ' _
+                'essentially unique among holds, ' _
+                'no fields arranged after rtime really have any effect in ' _
+                'determining best-hold selection.') %]</em></p>
+
+            <p>[% l('To choose which Best-Hold Selection Sort Order will be ' _
+                'used by Evergreen at copy capture time, see the Library ' _
+                'Settings interface.') %]</p>
+
+            <button id="cbho-save-changes"
+                onclick="module.editor_save(); return false" disabled="disabled">
+                [% l('Save Changes') %]
+            </button>
+        </div><!-- save changes -->
+    </div><!-- editing space -->
+
+</div><!-- main body -->
+
+<div class="hidden">
+    <div dojoType="openils.widget.ProgressDialog" id="progress-dialog"></div>
+    <div dojoType="dijit.Dialog" id="cbho-existing" title="[% l('Choose a best-hold order') %]">
+        <div class="body-part">
+            <label for="cbho-existing-selector">
+                [% l('Choose a best-hold order') %]
+            </label>
+            <span id="cbho-existing-selector"></span>
+        </div>
+        <div class="body-part">
+            <span dojoType="dijit.form.Button" type="submit"
+                id="cbho-existing-edit-go">
+                [% l('Edit') %]
+            </span>
+        </div>
+    </div>
+</div>
+
+<script type="text/javascript">
+    dojo.require("dijit.form.Button");
+    dojo.require("dijit.form.TextBox");
+    dojo.require("dijit.Dialog");
+    dojo.require("openils.widget.ProgressDialog");
+    dojo.require("openils.conify.BestHoldOrder");
+
+    var module;
+
+    openils.Util.addOnLoad(
+        function() {
+            module = openils.conify.BestHoldOrder;
+            module.init();
+        }
+    );
+</script>
+[% END %]
diff --git a/Open-ILS/web/js/dojo/openils/conify/BestHoldOrder.js b/Open-ILS/web/js/dojo/openils/conify/BestHoldOrder.js
new file mode 100644
index 0000000..52d2862
--- /dev/null
+++ b/Open-ILS/web/js/dojo/openils/conify/BestHoldOrder.js
@@ -0,0 +1,307 @@
+if (!dojo._hasResource["openils.conify.BestHoldOrder"]) {
+    dojo.requireLocalization("openils.conify", "conify");
+
+    dojo._hasResource["openils.conify.BestHoldOrder"] = true;
+    dojo.provide("openils.conify.BestHoldOrder");
+    dojo.provide("openils.conify.SetOrderer");
+
+    dojo.require("dojo.string");
+    dojo.require("openils.Util");
+    dojo.require("openils.User");
+    dojo.require("openils.PermaCrud");
+    dojo.require("openils.widget.AutoFieldWidget");
+
+(function() {
+    var localeStrings =
+        dojo.i18n.getLocalization("openils.conify", "conify");
+
+    /* This helper module is OO. */
+    dojo.declare(
+        "openils.conify.SetOrderer", null, {
+            "constructor": function(select, field_map, format_string) {
+                this.select = select;   /* HTML <select> node */
+                this.field_map = field_map; /* object of id:label pairs */
+                this.format_string = format_string || "[${0}] ${1}";
+            },
+
+            "clear": function() {
+                dojo.forEach(
+                    this.select.options,
+                    dojo.hitch(
+                        this, function(o) { this.select.options.remove(o); }
+                    )
+                );
+            },
+
+            /* This trusts that what you are passing is actually a set (no
+             * repeats). */
+            "set": function(
+                set, pos_callback /* called for each set member's <option>
+                                     node now and at any position change */
+            ) {
+                this.clear();
+                this.pos_callback = pos_callback;
+                dojo.forEach(
+                    set, dojo.hitch(this, function(o, p) { this.add(o, p); })
+                );
+            },
+
+            "focus": function() {
+                this.select.focus();
+            },
+
+            /* For now this trusts that your item is in the field_map */
+            "add": function(item, position) {
+                var option = dojo.create(
+                    "option", {
+                        "value": item,
+                        "innerHTML": dojo.string.substitute(
+                            this.format_string, [item, this.field_map[item]]
+                        )
+                    }
+                );
+
+                this.select.options.add(option, null);
+                if (this.pos_callback)
+                    this.pos_callback(option, position);
+            },
+
+            /* Returns option values in order, as a set, assuming you didn't
+             * add dupes. */
+            "get": function() {
+                /* XXX Could probably use dojo.forEach() here, but don't have
+                 * time to check whether it's sure to preserve order
+                 * with pseudo-arrays or NodeLists or whatever this is. */
+                var list = [];
+                for (var i = 0; i < this.select.options.length; i++)
+                    list.push(this.select.options[i].value);
+
+                return list;
+            },
+
+            "move_selected": function(offset) {
+                var si = this.select.selectedIndex;
+                if (si < 0)
+                    return false;
+
+                var opt = this.select.options[si];
+                var len = this.select.options.length;
+                var newpos = si + offset;
+
+                if (newpos >= 0 && newpos < len) {
+                    var newopt = dojo.clone(opt);
+                    this.select.remove(si);
+                    this.select.add(newopt, newpos);
+
+                    if (this.pos_callback)
+                        for (var i = 0; i < len; i++)
+                            this.pos_callback(this.select.options[i], i);
+
+                    this.select.selectedIndex = newpos;
+                    return true;
+                } else {
+                    return false;
+                }
+            },
+        }
+    );
+
+    /* This module is *not* OO. */
+    dojo.declare("openils.conify.BestHoldOrder", null, {});
+
+    var module = openils.conify.BestHoldOrder;
+
+    /* We could get these from the IDL, but if we add more fields to that
+     * later, we have no particular mechanism for determining what is or
+     * isn't metadata. */
+    module.fields = ["pprox", "hprox", "aprox", "priority", "cut", "depth",
+        "htime", "rtime", "approx", "shtime"];
+
+    module.init = function() {
+        module.progress_dialog = dijit.byId("progress-dialog");
+        module.existing_dialog = dijit.byId("cbho-existing");
+
+        dojo.connect(
+            dijit.byId("cbho-existing-edit-go"),
+            "onClick",
+            null,
+            module.editor_load_selected_cbho
+        );
+
+        module.field_labels = {};
+        dojo.forEach(
+            module.fields, function(f) {
+                module.field_labels[f] = fieldmapper.IDL.fmclasses.cbho.
+                    field_map[f].label
+            }
+        );
+
+        module.set_orderer = new openils.conify.SetOrderer(
+            dojo.byId("cbho-field-order"),
+            module.field_labels,
+            localeStrings.CBHO_FIELD_DISPLAY
+        );
+
+        openils.Util.hide("cbho-loading");
+        openils.Util.show("cbho-main-body");
+    };
+
+    module.new_cbho = function() {
+        module.cbho = new fieldmapper.cbho();
+
+        module.editor_start();
+    };
+
+    module.edit_cbho = function() {
+        module.progress_dialog.show(true);
+
+        function proceed(w) {
+            module.edit_cbho_selector = w;
+            module.progress_dialog.hide();
+            module.existing_dialog.show();
+        };
+
+        if (module.edit_cbho_selector) {
+            proceed(module.edit_cbho_selector);
+        } else {
+            new openils.widget.AutoFieldWidget({
+                "fmClass": "cbho",
+                "selfReference": true,
+                "dijitArgs": {"required": true},
+                "parentNode": dojo.create(
+                    "span", null, dojo.byId("cbho-existing-selector")
+                )
+            }).build(proceed);
+        }
+    };
+
+    /* Causes next use of Edit Existing button to recreate, thereby picking
+     * up any new objects */
+    module.clear_cbho_selector = function() {
+        if (module.edit_cbho_selector) {
+            module.edit_cbho_selector.destroy();
+            module.edit_cbho_selector = null;
+        }
+    };
+
+    module.editor_load_selected_cbho = function() {
+        var id = module.edit_cbho_selector.attr("value");
+
+        if (id) {
+            module.cbho = (new openils.PermaCrud()).retrieve("cbho", id);
+            module.editor_start();
+        } else {
+            alert(localeStrings.CBHO_NO_LOAD);
+        }
+    };
+
+    module.editor_start = function() {
+        dojo.byId("cbho-editing").innerHTML = module.cbho.id() ?
+            dojo.string.substitute(
+                localeStrings.CBHO_EDITING_EXISTING,
+                [module.cbho.id(), module.cbho.name()]
+            ) :
+            localeStrings.CBHO_EDITING_NEW;
+
+        dojo.byId("cbho-name").value = module.cbho.name() || "";
+        module.editor_reset_order();
+
+        openils.Util.show("cbho-edit-space");
+        module.editor_changed(false);
+    };
+
+    /* Used to set all <option> nodes in the set_orderer to appear disabled if
+     * they now come after rtime. */
+    module.set_pos_callback = function(opt_node, pos) {
+        var method = module.rtime_reached ? "addClass" : "removeClass";
+        dojo[method](opt_node, "post-rtime");
+
+        if (opt_node.value == "rtime")
+            module.rtime_reached = true;
+    };
+
+    module.stored_cbho_field_order = function() {
+        var obj = module.cbho;
+
+        return module.fields.sort(
+            function(a, b) {
+                a = obj[a]();
+                var left = (a === null || typeof a == "undefined") ?
+                    999 : Number(a);
+
+                b = obj[b]();
+                var right = (b === null || typeof b == "undefined") ?
+                    999 : Number(b);
+
+                return left - right;
+            }
+        );
+    };
+
+    module.editor_reset_order = function() {
+        module.rtime_reached = false;
+        module.set_orderer.set(
+            module.stored_cbho_field_order(), module.set_pos_callback
+        );
+    };
+
+    module.editor_move = function(offset) {
+        module.rtime_reached = false;
+        if (module.set_orderer.move_selected(offset))
+            module.editor_changed(true);
+
+        /* Without this, focus is now on the up or down button, breaking
+         * the user's ability to select other rows with the arrow keys. */
+        module.set_orderer.focus();
+    };
+
+    module.editor_changed = function(changed) {
+        dojo.attr("cbho-save-changes", "disabled", !changed);
+        if (changed)
+            openils.Util.show("cbho-needs-saved", "inline");
+        else
+            openils.Util.hide("cbho-needs-saved");
+    };
+
+    module.editor_save = function() {
+        var name = dojo.byId("cbho-name").value;
+        if (!name || !name.length) {
+            alert(localeStrings.CBHO_NEEDS_NAME);
+            return false;
+        } else {
+            module.cbho.name(name);
+        }
+
+        module.progress_dialog.show(true);
+        var fields = module.set_orderer.get();
+        for (var i = 0; i < fields.length; i++)
+            module.cbho[fields[i]](i);
+
+        try {
+            var pcrud = new openils.PermaCrud();
+            pcrud[module.cbho.id() ? "update" : "create"](
+                module.cbho, {
+                    "oncomplete": function(r, list) {
+                        module.progress_dialog.hide();
+                        openils.Util.readResponse(r); /* alert on exceptions? */
+
+                        if (dojo.isArray(list) && list.length) {
+                            if (typeof list[0] == "object")
+                                module.cbho = list[0];
+
+                            module.clear_cbho_selector();
+                            module.editor_start();
+                        }
+
+                        pcrud.session.disconnect(); /* good hygiene? */
+                    }
+                }
+            );
+        } catch (E) {
+            alert(E);   /* better than doing nothing? */
+        }
+    };
+
+})();
+
+}
diff --git a/Open-ILS/web/js/dojo/openils/conify/nls/conify.js b/Open-ILS/web/js/dojo/openils/conify/nls/conify.js
index c482bb6..608b998 100644
--- a/Open-ILS/web/js/dojo/openils/conify/nls/conify.js
+++ b/Open-ILS/web/js/dojo/openils/conify/nls/conify.js
@@ -88,6 +88,11 @@
     "SURVEY_FOOT_LABEL": "Questions & Answers",
     "EVENT_DEF_LABEL" : "${0}: ${1}",
     "ACQ_DISTRIB_FORMULA_NAME_PROMPT" : "Enter new formula name",
-    "ACQ_DISTRIB_FORMULA_NAME_CLONE" : "${0} (Clone)"
+    "ACQ_DISTRIB_FORMULA_NAME_CLONE" : "${0} (Clone)",
+    "CBHO_EDITING_NEW": "You are editing a new best-hold order.",
+    "CBHO_EDITING_EXISTING": "You are editing best-hold order #${0}: ${1}.",
+    "CBHO_FIELD_DISPLAY": "[${0}] ${1}",
+    "CBHO_NO_LOAD": "Unable to load selected item.",
+    "CBHO_NEEDS_NAME": "You need to enter a name for the best-hold order."
 }
 
diff --git a/Open-ILS/web/js/dojo/openils/widget/AutoFieldWidget.js b/Open-ILS/web/js/dojo/openils/widget/AutoFieldWidget.js
index f5e5a99..6738ffd 100644
--- a/Open-ILS/web/js/dojo/openils/widget/AutoFieldWidget.js
+++ b/Open-ILS/web/js/dojo/openils/widget/AutoFieldWidget.js
@@ -4,6 +4,7 @@ if(!dojo._hasResource['openils.widget.AutoFieldWidget']) {
     dojo.require('openils.User');
     dojo.require('fieldmapper.IDL');
     dojo.require('openils.PermaCrud');
+    dojo.require('dojo.data.ItemFileReadStore');
 	dojo.requireLocalization("openils.widget", "AutoFieldWidget");
 
     dojo.declare('openils.widget.AutoFieldWidget', null, {
diff --git a/Open-ILS/web/opac/locale/en-US/lang.dtd b/Open-ILS/web/opac/locale/en-US/lang.dtd
index 109c9a6..877c926 100644
--- a/Open-ILS/web/opac/locale/en-US/lang.dtd
+++ b/Open-ILS/web/opac/locale/en-US/lang.dtd
@@ -774,6 +774,7 @@
 <!ENTITY staff.main.menu.admin.server_admin.conify.usr_setting_type "User Setting Types">
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_hard_due_date "Hard Due Date Changes">
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_rule_circ_duration "Circulation Duration Rules">
+<!ENTITY staff.main.menu.admin.server_admin.conify.config_best_hold_order "Best-Hold Selection Sort Order">
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_rule_recurring_fine "Circulation Recurring Fine Rules">
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_rule_max_fine "Circulation Max Fine Rules">
 <!ENTITY staff.main.menu.admin.server_admin.conify.config_rule_age_hold_protect "Age Hold Protect Rules">
diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu.js b/Open-ILS/xul/staff_client/chrome/content/main/menu.js
index 1bb5db9..2fb9268 100644
--- a/Open-ILS/xul/staff_client/chrome/content/main/menu.js
+++ b/Open-ILS/xul/staff_client/chrome/content/main/menu.js
@@ -792,6 +792,10 @@ main.menu.prototype = {
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/config/circ_limit_group', null, event); }
             ],
+            'cmd_server_admin_config_best_hold_order' : [
+                ['oncommand'],
+                function(event) { open_eg_web_page('conify/global/config/best_hold_order', null, event); }
+            ],
             'cmd_server_admin_config_usr_activity_type' : [
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/config/usr_activity_type', null, event); }
diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
index 347fac7..dcd0003 100644
--- a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
+++ b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
@@ -322,6 +322,9 @@
     <command id="cmd_server_admin_actor_org_unit_custom_tree"
              perm="ADMIN_ORG_UNIT_CUSTOM_TREE VIEW_ORG_UNIT_CUSTOM_TREE"
              />
+    <command id="cmd_server_admin_config_best_hold_order"
+             perm="ADMIN_HOLD_CAPTURE_SORT"
+             />
 
     <command id="cmd_hotkeys_toggle" />
     <command id="cmd_hotkeys_set" />
@@ -632,6 +635,7 @@
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.config_actor_sip_fields;" command="cmd_server_admin_config_actor_sip_fields"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.config_asset_sip_fields;" command="cmd_server_admin_config_asset_sip_fields"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.config_usr_activity_type;" command="cmd_server_admin_config_usr_activity_type"/>
+                <menuitem label="&staff.main.menu.admin.server_admin.conify.config_best_hold_order;" command="cmd_server_admin_config_best_hold_order"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.actor.org_unit_custom_tree;" command="cmd_server_admin_actor_org_unit_custom_tree"/>
                 <menu id="main.menu.admin.server.acq" label="&staff.main.menu.admin.server_admin.acq.label;" accesskey="&staff.main.menu.admin.server_admin.acq.accesskey;">
                     <menupopup id="main.menu.admin.server.acq.popup">
diff --git a/docs/RELEASE_NOTES_NEXT/custom-best-hold-selection.txt b/docs/RELEASE_NOTES_NEXT/custom-best-hold-selection.txt
new file mode 100644
index 0000000..4fb440a
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/custom-best-hold-selection.txt
@@ -0,0 +1,16 @@
+Custom best-hold selection sort order
+=====================================
+
+The ranking algorithm that chooses the best hold to target a copy in 
+hand at a capture time used to be fairly simple.  It had two modes, FIFO
+and not-FIFO, and that was it.
+
+This change allows full configuration of that algorithm.  In other 
+words, when the system captures a copy and sets out to evaluate what
+hold, if any, that copy might best fulfull, site staff of sufficient
+permission level are now empowered to choose exactly which comparisons
+the systems makes in what order.  This gives said staff much greater
+flexibililty than they have today over holds policy.
+
+For more information, see the included tech spec documents.
+
diff --git a/docs/TechRef/Circ/custom-best-hold-selection.txt b/docs/TechRef/Circ/custom-best-hold-selection.txt
new file mode 100644
index 0000000..21e110e
--- /dev/null
+++ b/docs/TechRef/Circ/custom-best-hold-selection.txt
@@ -0,0 +1,247 @@
+Custom Best-Hold Selection
+==========================
+
+Background
+----------
+
+In the Evergreen ILS, during opportunistic capture (which occurs at copy
+checkin time), the copy being checked in is evaluated by the system for its
+fitness to fulfill outstanding holds.  When the copy might fulfill more than
+one hold, a set of 'determinants' are used to rank the possible holds that
+might be fulfilled, so that the best hold may be chosen.
+
+Evergreen currently uses one of two possible sets of 'determinants' to rank
+the holds that a given copy might fulfill.  An org-unit setting determines
+which set of 'determinants' is used.
+
+We will call these sets the "best-hold selection sort orders".  The best-hold
+selection sort orders available for use at hold capture time are:
+
+Traditional
+~~~~~~~~~~~
+  . 'pprox' - Proximity of capturing location to pickup library
+  . 'priority' - Group hold priority
+  . 'cut' - Hold cut-in-line
+  . 'depth' - Hold selection depth (deeper/narrower first)
+  . 'rtime' - Hold request time
+
+FIFO
+~~~~
+  . 'priority' - Group hold priority
+  . 'cut' - Hold cut-in-line
+  . 'rtime' - Hold request time
+  . 'depth' - Hold selection depth (deeper/narrower first)
+  . 'pprox' - Proximity of capturing location to pickup library
+
+In either of these scenarios, a case could be made for changing the order of
+several fields. However, the use of these is currently controlled only by a
+single org-unit setting to turn on or off FIFO (if FIFO is "off," the
+Traditional set is used).
+
+Adding more org-unit settings to control yet more hard-coded orderings is a
+path to madness, and therefore we should support custom field ordering for
+best-hold selection.
+
+Proposal
+--------
+
+To that end, we propose a new table to define field importance, and a new org-
+unit setting to replace "FIFO Holds" and select the appropriate definition for
+the capturing location. The UI for creating or editing hold order definitions
+should consist of a list for ordering the options, controlled by up-and-down
+buttons both clickable and accessible by keyboard.  There will also be a field
+for naming the definition and a save button.
+
+This org-unit setting will be retrieved at capture time, instead of the FIFO
+setting, and inspected by open-ils.storage.action.hold_request.nearest_hold.
+If no value is set, the equivalent of the "traditional" order will be used.
+
+An upgrade script will change all FIFO settings to version of the new setting
+which points to the system-supplied definition that implements FIFO as it
+stands today, thus avoiding functional changes and configuration problems.
+
+Design
+------
+
+Database Sketch
+~~~~~~~~~~~~~~~
+
+The 'config.best_hold_order' database table will have two metadata columns
+and eight data columns.
+
+Each of the eight data columns corresponds to a similarly named column used for
+ranking in the best-hold selection process (i.e., the 'determinants').  In a
+given row, the value of each of these columns corresponds to its relative
+priority in the ranking decision (lowest value representing the highest
+priority).
+
+Data columns with a null value have the effect of omitting the corresponding
+determinant in the ORDER BY clause for best-hold selection when the given
+best-hold selector order set is in play.
+
+One of the 'determinants', *aprox*, depends on the Calculated Proximity
+Adjustment enchancement (documented elsewhere).
+
+The 'determinant' *rtime*, which in practice is virtually unique among the
+set of all holds at a site, will always terminate the list of determinants
+used in constructing the ORDER BY clause whenever it appears.  In other words,
+because *rtime* will never tie anyway, no more comparisons after rtime have
+any meaning.
+
+The default best-hold order sets sketched here are subject to refinement and
+are not guaranteed to represent the final product.
+
+[source,sql]
+------------------------------------------------------------------------------
+
+CREATE TABLE config.best_hold_order(
+    id          SERIAL      PRIMARY KEY,    -- (metadata)
+    name        TEXT        UNIQUE,   -- i18n (metadata)
+    pprox       INT, -- copy capture <-> pickup lib prox
+    hprox       INT, -- copy circ lib <-> request lib prox
+    aprox       INT, -- copy circ lib <-> pickup lib ADJUSTED prox on ahcm
+    priority    INT, -- group hold priority
+    cut         INT, -- cut-in-line
+    depth       INT, -- selection depth
+    htime       INT, -- time since last home-lib circ exceeds org-unit setting
+    rtime       INT  -- request time
+);
+
+-- At least one of these columns must contain a non-null value
+ALTER TABLE config.best_hold_order ADD CHECK ((
+    pprox IS NOT NULL OR
+    hprox IS NOT NULL OR
+    aprox IS NOT NULL OR
+    priority IS NOT NULL OR
+    cut IS NOT NULL OR
+    depth IS NOT NULL OR
+    htime IS NOT NULL OR
+    rtime IS NOT NULL
+));
+
+INSERT INTO config.best_hold_order (
+    name,
+    pprox, aprox, priority, cut, depth, rtime, htime, hprox
+) VALUES (
+    'Traditional',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    hprox, pprox, aprox, priority, cut, depth, rtime, htime
+) VALUES (
+    'Traditional with Holds-always-go-to-home-patrons',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    htime, hprox, pprox, aprox, priority, cut, depth, rtime
+) VALUES (
+    'Traditional with Holds-go-home',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    priority, cut, rtime, depth, pprox, hprox, aprox, htime
+) VALUES (
+    'FIFO',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    hprox, priority, cut, rtime, depth, pprox, aprox, htime
+) VALUES (
+    'FIFO with Holds-always-go-to-home-patrons',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.best_hold_order (
+    name,
+    htime, priority, cut, rtime, depth, pprox, aprox, hprox
+) VALUES (
+    'FIFO with Holds-go-home',
+    1, 2, 3, 4, 5, 6, 7, 8
+);
+
+INSERT INTO config.org_unit_setting_type (
+    name, label, description, datatype, fm_class, update_perm
+) VALUES (
+    'circ.hold_capture_order',
+    'Best-hold selection precedence',
+    'Defines the sort order of holds when selecting a hold to fill using a given copy at capture time',
+    'link',
+    'cbho',
+    'ADMIN_HOLD_CAPTURE_SORT'
+);
+
+INSERT INTO config.org_unit_setting_type (
+    name, label, description, datatype, update_perm
+) VALUES (
+    'circ.hold_go_home_interval',
+    'Max foreign-circulation time',
+    'Time a copy can spend circulating away from its circ lib before returning there to fill a hold (if one exists there)',
+    'interval',
+    'ADMIN_HOLD_CAPTURE_SORT'
+);
+
+INSERT INTO actor.org_unit_setting (
+    org_unit, name, value
+) VALUES (
+    1,
+    'circ.hold_go_home_interval',
+    '6 months'
+);
+
+UPDATE actor.org_unit_setting SET
+    name = 'circ.hold_capture_order',
+    value = (SELECT id FROM config.hold_capture_sort WHERE name = 'FIFO')
+WHERE
+    name = 'circ.holds_fifo';
+------------------------------------------------------------------------------
+
+
+When constructing ORDER BY clauses, the *htime* determinant will be
+represented by a more complex expression than the other determinants.  The
+likely form of this will be as follows:
+
+[source,sql]
+-----------------------------------------------
+CASE WHEN
+    ['value of org setting circ.hold_go_home_interval'] <
+        NOW() - ['timestamp of last circulation at copy circ lib']
+    THEN hprox      -- sic
+    ELSE 999
+END
+
+-----------------------------------------------
+
+Middle Layer
+~~~~~~~~~~~~
+
+The 'open-ils.storage.action.hold_request.nearest_hold' method issues a query
+with an ORDER BY clause.
+
+This clause, previously selected from two hard-coded choices based on a
+boolean value indicating use- or don't-use-FIFO, will now be
+dynamically prepared based on the order specified in the
+'circ.hold_capture_order' org-unit setting.
+
+User Interface
+~~~~~~~~~~~~~~
+
+A user interface will allow the creation of new best-hold orders and the
+editing of existing ones, given sufficient user permission.
+
+The name field (metadata) will be editable with a free-form text widget, and
+the remaining (data) fields will be represented by objects that the user
+manipulates via clickable buttons (also keyboard accessible) to indicate order.
+
+////
+vim: ft=asciidoc
+////
+
+

commit fc048034c0a45795057dcea75ad2e876b6d5785a
Author: Lebbeous Fogle-Weekley <lebbeous at esilibrary.com>
Date:   Wed Dec 12 12:12:12 2012 -0500

    Calculated Proximity Adjustments, a new feature
    
    Allows customization to the way that Evergreen measures the distance
    between org units for the purposes of 1) determining what copy at what
    org unit is best suited for targeting a title-level hold, and 2)
    determining what hold is best suited for fulfillment by a copy-in-hand
    at capture (checkin) time.  The customization is based on a table
    'actor.org_unit_proximity_adjustment', with certain matching criteria
    that the system compares to properties of the holds and copies in
    question.
    
    This feature is actually side-ported from the FulfILLment project, where
    it was originally developed by Mike Rylander.  Lebbeous Fogle-Weekley
    was responsible for integration into current Evergreen code, some
    testing and bug-fixing, and minor refinement of documentation.
    
    Signed-off-by: Lebbeous Fogle-Weekley <lebbeous at esilibrary.com>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index ce64b5f..771ab0b 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -4103,6 +4103,7 @@ SELECT  usr,
 			<field name="hold" reporter:datatype="link"/>
 			<field name="id" reporter:datatype="id" />
 			<field name="target_copy" reporter:datatype="link"/>
+			<field name="proximity" reporter:datatype="number"/>
 		</fields>
 		<links>
 			<link field="hold" reltype="has_a" key="id" map="" class="ahr"/>
@@ -4870,6 +4871,36 @@ SELECT  usr,
             </actions>
         </permacrud>
 	</class>
+	<class id="aoupa" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::org_unit_proximity_adjustment" oils_persist:tablename="actor.org_unit_proximity_adjustment" reporter:label="Org Unit Proximity Adjustment">
+		<fields oils_persist:primary="id" oils_persist:sequence="actor.org_unit_proximity_adjustment_id_seq">
+			<field name="id" reporter:label="ID" reporter:datatype="id" />
+			<field name="item_circ_lib" reporter:label="Item Circ Lib" reporter:datatype="org_unit"/>
+			<field name="item_owning_lib" reporter:label="Item Owning Lib" reporter:datatype="org_unit"/>
+			<field name="hold_pickup_lib" reporter:label="Hold Pickup Lib" reporter:datatype="org_unit"/>
+			<field name="hold_request_lib" reporter:label="Hold Request Lib" reporter:datatype="org_unit"/>
+			<field name="copy_location" reporter:label="Copy Location" reporter:datatype="link"/>
+			<field name="circ_mod" reporter:label="Circ Modifier" reporter:datatype="link"/>
+			<field name="pos" reporter:label="Position" reporter:datatype="int" />
+			<field name="absolute_adjustment" reporter:label="Absolute adjustment?" reporter:datatype="bool" />
+			<field name="prox_adjustment" reporter:label="Proximity Adjustment" reporter:datatype="number" />
+		</fields>
+		<links>
+			<link field="item_circ_lib" reltype="has_a" key="id" map="" class="aou"/>
+			<link field="item_owning_lib" reltype="has_a" key="id" map="" class="aou"/>
+			<link field="hold_pickup_lib" reltype="has_a" key="id" map="" class="aou"/>
+			<link field="hold_request_lib" reltype="has_a" key="id" map="" class="aou"/>
+			<link field="circ_mod" reltype="has_a" key="code" map="" class="ccm"/>
+			<link field="copy_location" reltype="has_a" key="id" map="" class="acpl"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_PROXIMITY_ADJUSTMENT" global_required="true"/>
+                <retrieve permission="ADMIN_PROXIMITY_ADJUSTMENT" global_required="true"/>
+                <update permission="ADMIN_PROXIMITY_ADJUSTMENT" global_required="true"/>
+                <delete permission="ADMIN_PROXIMITY_ADJUSTMENT" global_required="true"/>
+            </actions>
+        </permacrud>
+	</class>
 	<class id="aoup" controller="open-ils.cstore" oils_obj:fieldmapper="actor::org_unit_proximity" oils_persist:tablename="actor.org_unit_proximity" reporter:label="Org Unit Proximity">
 		<fields oils_persist:primary="id" oils_persist:sequence="actor.org_unit_proximity_id_seq">
 			<field name="id" reporter:datatype="id" />
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/action.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/action.pm
index 4868664..c793710 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/action.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/CDBI/action.pm
@@ -114,7 +114,7 @@ package action::hold_copy_map;
 use base qw/action/;
 __PACKAGE__->table('action_hold_copy_map');
 __PACKAGE__->columns(Primary => 'id');
-__PACKAGE__->columns(Essential => qw/hold target_copy/);
+__PACKAGE__->columns(Essential => qw/hold target_copy proximity/);
 
 #-------------------------------------------------------------------------------
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
index 4101309..60fe6b7 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/action.pm
@@ -294,8 +294,8 @@ sub nearest_hold {
 	local $OpenILS::Application::Storage::WRITE = 1;
 
 	my $holdsort = isTrue($fifo) ?
-			"pgt.hold_priority, CASE WHEN h.cut_in_line IS TRUE THEN 0 ELSE 1 END, h.request_time, h.selection_depth DESC, p.prox " :
-			"p.prox, pgt.hold_priority, CASE WHEN h.cut_in_line IS TRUE THEN 0 ELSE 1 END, h.selection_depth DESC, h.request_time ";
+			"pgt.hold_priority, CASE WHEN h.cut_in_line IS TRUE THEN 0 ELSE 1 END, h.request_time, h.selection_depth DESC, COALESCE(hm.proximity, h.prox) " :
+			"COALESCE(hm.proximity, h.prox), pgt.hold_priority, CASE WHEN h.cut_in_line IS TRUE THEN 0 ELSE 1 END, h.selection_depth DESC, h.request_time ";
 
 	my $ids = action::hold_request->db_Main->selectcol_arrayref(<<"	SQL", {}, $here, $cp, $age);
 		SELECT	h.id
@@ -1293,8 +1293,12 @@ sub new_hold_copy_targeter {
 			# map the potentials, so that we can pick up checkins
 			# XXX Loop-based targeting may require that /only/ copies from this loop should be added to
 			# XXX the potentials list.  If this is the cased, hold_copy_map creation will move down further.
+			my $pu_lib = ''.$hold->pickup_lib;
+			my $prox_list = create_prox_list( $self, $pu_lib, $all_copies, $hold );
 			$log->debug( "\tMapping ".scalar(@$all_copies)." potential copies for hold ".$hold->id);
-			action::hold_copy_map->create( { hold => $hold->id, target_copy => $_->id } ) for (@$all_copies);
+			for my $prox ( keys %$prox_list ) {
+				action::hold_copy_map->create( { proximity => $prox, hold => $hold->id, target_copy => $_->id } ) for (@{$$prox_list{$prox}});
+			}
 
 			#$client->status( new OpenSRF::DomainObject::oilsContinueStatus );
 
@@ -1374,26 +1378,23 @@ sub new_hold_copy_targeter {
 				}
 			}
 
-            my $pu_lib = ''.$hold->pickup_lib;
+			# reset prox list after trimming good copies
+			$prox_list = create_prox_list( $self, $pu_lib, \@good_copies, $hold );
 
-			my $prox_list = [];
-			$$prox_list[0] =
-			[
-				grep {
-					''.$_->circ_lib eq $pu_lib &&
-                    ( $_->status == 0 || $_->status == 7 )
-				} @good_copies
-			];
 
-			$all_copies = [grep { $_->status == 0 || $_->status == 7 } grep {''.$_->circ_lib ne $pu_lib } @good_copies];
-			# $all_copies is now a list of copies not at the pickup library
-			
-            my $best;
-            if  ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') { # Recall/Force holds bypass hold rules.
-                $best = $good_copies[0] if(scalar @good_copies);
-            } else {
-                $best = choose_nearest_copy($hold, $prox_list);
-            }
+			my $min_prox = [ sort keys %$prox_list ]->[0];
+			my $best;
+			if  ($hold->hold_type eq 'R' || $hold->hold_type eq 'F') { # Recall/Force holds bypass hold rules.
+				$best = $good_copies[0] if(scalar @good_copies);
+			} else {
+				$best = choose_nearest_copy($hold, { $min_prox => delete($$prox_list{$min_prox}) });
+			}
+
+			$all_copies = [];
+			for my $prox (keys %$prox_list) {
+				push @$all_copies, @{$$prox_list{$prox}};
+			}
+	
 			$client->status( new OpenSRF::DomainObject::oilsContinueStatus );
 
 			if (!$best) {
@@ -1481,11 +1482,12 @@ sub new_hold_copy_targeter {
 
 						die "OK\n";
 					}
-				}
 
-				$prox_list = create_prox_list( $self, $pu_lib, $all_copies );
+    				$prox_list = create_prox_list( $self, $pu_lib, $all_copies, $hold );
 
-				$client->status( new OpenSRF::DomainObject::oilsContinueStatus );
+    				$client->status( new OpenSRF::DomainObject::oilsContinueStatus );
+
+				}
 
 				$best = choose_nearest_copy($hold, $prox_list);
 			}
@@ -1806,6 +1808,10 @@ sub reservation_targeter {
 
 			$log->debug("\t".scalar(@good_resources)." resources available for targeting...");
 
+			# LFW: note that after the inclusion of hold proximity
+			# adjustment, this prox_list is the only prox_list
+			# array in this perl package.  Other occurences are
+			# hashes.
 			my $prox_list = [];
 			$$prox_list[0] =
 			[
@@ -1938,10 +1944,10 @@ sub choose_nearest_copy {
 	my $hold = shift;
 	my $prox_list = shift;
 
-	for my $p ( 0 .. int( scalar(@$prox_list) - 1) ) {
-		next unless (ref $$prox_list[$p]);
+	for my $p ( sort keys %$prox_list ) {
+		next unless (ref $$prox_list{$p});
 
-		my @capturable = @{ $$prox_list[$p] };
+		my @capturable = @{ $$prox_list{$p} };
 		next unless (@capturable);
 
 		my $rand = int(rand(scalar(@capturable)));
@@ -1970,12 +1976,13 @@ sub create_prox_list {
 	my $self = shift;
 	my $lib = shift;
 	my $copies = shift;
+	my $hold = shift;
 
 	my $actor = OpenSRF::AppSession->create('open-ils.actor');
 
-	my @prox_list;
+	my %prox_list;
 	for my $cp (@$copies) {
-		my ($prox) = $self->method_lookup('open-ils.storage.asset.copy.proximity')->run( $cp, $lib );
+		my ($prox) = $self->method_lookup('open-ils.storage.asset.copy.proximity')->run( $cp, $lib, $hold );
 		next unless (defined($prox));
 
         my $copy_circ_lib = ''.$cp->circ_lib;
@@ -1986,12 +1993,12 @@ sub create_prox_list {
         $self->{target_weight}{$copy_circ_lib} = $self->{target_weight}{$copy_circ_lib}{value} if (ref $self->{target_weight}{$copy_circ_lib});
         $self->{target_weight}{$copy_circ_lib} ||= 1;
 
-		$prox_list[$prox] = [] unless defined($prox_list[$prox]);
+		$prox_list{$prox} = [] unless defined($prox_list{$prox});
 		for my $w ( 1 .. $self->{target_weight}{$copy_circ_lib} ) {
-			push @{$prox_list[$prox]}, $cp;
+			push @{$prox_list{$prox}}, $cp;
 		}
 	}
-	return \@prox_list;
+	return \%prox_list;
 }
 
 sub volume_hold_capture {
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/asset.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/asset.pm
index 42fb891..90ca204 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/asset.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/asset.pm
@@ -396,10 +396,40 @@ sub copy_proximity {
 	my $client = shift;
 
 	my $cp = shift;
-	my $org = shift;
+	my $org = shift;	# hold pickup lib
+	my $hold = shift;
 
 	return unless ($cp && $org);
 
+	if ($hold) {
+		my $row = action::hold_request->db_Main->selectrow_hashref(
+			'SELECT proximity AS prox FROM action.hold_copy_map WHERE hold = ? and target_copy = ?',
+			{},
+			"$hold",
+			"$cp"
+		);
+		return $row->{prox} if $row;
+
+		# There was a bug here before.
+		# action.hold_copy_calculated_proximity()  was called with a
+		# third argument, $org.  Wrong.  a.hccp() interprets its third
+		# argument as an optional override of copy circ lib.  $org
+		# here is hold pickup lib.  This had the effect of basically
+		# measuring the distance between a hold's pickup lib and
+		# itself, which is always zero, so all proximities landing in
+		# the hold copy map were zero.
+
+		$log->debug("Calculating copy proximity with: action.hold_copy_calculated_proximity($hold,$cp)", DEBUG);
+		$row = action::hold_request->db_Main->selectrow_hashref(
+			'SELECT action.hold_copy_calculated_proximity(?,?) AS prox',
+			{},
+			"$hold",
+			"$cp"
+		);
+
+		return $row->{prox} if $row;
+	}
+
 	$cp = asset::copy->retrieve($cp) unless (ref($cp));
 
 	return unless $cp;
diff --git a/Open-ILS/src/sql/Pg/005.schema.actors.sql b/Open-ILS/src/sql/Pg/005.schema.actors.sql
index 8695852..176f465 100644
--- a/Open-ILS/src/sql/Pg/005.schema.actors.sql
+++ b/Open-ILS/src/sql/Pg/005.schema.actors.sql
@@ -380,6 +380,27 @@ default entry.
 $$;
 
 
+CREATE TABLE actor.org_unit_proximity_adjustment (
+    id                  SERIAL   PRIMARY KEY,
+    item_circ_lib       INT         REFERENCES actor.org_unit (id),
+    item_owning_lib     INT         REFERENCES actor.org_unit (id),
+    copy_location       INT         REFERENCES asset.copy_location (id),
+    hold_pickup_lib     INT         REFERENCES actor.org_unit (id),
+    hold_request_lib    INT         REFERENCES actor.org_unit (id),
+    pos                 INT         NOT NULL DEFAULT 0,
+    absolute_adjustment BOOL        NOT NULL DEFAULT FALSE,
+    prox_adjustment     NUMERIC,
+    circ_mod            TEXT,       -- REFERENCES config.circ_modifier (code),
+    CONSTRAINT prox_adj_criterium CHECK (COALESCE(item_circ_lib::TEXT,item_owning_lib::TEXT,copy_location::TEXT,hold_pickup_lib::TEXT,hold_request_lib::TEXT,circ_mod) IS NOT NULL)
+);
+CREATE UNIQUE INDEX prox_adj_once_idx ON actor.org_unit_proximity_adjustment (item_circ_lib,item_owning_lib,copy_location,hold_pickup_lib,hold_request_lib,circ_mod);
+CREATE INDEX prox_adj_circ_lib_idx ON actor.org_unit_proximity_adjustment (item_circ_lib);
+CREATE INDEX prox_adj_owning_lib_idx ON actor.org_unit_proximity_adjustment (item_owning_lib);
+CREATE INDEX prox_adj_copy_location_idx ON actor.org_unit_proximity_adjustment (copy_location);
+CREATE INDEX prox_adj_pickup_lib_idx ON actor.org_unit_proximity_adjustment (hold_pickup_lib);
+CREATE INDEX prox_adj_request_lib_idx ON actor.org_unit_proximity_adjustment (hold_request_lib);
+CREATE INDEX prox_adj_circ_mod_idx ON actor.org_unit_proximity_adjustment (circ_mod);
+
 CREATE TABLE actor.hours_of_operation (
 	id		INT	PRIMARY KEY REFERENCES actor.org_unit (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
 	dow_0_open	TIME	NOT NULL DEFAULT '09:00',
diff --git a/Open-ILS/src/sql/Pg/020.schema.functions.sql b/Open-ILS/src/sql/Pg/020.schema.functions.sql
index f69bfa4..739b317 100644
--- a/Open-ILS/src/sql/Pg/020.schema.functions.sql
+++ b/Open-ILS/src/sql/Pg/020.schema.functions.sql
@@ -193,6 +193,17 @@ CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABL
     SELECT * FROM org_unit_ancestors_distance;
 $$ LANGUAGE SQL STABLE ROWS 1;
 
+CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE org_unit_ancestors_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT ou.parent_ou, ouad.distance+1
+            FROM actor.org_unit ou JOIN org_unit_ancestors_distance ouad ON (ou.id = ouad.id)
+            WHERE ou.parent_ou IS NOT NULL
+    )
+    SELECT * FROM org_unit_ancestors_distance;
+$$ LANGUAGE SQL STABLE ROWS 1;
+
 CREATE OR REPLACE FUNCTION actor.org_unit_full_path ( INT ) RETURNS SETOF actor.org_unit AS $$
 	SELECT	*
 	  FROM	actor.org_unit_ancestors($1)
diff --git a/Open-ILS/src/sql/Pg/090.schema.action.sql b/Open-ILS/src/sql/Pg/090.schema.action.sql
index 833d7bc..3f7bb6f 100644
--- a/Open-ILS/src/sql/Pg/090.schema.action.sql
+++ b/Open-ILS/src/sql/Pg/090.schema.action.sql
@@ -452,6 +452,7 @@ CREATE TABLE action.hold_copy_map (
 	id		BIGSERIAL	PRIMARY KEY,
 	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,
 	CONSTRAINT copy_once_per_hold UNIQUE (hold,target_copy)
 );
 -- CREATE INDEX acm_hold_idx ON action.hold_copy_map (hold);
@@ -972,5 +973,77 @@ query-based fieldsets.
 Returns NULL if successful, or an error message if not.
 $$;
 
+CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity(ahr_id INT, acp_id BIGINT, context_ou INT DEFAULT NULL) RETURNS NUMERIC AS $f$
+DECLARE
+    aoupa           actor.org_unit_proximity_adjustment%ROWTYPE;
+    ahr             action.hold_request%ROWTYPE;
+    acp             asset.copy%ROWTYPE;
+    acn             asset.call_number%ROWTYPE;
+    acl             asset.copy_location%ROWTYPE;
+    baseline_prox   NUMERIC;
+
+    icl_list        INT[];
+    iol_list        INT[];
+    isl_list        INT[];
+    hpl_list        INT[];
+    hrl_list        INT[];
+
+BEGIN
+
+    SELECT * INTO ahr FROM action.hold_request WHERE id = ahr_id;
+    SELECT * INTO acp FROM asset.copy WHERE id = acp_id;
+    SELECT * INTO acn FROM asset.call_number WHERE id = acp.call_number;
+    SELECT * INTO acl FROM asset.copy_location WHERE id = acp.location;
+
+    IF context_ou IS NULL THEN
+        context_ou := acp.circ_lib;
+    END IF;
+
+    -- First, gather the baseline proximity of "here" to pickup lib
+    SELECT prox INTO baseline_prox FROM actor.org_unit_proximity WHERE from_org = context_ou AND to_org = ahr.pickup_lib;
+
+    -- Find any absolute adjustments, and set the baseline prox to that
+    SELECT  adj.* INTO aoupa
+      FROM  actor.org_unit_proximity_adjustment adj
+            LEFT JOIN actor.org_unit_ancestors_distance(context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
+            LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
+            LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acn_ol.id = adj.copy_location)
+            LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
+            LEFT JOIN actor.org_unit_ancestors_distance(ahr.request_lib) ahr_rl ON (ahr_rl.id = adj.hold_request_lib)
+      WHERE (adj.circ_mod IS NULL OR adj.circ_mod = acp.circ_modifier) AND
+        absolute_adjustment AND
+        COALESCE(acp_cl.id, acn_ol.id, acl_ol.id, ahr_pl.id, ahr_rl.id) IS NOT NULL
+      ORDER BY
+            COALESCE(acp_cl.distance,999)
+                + COALESCE(acn_ol.distance,999)
+                + COALESCE(acl_ol.distance,999)
+                + COALESCE(ahr_pl.distance,999)
+                + COALESCE(ahr_rl.distance,999),
+            adj.pos
+      LIMIT 1;
+
+    IF FOUND THEN
+        baseline_prox := aoupa.prox_adjustment;
+    END IF;
+
+    -- Now find any relative adjustments, and change the baseline prox based on them
+    FOR aoupa IN
+        SELECT  adj.* 
+          FROM  actor.org_unit_proximity_adjustment adj
+                LEFT JOIN actor.org_unit_ancestors_distance(context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
+                LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
+                LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acn_ol.id = adj.copy_location)
+                LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
+                LEFT JOIN actor.org_unit_ancestors_distance(ahr.request_lib) ahr_rl ON (ahr_rl.id = adj.hold_request_lib)
+          WHERE (adj.circ_mod IS NULL OR adj.circ_mod = acp.circ_modifier) AND
+            NOT absolute_adjustment AND
+            COALESCE(acp_cl.id, acn_ol.id, acl_ol.id, ahr_pl.id, ahr_rl.id) IS NOT NULL
+    LOOP
+        baseline_prox := baseline_prox + aoupa.prox_adjustment;
+    END LOOP;
+
+    RETURN baseline_prox;
+END;
+$f$ LANGUAGE PLPGSQL;
 
 COMMIT;
diff --git a/Open-ILS/src/sql/Pg/800.fkeys.sql b/Open-ILS/src/sql/Pg/800.fkeys.sql
index 414e5e4..36e9438 100644
--- a/Open-ILS/src/sql/Pg/800.fkeys.sql
+++ b/Open-ILS/src/sql/Pg/800.fkeys.sql
@@ -39,6 +39,8 @@ ALTER TABLE actor.org_unit ADD CONSTRAINT actor_org_unit_billing_address_fkey FO
 ALTER TABLE actor.org_unit ADD CONSTRAINT actor_org_unit_holds_address_fkey FOREIGN KEY (holds_address) REFERENCES actor.org_address (id) DEFERRABLE INITIALLY DEFERRED;
 ALTER TABLE actor.org_unit ADD CONSTRAINT actor_org_unit_ill_address_fkey FOREIGN KEY (ill_address) REFERENCES actor.org_address (id) DEFERRABLE INITIALLY DEFERRED;
 
+ALTER TABLE actor.org_unit_proximity_adjustment ADD CONSTRAINT actor_org_unit_proximity_adjustment_circ_mod_fkey FOREIGN KEY (circ_mod) REFERENCES config.circ_modifier (code) DEFERRABLE INITIALLY DEFERRED;
+
 ALTER TABLE acq.provider ADD CONSTRAINT acq_provider_edi_default_fkey FOREIGN KEY (edi_default) REFERENCES acq.edi_account (id) DEFERRABLE INITIALLY DEFERRED;
 
 ALTER TABLE biblio.record_note ADD CONSTRAINT biblio_record_note_record_fkey FOREIGN KEY (record) REFERENCES biblio.record_entry (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED;
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.org_prox_adjust.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.org_prox_adjust.sql
new file mode 100644
index 0000000..d9eb082
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.org_prox_adjust.sql
@@ -0,0 +1,115 @@
+BEGIN;
+
+CREATE TABLE actor.org_unit_proximity_adjustment (
+    id                  SERIAL   PRIMARY KEY,
+    item_circ_lib       INT         REFERENCES actor.org_unit (id),
+    item_owning_lib     INT         REFERENCES actor.org_unit (id),
+    copy_location       INT         REFERENCES asset.copy_location (id),
+    hold_pickup_lib     INT         REFERENCES actor.org_unit (id),
+    hold_request_lib    INT         REFERENCES actor.org_unit (id),
+    pos                 INT         NOT NULL DEFAULT 0,
+    absolute_adjustment BOOL        NOT NULL DEFAULT FALSE,
+    prox_adjustment     NUMERIC,
+    circ_mod            TEXT,       -- REFERENCES config.circ_modifier (code),
+    CONSTRAINT prox_adj_criterium CHECK (COALESCE(item_circ_lib::TEXT,item_owning_lib::TEXT,copy_location::TEXT,hold_pickup_lib::TEXT,hold_request_lib::TEXT,circ_mod) IS NOT NULL)
+);
+CREATE UNIQUE INDEX prox_adj_once_idx ON actor.org_unit_proximity_adjustment (item_circ_lib,item_owning_lib,copy_location,hold_pickup_lib,hold_request_lib,circ_mod);
+CREATE INDEX prox_adj_circ_lib_idx ON actor.org_unit_proximity_adjustment (item_circ_lib);
+CREATE INDEX prox_adj_owning_lib_idx ON actor.org_unit_proximity_adjustment (item_owning_lib);
+CREATE INDEX prox_adj_copy_location_idx ON actor.org_unit_proximity_adjustment (copy_location);
+CREATE INDEX prox_adj_pickup_lib_idx ON actor.org_unit_proximity_adjustment (hold_pickup_lib);
+CREATE INDEX prox_adj_request_lib_idx ON actor.org_unit_proximity_adjustment (hold_request_lib);
+CREATE INDEX prox_adj_circ_mod_idx ON actor.org_unit_proximity_adjustment (circ_mod);
+
+CREATE OR REPLACE FUNCTION actor.org_unit_ancestors_distance( INT ) RETURNS TABLE (id INT, distance INT) AS $$
+    WITH RECURSIVE org_unit_ancestors_distance(id, distance) AS (
+            SELECT $1, 0
+        UNION
+            SELECT ou.parent_ou, ouad.distance+1
+            FROM actor.org_unit ou JOIN org_unit_ancestors_distance ouad ON (ou.id = ouad.id)
+            WHERE ou.parent_ou IS NOT NULL
+    )
+    SELECT * FROM org_unit_ancestors_distance;
+$$ LANGUAGE SQL STABLE ROWS 1;
+
+CREATE OR REPLACE FUNCTION action.hold_copy_calculated_proximity(ahr_id INT, acp_id BIGINT, context_ou INT DEFAULT NULL) RETURNS NUMERIC AS $f$
+DECLARE
+    aoupa           actor.org_unit_proximity_adjustment%ROWTYPE;
+    ahr             action.hold_request%ROWTYPE;
+    acp             asset.copy%ROWTYPE;
+    acn             asset.call_number%ROWTYPE;
+    acl             asset.copy_location%ROWTYPE;
+    baseline_prox   NUMERIC;
+
+    icl_list        INT[];
+    iol_list        INT[];
+    isl_list        INT[];
+    hpl_list        INT[];
+    hrl_list        INT[];
+
+BEGIN
+
+    SELECT * INTO ahr FROM action.hold_request WHERE id = ahr_id;
+    SELECT * INTO acp FROM asset.copy WHERE id = acp_id;
+    SELECT * INTO acn FROM asset.call_number WHERE id = acp.call_number;
+    SELECT * INTO acl FROM asset.copy_location WHERE id = acp.location;
+
+    IF context_ou IS NULL THEN
+        context_ou := acp.circ_lib;
+    END IF;
+
+    -- First, gather the baseline proximity of "here" to pickup lib
+    SELECT prox INTO baseline_prox FROM actor.org_unit_proximity WHERE from_org = context_ou AND to_org = ahr.pickup_lib;
+
+    -- Find any absolute adjustments, and set the baseline prox to that
+    SELECT  adj.* INTO aoupa
+      FROM  actor.org_unit_proximity_adjustment adj
+            LEFT JOIN actor.org_unit_ancestors_distance(context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
+            LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
+            LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acn_ol.id = adj.copy_location)
+            LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
+            LEFT JOIN actor.org_unit_ancestors_distance(ahr.request_lib) ahr_rl ON (ahr_rl.id = adj.hold_request_lib)
+      WHERE (adj.circ_mod IS NULL OR adj.circ_mod = acp.circ_modifier) AND
+        absolute_adjustment AND
+        COALESCE(acp_cl.id, acn_ol.id, acl_ol.id, ahr_pl.id, ahr_rl.id) IS NOT NULL
+      ORDER BY
+            COALESCE(acp_cl.distance,999)
+                + COALESCE(acn_ol.distance,999)
+                + COALESCE(acl_ol.distance,999)
+                + COALESCE(ahr_pl.distance,999)
+                + COALESCE(ahr_rl.distance,999),
+            adj.pos
+      LIMIT 1;
+
+    IF FOUND THEN
+        baseline_prox := aoupa.prox_adjustment;
+    END IF;
+
+    -- Now find any relative adjustments, and change the baseline prox based on them
+    FOR aoupa IN
+        SELECT  adj.* 
+          FROM  actor.org_unit_proximity_adjustment adj
+                LEFT JOIN actor.org_unit_ancestors_distance(context_ou) acp_cl ON (acp_cl.id = adj.item_circ_lib)
+                LEFT JOIN actor.org_unit_ancestors_distance(acn.owning_lib) acn_ol ON (acn_ol.id = adj.item_owning_lib)
+                LEFT JOIN actor.org_unit_ancestors_distance(acl.owning_lib) acl_ol ON (acn_ol.id = adj.copy_location)
+                LEFT JOIN actor.org_unit_ancestors_distance(ahr.pickup_lib) ahr_pl ON (ahr_pl.id = adj.hold_pickup_lib)
+                LEFT JOIN actor.org_unit_ancestors_distance(ahr.request_lib) ahr_rl ON (ahr_rl.id = adj.hold_request_lib)
+          WHERE (adj.circ_mod IS NULL OR adj.circ_mod = acp.circ_modifier) AND
+            NOT absolute_adjustment AND
+            COALESCE(acp_cl.id, acn_ol.id, acl_ol.id, ahr_pl.id, ahr_rl.id) IS NOT NULL
+    LOOP
+        baseline_prox := baseline_prox + aoupa.prox_adjustment;
+    END LOOP;
+
+    RETURN baseline_prox;
+END;
+$f$ LANGUAGE PLPGSQL;
+
+ALTER TABLE actor.org_unit_proximity_adjustment
+    ADD CONSTRAINT actor_org_unit_proximity_adjustment_circ_mod_fkey
+    FOREIGN KEY (circ_mod) REFERENCES config.circ_modifier (code)
+    DEFERRABLE INITIALLY DEFERRED;
+
+ALTER TABLE action.hold_copy_map ADD COLUMN proximity NUMERIC;
+
+COMMIT;
diff --git a/Open-ILS/src/templates/conify/global/config/org_unit_proximity_adjustment.tt2 b/Open-ILS/src/templates/conify/global/config/org_unit_proximity_adjustment.tt2
new file mode 100644
index 0000000..1c1a2ab
--- /dev/null
+++ b/Open-ILS/src/templates/conify/global/config/org_unit_proximity_adjustment.tt2
@@ -0,0 +1,85 @@
+[% WRAPPER base.tt2 %]
+[% ctx.page_title = 'Org Unit Proximity Adjustments' %]
+<div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+    <div dojoType="dijit.layout.ContentPane" layoutAlign="top" class="oils-header-panel">
+        <div>[% ctx.page_title %]</div>
+        <div>
+            <button dojoType="dijit.form.Button"
+                onClick="aoupa_grid.showCreateDialog()">New OU Proximity Adjustment</button>
+            <button dojoType="dijit.form.Button"
+                onClick="aoupa_grid.deleteSelected()">Delete Selected</button>
+        </div>
+    </div>
+    <div>
+        Show adjustments involving this branch or deeper:
+        <select dojoType="openils.widget.OrgUnitFilteringSelect"
+            jsId="context_org_selector"></select>
+    </div>
+    <table jsId="aoupa_grid"
+        dojoType="openils.widget.AutoGrid"
+        query="{id: '*'}"
+        fmClass="aoupa"
+        fieldorder="['item_circ_lib','item_owning_lib','hold_pickup_lib','hold_request_lib','copy_location','circ_mod','pos','absolute_adjustment','prox_adjustment']"
+        showPaginator="true"
+        editOnEnter="true">
+        <thead>
+            <tr>
+                <th field="item_circ_lib"
+                    get="openils.widget.AutoGrid.orgUnitGetter"></th>
+                <th field="item_owning_lib"
+                    get="openils.widget.AutoGrid.orgUnitGetter"></th>
+                <th field="hold_pickup_lib"
+                    get="openils.widget.AutoGrid.orgUnitGetter"></th>
+                <th field="hold_request_lib"
+                    get="openils.widget.AutoGrid.orgUnitGetter"></th>
+            </tr>
+        </thead>
+    </table>
+</div>
+
+<script type="text/javascript">
+    dojo.require("openils.widget.AutoGrid");
+    dojo.require("openils.widget.OrgUnitFilteringSelect");
+
+    var context_org;
+
+    function load_grid(search) {
+        if (!search) search = {"id": {"!=": null}};
+
+        aoupa_grid.loadAll({
+            "order_by": {
+                "aoupa": ["item_circ_lib","item_owning_lib","hold_pickup_lib","hold_request_lib","pos"]
+            }
+        }, search);
+    }
+
+    function reload_grid_from_ou_selector() {
+        context_org = context_org_selector.attr("value");
+        var descendants = aou.descendantNodeList(context_org, true);
+        aoupa_grid.resetStore();
+        load_grid({
+            "-or": [
+                {"item_circ_lib": descendants},
+                {"item_owning_lib": descendants},
+                {"hold_pickup_lib": descendants},
+                {"hold_request_lib": descendants}
+            ]
+        });
+    }
+
+    openils.Util.addOnLoad(
+        function() {
+            new openils.User().buildPermOrgSelector(
+                "ADMIN_PROXIMITY_ADJUSTMENT",
+                context_org_selector,
+                null,
+                function() {
+                    context_org_selector.onChange =
+                        reload_grid_from_ou_selector;
+                    reload_grid_from_ou_selector();
+                }
+            );
+        }
+    );
+</script>
+[% END %]
diff --git a/Open-ILS/web/opac/locale/en-US/lang.dtd b/Open-ILS/web/opac/locale/en-US/lang.dtd
index e0cb546..109c9a6 100644
--- a/Open-ILS/web/opac/locale/en-US/lang.dtd
+++ b/Open-ILS/web/opac/locale/en-US/lang.dtd
@@ -767,6 +767,7 @@
 <!ENTITY staff.main.menu.admin.server_admin.conify.billing_type.label "Billing Types">
 <!ENTITY staff.main.menu.admin.server_admin.conify.sms_carrier.label "SMS Carriers">
 <!ENTITY staff.main.menu.admin.server_admin.conify.z3950_source.label "Z39.50 Servers">
+<!ENTITY staff.main.menu.admin.server_admin.conify.org_unit_proximity_adjustment.label "Org Unit Proximity Adjustments">
 <!ENTITY staff.main.menu.admin.server_admin.conify.circulation_modifier.label "Circulation Modifiers">
 <!ENTITY staff.main.menu.admin.server_admin.conify.org_unit_setting_type "Organization Unit Setting Types">
 <!ENTITY staff.main.menu.admin.server_admin.conify.import_match_set "Import Match Sets">
diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu.js b/Open-ILS/xul/staff_client/chrome/content/main/menu.js
index 29f0901..1bb5db9 100644
--- a/Open-ILS/xul/staff_client/chrome/content/main/menu.js
+++ b/Open-ILS/xul/staff_client/chrome/content/main/menu.js
@@ -1021,6 +1021,10 @@ main.menu.prototype = {
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/config/z3950_source', null, event); }
             ],
+            'cmd_server_admin_org_unit_proximity_adjustment' : [
+                ['oncommand'],
+                function(event) { open_eg_web_page('conify/global/config/org_unit_proximity_adjustment', null, event); }
+            ],
             'cmd_server_admin_circ_mod' : [
                 ['oncommand'],
                 function(event) { open_eg_web_page('conify/global/config/circ_modifier', null, event); }
diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
index 8967f9c..347fac7 100644
--- a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
+++ b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul
@@ -257,6 +257,7 @@
     <command id="cmd_server_admin_z39_source" 
              perm="ADMIN_Z3950_SOURCE"
              />
+    <command id="cmd_server_admin_org_unit_proximity_adjustment" />
     <command id="cmd_server_admin_circ_mod" 
              perm="CREATE_CIRC_MOD DELETE_CIRC_MOD UPDATE_CIRC_MOD ADMIN_CIRC_MOD"
              />
@@ -599,6 +600,7 @@
             <menupopup id="main.menu.admin.server.popup">
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.org_unit_type.label;" command="cmd_server_admin_org_type"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.org_unit.label;" command="cmd_server_admin_org_unit"/>
+                <menuitem label="&staff.main.menu.admin.server_admin.conify.org_unit_proximity_adjustment.label;" command="cmd_server_admin_org_unit_proximity_adjustment"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.grp_tree.label;" command="cmd_server_admin_grp_tree"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.perm_list.label;" command="cmd_server_admin_perm_list"/>
                 <menuitem label="&staff.main.menu.admin.server_admin.conify.copy_status.label;" command="cmd_server_admin_copy_status"/>
diff --git a/docs/RELEASE_NOTES_NEXT/calculated-proximity-adjustments.txt b/docs/RELEASE_NOTES_NEXT/calculated-proximity-adjustments.txt
new file mode 100644
index 0000000..61a81a7
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/calculated-proximity-adjustments.txt
@@ -0,0 +1,10 @@
+Calculated Proximity Adjustments
+================================
+
+Allows customization to the way that Evergreen measures the distance between
+org units for the purposes of 1) determining what copy at what org unit is best
+suited for targeting a title-level hold, and 2) determining what hold is best
+suited for fulfillment by a copy-in-hand at capture (checkin) time.  The
+customization is based on a table 'actor.org_unit_proximity_adjustment', with
+certain matching criteria that the system compares to properties of the holds
+and copies in question.
diff --git a/docs/TechRef/Circ/calculated-proximity-adjustments.txt b/docs/TechRef/Circ/calculated-proximity-adjustments.txt
new file mode 100644
index 0000000..586d6fc
--- /dev/null
+++ b/docs/TechRef/Circ/calculated-proximity-adjustments.txt
@@ -0,0 +1,46 @@
+Calculated Proximity Adjustments
+================================
+
+Summary
+-------
+
+Today in Evergreen, the way in which organizational hierarchy can be taken into account during hold targeting and capture is through the evaluation of Org Unit Proximity.  This is defined as the number of graph edges between Org Units, and for holds, specifically the distance between the capturing library and the pickup library. This value is used to rank sets of potential copies for holds based on their apparent nearness or proximity to the pickup lib at targeting time and to the checkin lib at op-capture time (in certain configurations).
+
+Evergreen needs a mechanism by which the proximity between libraries can be adjusted for the purpose of effecting hold capture.  This will support several use cases, including, but not limited to:
+
+  * Causing a specific library to be targeted for holds in preference to all others.
+  * Causing a specific library to be targeted for holds in preference to all others except for the pickup library.
+  * Allowing transit distance to be more accurately reflected in hold order choice, for instance, causing nearby systems to have lower effective transit distances than widely separated systems.
+  * Reporting on the true cost of transiting items in a broadly distributed consortium.
+
+Overview
+--------
+
+Evergreen can be made to provide a way to specify two types of proximity adjustment: Relative and Absolute.
+
+Relative proximity adjustment will allow Org Units, and descendants thereof, to be treated as closer or farther from one another than the simple edge distance describes by adding or subtracting full or partial edge distance amounts to the baseline edge distance under configured circumstances.
+
+Absolute proximity adjustment will allow Org Units, and descendants thereof, to be viewed as having a specific distance from one another that replaces the baseline edge distance under configure circumstances. This will naturally have an impact on how potential copies are evaluated for their 'proximity' when targeting holds and capturing copies for holds.
+
+Plan
+----
+
+Create a configuration interface allowing certain item- and hold-level criteria to be evaluated at targeting time.  Among the criteria would be:
+
+  * Item circ library (or ancestor thereof)
+  * Item owning library (or ancestor thereof)
+  * Hold pickup library (or ancestor thereof)
+  * Hold request library (or ancestor thereof)
+  * Item circ modifier
+  * Item shelving location
+
+At least one criterion must be supplied.  These criteria would be ranked by order, and reordering allowed.
+
+In addition to these criteria, an Absolute or Relative proximity adjustment would be supplied.  For Absolute proximity adjustments, the highest-ranked criteria-matching rule would be used for the copy.  For Relative proximity adjustments, all applicable adjustments would be summed.  In the case that both Absolute and Relative adjustments are found for the currently evaluated item and hold, the Absolute proximity adjustment will replace the baseline edge distance and then be modified by the Relative proximity adjustment calculation.
+
+To support both targeting-time and capture-time use of this derived proximity information, the calculated value will be stored on the hold-copy map.  In conjunction with the Custom Best-hold Sort Order proposal, this information would then be available for use in choosing the hold to be filled by a particular copy.
+
+
+////
+vim: ft=asciidoc
+////

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |   57 ++++
 .../perlmods/lib/OpenILS/Application/Circ/Holds.pm |    8 +-
 .../lib/OpenILS/Application/Storage/CDBI/action.pm |    2 +-
 .../Application/Storage/Publisher/action.pm        |  269 +++++++++++++++---
 .../OpenILS/Application/Storage/Publisher/asset.pm |   32 ++-
 Open-ILS/src/sql/Pg/002.schema.config.sql          |   29 ++-
 Open-ILS/src/sql/Pg/005.schema.actors.sql          |   21 ++
 Open-ILS/src/sql/Pg/020.schema.functions.sql       |   11 +
 Open-ILS/src/sql/Pg/090.schema.action.sql          |   82 ++++++
 Open-ILS/src/sql/Pg/800.fkeys.sql                  |    3 +
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |  107 +++++++-
 .../sql/Pg/upgrade/0759.schema.org_prox_adjust.sql |  126 ++++++++
 .../0760.schema.custom-best-hold-selection.sql     |  150 ++++++++++
 .../conify/global/config/best_hold_order.tt2       |  119 ++++++++
 .../config/org_unit_proximity_adjustment.tt2       |   85 ++++++
 .../web/js/dojo/openils/conify/BestHoldOrder.js    |  307 ++++++++++++++++++++
 Open-ILS/web/js/dojo/openils/conify/nls/conify.js  |    7 +-
 .../web/js/dojo/openils/widget/AutoFieldWidget.js  |    1 +
 Open-ILS/web/opac/locale/en-US/lang.dtd            |    2 +
 .../xul/staff_client/chrome/content/main/menu.js   |    8 +
 .../chrome/content/main/menu_frame_menus.xul       |    6 +
 .../calculated-proximity-adjustments.txt           |   10 +
 .../custom-best-hold-selection.txt                 |   16 +
 .../Circ/calculated-proximity-adjustments.txt      |   46 +++
 docs/TechRef/Circ/custom-best-hold-selection.txt   |  247 ++++++++++++++++
 25 files changed, 1702 insertions(+), 49 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0759.schema.org_prox_adjust.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/0760.schema.custom-best-hold-selection.sql
 create mode 100644 Open-ILS/src/templates/conify/global/config/best_hold_order.tt2
 create mode 100644 Open-ILS/src/templates/conify/global/config/org_unit_proximity_adjustment.tt2
 create mode 100644 Open-ILS/web/js/dojo/openils/conify/BestHoldOrder.js
 create mode 100644 docs/RELEASE_NOTES_NEXT/calculated-proximity-adjustments.txt
 create mode 100644 docs/RELEASE_NOTES_NEXT/custom-best-hold-selection.txt
 create mode 100644 docs/TechRef/Circ/calculated-proximity-adjustments.txt
 create mode 100644 docs/TechRef/Circ/custom-best-hold-selection.txt


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list