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, main has been updated
via ea83ead3ec1eb198e32c68dffbeb341b5deeb89a (commit)
via f19a3900af8395f29e212ab5d1267b5d2812abff (commit)
from f6f879ab8f0f3934ea8f0f71120372cfdbd29980 (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 ea83ead3ec1eb198e32c68dffbeb341b5deeb89a
Author: Chris Sharp <csharp(a)georgialibraries.org>
Date: Fri Jul 11 14:11:56 2025 -0400
LP#1648276: add release notes
Signed-off-by: Chris Sharp <csharp(a)georgialibraries.org>
diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/hold_policies_based_on_shelving_location.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/hold_policies_based_on_shelving_location.adoc
new file mode 100644
index 0000000000..6f4cec3cfc
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Circulation/hold_policies_based_on_shelving_location.adoc
@@ -0,0 +1,5 @@
+== Added Shelving Location to Hold Policies ==
+
+A new Shelving Location field has been added to hold policies
+and hold policy weights. This allows libraries to configure
+hold placement options based on which location the item belongs to.
commit f19a3900af8395f29e212ab5d1267b5d2812abff
Author: Chris Sharp <csharp(a)georgialibraries.org>
Date: Fri Apr 18 16:57:17 2025 -0400
LP#1648276: Add Shelving (copy) Location to Hold Matrix
Add copy_location to the fields in hold_matrix_matchpoint
and alter the function to check it. More or less copy/pasted
from the circ_modifier check.
Signed-off-by: Chris Sharp <csharp(a)georgialibraries.org>
Signed-off-by: Elizabeth Davis <elizabeth.davis(a)sparkpa.org>
diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 477a05af8c..2581b54d84 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -2124,6 +2124,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
<field reporter:label="User Permission Group" name="usr_grp" reporter:datatype="link"/>
<field reporter:label="Requestor Permission Group" name="requestor_grp" reporter:datatype="link" oils_obj:required="true"/>
<field reporter:label="Circulation Modifier" name="circ_modifier" oils_persist:primitive="string" reporter:datatype="link"/>
+ <field reporter:label="Shelving Location" name="copy_location" oils_persist:primitive="int" reporter:datatype="link"/>
<field reporter:label="MARC Type" name="marc_type" oils_persist:primitive="string" reporter:datatype="link"/>
<field reporter:label="MARC Form" name="marc_form" oils_persist:primitive="string" reporter:datatype="link"/>
<field reporter:label="MARC Bib Level" name="marc_bib_level" oils_persist:primitive="string" reporter:datatype="link"/>
@@ -2148,6 +2149,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
<link field="usr_grp" reltype="has_a" key="id" map="" class="pgt"/>
<link field="requestor_grp" reltype="has_a" key="id" map="" class="pgt"/>
<link field="circ_modifier" reltype="has_a" key="code" map="" class="ccm"/>
+ <link field="copy_location" reltype="has_a" key="id" map="" class="acpl"/>
<link field="marc_type" reltype="has_a" key="code" map="" class="citm"/>
<link field="marc_form" reltype="has_a" key="code" map="" class="cifm"/>
<link field="marc_bib_level" reltype="has_a" key="code" map="" class="cblvl"/>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts
index 25ddf1457d..26f44c15ed 100644
--- a/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts
+++ b/Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts
@@ -20,7 +20,7 @@ const routes: Routes = [{
table: 'hold_matrix_matchpoint',
disableOrgFilter: true,
fieldOrder: 'description,active,item_owning_ou,item_circ_ou,circ_modifier,' +
- 'marc_type,marc_form,marc_bib_level,marc_vr_format,ref_flag,item_age,' +
+ 'copy_location,marc_type,marc_form,marc_bib_level,marc_vr_format,ref_flag,item_age,' +
'user_home_ou,usr_grp,stop_blocked_user,holdable,age_hold_protect_rule,' +
'max_holds,include_frozen_holds,request_ou,pickup_ou,strict_ou_match,' +
'transit_range,distance_is_from_owner,requestor_grp,id'
diff --git a/Open-ILS/src/sql/Pg/099.matrix_weights.sql b/Open-ILS/src/sql/Pg/099.matrix_weights.sql
index 22210116d8..73ce8273bb 100644
--- a/Open-ILS/src/sql/Pg/099.matrix_weights.sql
+++ b/Open-ILS/src/sql/Pg/099.matrix_weights.sql
@@ -33,6 +33,7 @@ CREATE TABLE config.hold_matrix_weights (
pickup_ou NUMERIC(6,2) NOT NULL,
item_owning_ou NUMERIC(6,2) NOT NULL,
item_circ_ou NUMERIC(6,2) NOT NULL,
+ copy_location NUMERIC(6,2) NOT NULL,
usr_grp NUMERIC(6,2) NOT NULL,
requestor_grp NUMERIC(6,2) NOT NULL,
circ_modifier NUMERIC(6,2) NOT NULL,
diff --git a/Open-ILS/src/sql/Pg/110.hold_matrix.sql b/Open-ILS/src/sql/Pg/110.hold_matrix.sql
index a48e72f31f..ee783579d3 100644
--- a/Open-ILS/src/sql/Pg/110.hold_matrix.sql
+++ b/Open-ILS/src/sql/Pg/110.hold_matrix.sql
@@ -41,6 +41,7 @@ CREATE TABLE config.hold_matrix_matchpoint (
usr_grp INT REFERENCES permission.grp_tree (id) DEFERRABLE INITIALLY DEFERRED, -- Set to the top applicable group from the group tree; will need descendents and prox functions for filtering
requestor_grp INT NOT NULL REFERENCES permission.grp_tree (id) DEFERRABLE INITIALLY DEFERRED, -- Set to the top applicable group from the group tree; will need descendents and prox functions for filtering
circ_modifier TEXT REFERENCES config.circ_modifier (code) DEFERRABLE INITIALLY DEFERRED,
+ copy_location INT REFERENCES asset.copy_location (id) DEFERRABLE INITIALLY DEFERRED,
marc_type TEXT,
marc_form TEXT,
marc_bib_level TEXT,
@@ -60,7 +61,7 @@ CREATE TABLE config.hold_matrix_matchpoint (
);
-- Nulls don't count for a constraint match, so we have to coalesce them into something that does.
-CREATE UNIQUE INDEX chmm_once_per_paramset ON config.hold_matrix_matchpoint (COALESCE(user_home_ou::TEXT, ''), COALESCE(request_ou::TEXT, ''), COALESCE(pickup_ou::TEXT, ''), COALESCE(item_owning_ou::TEXT, ''), COALESCE(item_circ_ou::TEXT, ''), COALESCE(usr_grp::TEXT, ''), COALESCE(requestor_grp::TEXT, ''), COALESCE(circ_modifier, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level, ''), COALESCE(marc_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(item_age, '0 seconds')) WHERE active;
+CREATE UNIQUE INDEX chmm_once_per_paramset ON config.hold_matrix_matchpoint (COALESCE(user_home_ou::TEXT, ''), COALESCE(request_ou::TEXT, ''), COALESCE(pickup_ou::TEXT, ''), COALESCE(item_owning_ou::TEXT, ''), COALESCE(item_circ_ou::TEXT, ''), COALESCE(usr_grp::TEXT, ''), COALESCE(requestor_grp::TEXT, ''), COALESCE(circ_modifier, ''), COALESCE(copy_location::TEXT, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level, ''), COALESCE(marc_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(item_age, '0 seconds')) WHERE active;
/*
Figure out which rules apply for a given hold
@@ -129,6 +130,7 @@ BEGIN
weights.usr_grp := 7.0;
weights.requestor_grp := 8.0;
weights.circ_modifier := 4.0;
+ weights.copy_location := 4.0;
weights.marc_type := 3.0;
weights.marc_form := 2.0;
weights.marc_bib_level := 1.0;
@@ -186,6 +188,7 @@ BEGIN
AND (m.juvenile_flag IS NULL OR m.juvenile_flag = user_object.juvenile)
-- Static Item Checks
AND (m.circ_modifier IS NULL OR m.circ_modifier = item_object.circ_modifier)
+ AND (m.copy_location IS NULL OR m.copy_location = item_object.location)
AND (m.marc_type IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
AND (m.marc_form IS NULL OR m.marc_form = rec_descriptor.item_form)
AND (m.marc_bib_level IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
@@ -206,6 +209,7 @@ BEGIN
CASE WHEN m.juvenile_flag IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
-- Static Item Checks
CASE WHEN m.circ_modifier IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
+ CASE WHEN m.copy_location IS NOT NULL THEN 4^weights.copy_location ELSE 0.0 END +
CASE WHEN m.marc_type IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
CASE WHEN m.marc_form IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
CASE WHEN m.marc_vr_format IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.add_copy_location_to_hold_matrix.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.add_copy_location_to_hold_matrix.sql
new file mode 100644
index 0000000000..e0369d466b
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.add_copy_location_to_hold_matrix.sql
@@ -0,0 +1,172 @@
+BEGIN;
+
+ALTER TABLE config.hold_matrix_matchpoint ADD COLUMN copy_location INT REFERENCES asset.copy_location (id) DEFERRABLE INITIALLY DEFERRED;
+
+DROP INDEX config.chmm_once_per_paramset;
+
+CREATE UNIQUE INDEX chmm_once_per_paramset ON config.hold_matrix_matchpoint (COALESCE(user_home_ou::TEXT, ''), COALESCE(request_ou::TEXT, ''), COALESCE(pickup_ou::TEXT, ''), COALESCE(item_owning_ou::TEXT, ''), COALESCE(item_circ_ou::TEXT, ''), COALESCE(usr_grp::TEXT, ''), COALESCE(requestor_grp::TEXT, ''), COALESCE(circ_modifier, ''), COALESCE(copy_location::TEXT, ''), COALESCE(marc_type, ''), COALESCE(marc_form, ''), COALESCE(marc_bib_level, ''), COALESCE(marc_vr_format, ''), COALESCE(juvenile_flag::TEXT, ''), COALESCE(ref_flag::TEXT, ''), COALESCE(item_age, '0 seconds')) WHERE active;
+
+CREATE OR REPLACE FUNCTION action.find_hold_matrix_matchpoint(pickup_ou integer, request_ou integer, match_item bigint, match_user integer, match_requestor integer)
+ RETURNS integer AS
+$func$
+DECLARE
+ requestor_object actor.usr%ROWTYPE;
+ user_object actor.usr%ROWTYPE;
+ item_object asset.copy%ROWTYPE;
+ item_cn_object asset.call_number%ROWTYPE;
+ my_item_age INTERVAL;
+ rec_descriptor metabib.rec_descriptor%ROWTYPE;
+ matchpoint config.hold_matrix_matchpoint%ROWTYPE;
+ weights config.hold_matrix_weights%ROWTYPE;
+ denominator NUMERIC(6,2);
+ v_pickup_ou ALIAS FOR pickup_ou;
+ v_request_ou ALIAS FOR request_ou;
+BEGIN
+ SELECT INTO user_object * FROM actor.usr WHERE id = match_user;
+ SELECT INTO requestor_object * FROM actor.usr WHERE id = match_requestor;
+ SELECT INTO item_object * FROM asset.copy WHERE id = match_item;
+ SELECT INTO item_cn_object * FROM asset.call_number WHERE id = item_object.call_number;
+ SELECT INTO rec_descriptor * FROM metabib.rec_descriptor WHERE record = item_cn_object.record;
+
+ SELECT INTO my_item_age age(coalesce(item_object.active_date, now()));
+
+ -- The item's owner should probably be the one determining if the item is holdable
+ -- How to decide that is debatable. Decided to default to the circ library (where the item lives)
+ -- This flag will allow for setting it to the owning library (where the call number "lives")
+ PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.weight_owner_not_circ' AND enabled;
+
+ -- Grab the closest set circ weight setting.
+ IF NOT FOUND THEN
+ -- Default to circ library
+ SELECT INTO weights hw.*
+ FROM config.weight_assoc wa
+ JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
+ JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) d ON (wa.org_unit = d.id)
+ WHERE active
+ ORDER BY d.distance
+ LIMIT 1;
+ ELSE
+ -- Flag is set, use owning library
+ SELECT INTO weights hw.*
+ FROM config.weight_assoc wa
+ JOIN config.hold_matrix_weights hw ON (hw.id = wa.hold_weights)
+ JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) d ON (wa.org_unit = d.id)
+ WHERE active
+ ORDER BY d.distance
+ LIMIT 1;
+ END IF;
+
+ -- No weights? Bad admin! Defaults to handle that anyway.
+ IF weights.id IS NULL THEN
+ weights.user_home_ou := 5.0;
+ weights.request_ou := 5.0;
+ weights.pickup_ou := 5.0;
+ weights.item_owning_ou := 5.0;
+ weights.item_circ_ou := 5.0;
+ weights.usr_grp := 7.0;
+ weights.requestor_grp := 8.0;
+ weights.circ_modifier := 4.0;
+ weights.copy_location := 4.0;
+ weights.marc_type := 3.0;
+ weights.marc_form := 2.0;
+ weights.marc_bib_level := 1.0;
+ weights.marc_vr_format := 1.0;
+ weights.juvenile_flag := 4.0;
+ weights.ref_flag := 0.0;
+ weights.item_age := 0.0;
+ END IF;
+
+ -- Determine the max (expected) depth (+1) of the org tree and max depth of the permisson tree
+ -- If you break your org tree with funky parenting this may be wrong
+ -- Note: This CTE is duplicated in the find_circ_matrix_matchpoint function, and it may be a good idea to split it off to a function
+ -- We use one denominator for all tree-based checks for when permission groups and org units have the same weighting
+ WITH all_distance(distance) AS (
+ SELECT depth AS distance FROM actor.org_unit_type
+ UNION
+ SELECT distance AS distance FROM permission.grp_ancestors_distance((SELECT id FROM permission.grp_tree WHERE parent IS NULL))
+ )
+ SELECT INTO denominator MAX(distance) + 1 FROM all_distance;
+
+ -- To ATTEMPT to make this work like it used to, make it reverse the user/requestor profile ids.
+ -- This may be better implemented as part of the upgrade script?
+ -- Set usr_grp = requestor_grp, requestor_grp = 1 or something when this flag is already set
+ -- Then remove this flag, of course.
+ PERFORM * FROM config.internal_flag WHERE name = 'circ.holds.usr_not_requestor' AND enabled;
+
+ IF FOUND THEN
+ -- Note: This, to me, is REALLY hacky. I put it in anyway.
+ -- If you can't tell, this is a single call swap on two variables.
+ SELECT INTO user_object.profile, requestor_object.profile
+ requestor_object.profile, user_object.profile;
+ END IF;
+
+ -- Select the winning matchpoint into the matchpoint variable for returning
+ SELECT INTO matchpoint m.*
+ FROM config.hold_matrix_matchpoint m
+ /*LEFT*/ JOIN permission.grp_ancestors_distance( requestor_object.profile ) rpgad ON m.requestor_grp = rpgad.id
+ LEFT JOIN permission.grp_ancestors_distance( user_object.profile ) upgad ON m.usr_grp = upgad.id
+ LEFT JOIN actor.org_unit_ancestors_distance( v_pickup_ou ) puoua ON m.pickup_ou = puoua.id
+ LEFT JOIN actor.org_unit_ancestors_distance( v_request_ou ) rqoua ON m.request_ou = rqoua.id
+ LEFT JOIN actor.org_unit_ancestors_distance( item_cn_object.owning_lib ) cnoua ON m.item_owning_ou = cnoua.id
+ LEFT JOIN actor.org_unit_ancestors_distance( item_object.circ_lib ) iooua ON m.item_circ_ou = iooua.id
+ LEFT JOIN actor.org_unit_ancestors_distance( user_object.home_ou ) uhoua ON m.user_home_ou = uhoua.id
+ WHERE m.active
+ -- Permission Groups
+ -- AND (m.requestor_grp IS NULL OR upgad.id IS NOT NULL) -- Optional Requestor Group?
+ AND (m.usr_grp IS NULL OR upgad.id IS NOT NULL)
+ -- Org Units
+ AND (m.pickup_ou IS NULL OR (puoua.id IS NOT NULL AND (puoua.distance = 0 OR NOT m.strict_ou_match)))
+ AND (m.request_ou IS NULL OR (rqoua.id IS NOT NULL AND (rqoua.distance = 0 OR NOT m.strict_ou_match)))
+ AND (m.item_owning_ou IS NULL OR (cnoua.id IS NOT NULL AND (cnoua.distance = 0 OR NOT m.strict_ou_match)))
+ AND (m.item_circ_ou IS NULL OR (iooua.id IS NOT NULL AND (iooua.distance = 0 OR NOT m.strict_ou_match)))
+ AND (m.user_home_ou IS NULL OR (uhoua.id IS NOT NULL AND (uhoua.distance = 0 OR NOT m.strict_ou_match)))
+ -- Static User Checks
+ AND (m.juvenile_flag IS NULL OR m.juvenile_flag = user_object.juvenile)
+ -- Static Item Checks
+ AND (m.circ_modifier IS NULL OR m.circ_modifier = item_object.circ_modifier)
+ AND (m.copy_location IS NULL OR m.copy_location = item_object.location)
+ AND (m.marc_type IS NULL OR m.marc_type = COALESCE(item_object.circ_as_type, rec_descriptor.item_type))
+ AND (m.marc_form IS NULL OR m.marc_form = rec_descriptor.item_form)
+ AND (m.marc_bib_level IS NULL OR m.marc_bib_level = rec_descriptor.bib_level)
+ AND (m.marc_vr_format IS NULL OR m.marc_vr_format = rec_descriptor.vr_format)
+ AND (m.ref_flag IS NULL OR m.ref_flag = item_object.ref)
+ AND (m.item_age IS NULL OR (my_item_age IS NOT NULL AND m.item_age > my_item_age))
+ ORDER BY
+ -- Permission Groups
+ CASE WHEN rpgad.distance IS NOT NULL THEN 2^(2*weights.requestor_grp - (rpgad.distance/denominator)) ELSE 0.0 END +
+ CASE WHEN upgad.distance IS NOT NULL THEN 2^(2*weights.usr_grp - (upgad.distance/denominator)) ELSE 0.0 END +
+ -- Org Units
+ CASE WHEN puoua.distance IS NOT NULL THEN 2^(2*weights.pickup_ou - (puoua.distance/denominator)) ELSE 0.0 END +
+ CASE WHEN rqoua.distance IS NOT NULL THEN 2^(2*weights.request_ou - (rqoua.distance/denominator)) ELSE 0.0 END +
+ CASE WHEN cnoua.distance IS NOT NULL THEN 2^(2*weights.item_owning_ou - (cnoua.distance/denominator)) ELSE 0.0 END +
+ CASE WHEN iooua.distance IS NOT NULL THEN 2^(2*weights.item_circ_ou - (iooua.distance/denominator)) ELSE 0.0 END +
+ CASE WHEN uhoua.distance IS NOT NULL THEN 2^(2*weights.user_home_ou - (uhoua.distance/denominator)) ELSE 0.0 END +
+ -- Static User Checks -- Note: 4^x is equiv to 2^(2*x)
+ CASE WHEN m.juvenile_flag IS NOT NULL THEN 4^weights.juvenile_flag ELSE 0.0 END +
+ -- Static Item Checks
+ CASE WHEN m.circ_modifier IS NOT NULL THEN 4^weights.circ_modifier ELSE 0.0 END +
+ CASE WHEN m.copy_location IS NOT NULL THEN 4^weights.copy_location ELSE 0.0 END +
+ CASE WHEN m.marc_type IS NOT NULL THEN 4^weights.marc_type ELSE 0.0 END +
+ CASE WHEN m.marc_form IS NOT NULL THEN 4^weights.marc_form ELSE 0.0 END +
+ CASE WHEN m.marc_vr_format IS NOT NULL THEN 4^weights.marc_vr_format ELSE 0.0 END +
+ CASE WHEN m.ref_flag IS NOT NULL THEN 4^weights.ref_flag ELSE 0.0 END +
+ -- Item age has a slight adjustment to weight based on value.
+ -- This should ensure that a shorter age limit comes first when all else is equal.
+ -- NOTE: This assumes that intervals will normally be in days.
+ CASE WHEN m.item_age IS NOT NULL THEN 4^weights.item_age - 86400/EXTRACT(EPOCH FROM m.item_age) ELSE 0.0 END DESC,
+ -- Final sort on id, so that if two rules have the same sorting in the previous sort they have a defined order
+ -- This prevents "we changed the table order by updating a rule, and we started getting different results"
+ m.id;
+
+ -- Return just the ID for now
+ RETURN matchpoint.id;
+END;
+$func$ LANGUAGE 'plpgsql';
+
+ALTER TABLE config.hold_matrix_weights ADD COLUMN copy_location NUMERIC(6,2);
+-- we need to set some values, so initially, match whatever the weight for circ_modifier is
+UPDATE config.hold_matrix_weights SET copy_location = circ_modifier;
+ALTER TABLE config.hold_matrix_weights ALTER COLUMN copy_location SET NOT NULL;
+
+COMMIT;
+
-----------------------------------------------------------------------
Summary of changes:
Open-ILS/examples/fm_IDL.xml | 2 +
.../src/app/staff/admin/local/routing.module.ts | 2 +-
Open-ILS/src/sql/Pg/099.matrix_weights.sql | 1 +
Open-ILS/src/sql/Pg/110.hold_matrix.sql | 6 +-
...XX.schema.add_copy_location_to_hold_matrix.sql} | 148 ++++-----------------
.../hold_policies_based_on_shelving_location.adoc | 5 +
6 files changed, 43 insertions(+), 121 deletions(-)
copy Open-ILS/src/sql/Pg/upgrade/{0483.dynamic_weights_fix.sql => XXXX.schema.add_copy_location_to_hold_matrix.sql} (52%)
create mode 100644 docs/RELEASE_NOTES_NEXT/Circulation/hold_policies_based_on_shelving_location.adoc
hooks/post-receive
--
Evergreen ILS