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 ('1341', :eg_version); -- miker/sandbergja/dyrcona/blake
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1342', :eg_version); -- jeffdavis/blake/sandbergja
CREATE TABLE config.bib_source (
id SERIAL PRIMARY KEY,
--- /dev/null
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('1342', :eg_version);
+
+INSERT INTO config.org_unit_setting_type
+ (name, label, datatype, description, grp, update_perm, view_perm)
+VALUES (
+ 'circ.permit_renew_when_exceeds_fines',
+ oils_i18n_gettext(
+ 'circ.permit_renew_when_exceeds_fines',
+ 'Permit renewals when patron exceeds max fine threshold',
+ 'coust',
+ 'label'
+ ),
+ 'bool',
+ oils_i18n_gettext(
+ 'circ.permit_renew_when_exceeds_fines',
+ 'Permit renewals even when the patron exceeds the maximum fine threshold',
+ 'coust',
+ 'description'
+ ),
+ 'opac',
+ 93,
+ NULL
+);
+
+CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
+DECLARE
+ user_object actor.usr%ROWTYPE;
+ standing_penalty config.standing_penalty%ROWTYPE;
+ item_object asset.copy%ROWTYPE;
+ item_status_object config.copy_status%ROWTYPE;
+ item_location_object asset.copy_location%ROWTYPE;
+ result action.circ_matrix_test_result;
+ circ_test action.found_circ_matrix_matchpoint;
+ circ_matchpoint config.circ_matrix_matchpoint%ROWTYPE;
+ circ_limit_set config.circ_limit_set%ROWTYPE;
+ hold_ratio action.hold_stats%ROWTYPE;
+ penalty_type TEXT;
+ items_out INT;
+ context_org_list INT[];
+ permit_renew TEXT;
+ done BOOL := FALSE;
+BEGIN
+ -- Assume success unless we hit a failure condition
+ result.success := TRUE;
+
+ -- Need user info to look up matchpoints
+ SELECT INTO user_object * FROM actor.usr WHERE id = match_user AND NOT deleted;
+
+ -- (Insta)Fail if we couldn't find the user
+ IF user_object.id IS NULL THEN
+ result.fail_part := 'no_user';
+ result.success := FALSE;
+ done := TRUE;
+ RETURN NEXT result;
+ RETURN;
+ END IF;
+
+ -- Need item info to look up matchpoints
+ SELECT INTO item_object * FROM asset.copy WHERE id = match_item AND NOT deleted;
+
+ -- (Insta)Fail if we couldn't find the item
+ IF item_object.id IS NULL THEN
+ result.fail_part := 'no_item';
+ result.success := FALSE;
+ done := TRUE;
+ RETURN NEXT result;
+ RETURN;
+ END IF;
+
+ SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
+
+ circ_matchpoint := circ_test.matchpoint;
+ result.matchpoint := circ_matchpoint.id;
+ result.circulate := circ_matchpoint.circulate;
+ result.duration_rule := circ_matchpoint.duration_rule;
+ result.recurring_fine_rule := circ_matchpoint.recurring_fine_rule;
+ result.max_fine_rule := circ_matchpoint.max_fine_rule;
+ result.hard_due_date := circ_matchpoint.hard_due_date;
+ result.renewals := circ_matchpoint.renewals;
+ result.grace_period := circ_matchpoint.grace_period;
+ result.buildrows := circ_test.buildrows;
+
+ -- (Insta)Fail if we couldn't find a matchpoint
+ IF circ_test.success = false THEN
+ result.fail_part := 'no_matchpoint';
+ result.success := FALSE;
+ done := TRUE;
+ RETURN NEXT result;
+ RETURN;
+ END IF;
+
+ -- All failures before this point are non-recoverable
+ -- Below this point are possibly overridable failures
+
+ -- Fail if the user is barred
+ IF user_object.barred IS TRUE THEN
+ result.fail_part := 'actor.usr.barred';
+ result.success := FALSE;
+ done := TRUE;
+ RETURN NEXT result;
+ END IF;
+
+ -- Fail if the item can't circulate
+ IF item_object.circulate IS FALSE THEN
+ result.fail_part := 'asset.copy.circulate';
+ result.success := FALSE;
+ done := TRUE;
+ RETURN NEXT result;
+ END IF;
+
+ -- Fail if the item isn't in a circulateable status on a non-renewal
+ IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN
+ result.fail_part := 'asset.copy.status';
+ result.success := FALSE;
+ done := TRUE;
+ RETURN NEXT result;
+ -- Alternately, fail if the item isn't checked out on a renewal
+ ELSIF renewal AND item_object.status <> 1 THEN
+ result.fail_part := 'asset.copy.status';
+ result.success := FALSE;
+ done := TRUE;
+ RETURN NEXT result;
+ END IF;
+
+ -- Fail if the item can't circulate because of the shelving location
+ SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
+ IF item_location_object.circulate IS FALSE THEN
+ result.fail_part := 'asset.copy_location.circulate';
+ result.success := FALSE;
+ done := TRUE;
+ RETURN NEXT result;
+ END IF;
+
+ -- Use Circ OU for penalties and such
+ SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( circ_ou );
+
+ IF renewal THEN
+ penalty_type = '%RENEW%';
+ ELSE
+ penalty_type = '%CIRC%';
+ END IF;
+
+ FOR standing_penalty IN
+ SELECT DISTINCT csp.*
+ FROM actor.usr_standing_penalty usp
+ JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
+ WHERE usr = match_user
+ AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
+ AND (usp.stop_date IS NULL or usp.stop_date > NOW())
+ AND csp.block_list LIKE penalty_type LOOP
+
+ -- override PATRON_EXCEEDS_FINES penalty for renewals based on org setting
+ IF renewal AND standing_penalty.name = 'PATRON_EXCEEDS_FINES' THEN
+ SELECT INTO permit_renew value FROM actor.org_unit_ancestor_setting('circ.permit_renew_when_exceeds_fines', circ_ou);
+ IF permit_renew IS NOT NULL AND permit_renew ILIKE 'true' THEN
+ CONTINUE;
+ END IF;
+ END IF;
+
+ result.fail_part := standing_penalty.name;
+ result.success := FALSE;
+ done := TRUE;
+ RETURN NEXT result;
+ END LOOP;
+
+ -- Fail if the test is set to hard non-circulating
+ IF circ_matchpoint.circulate IS FALSE THEN
+ result.fail_part := 'config.circ_matrix_test.circulate';
+ result.success := FALSE;
+ done := TRUE;
+ RETURN NEXT result;
+ END IF;
+
+ -- Fail if the total copy-hold ratio is too low
+ IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
+ SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
+ IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
+ result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
+ result.success := FALSE;
+ done := TRUE;
+ RETURN NEXT result;
+ END IF;
+ END IF;
+
+ -- Fail if the available copy-hold ratio is too low
+ IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
+ IF hold_ratio.hold_count IS NULL THEN
+ SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
+ END IF;
+ IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
+ result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
+ result.success := FALSE;
+ done := TRUE;
+ RETURN NEXT result;
+ END IF;
+ END IF;
+
+ -- Fail if the user has too many items out by defined limit sets
+ FOR circ_limit_set IN SELECT ccls.* FROM config.circ_limit_set ccls
+ JOIN config.circ_matrix_limit_set_map ccmlsm ON ccmlsm.limit_set = ccls.id
+ WHERE ccmlsm.active AND ( ccmlsm.matchpoint = circ_matchpoint.id OR
+ ( ccmlsm.matchpoint IN (SELECT * FROM unnest(result.buildrows)) AND ccmlsm.fallthrough )
+ ) LOOP
+ IF circ_limit_set.items_out > 0 AND NOT renewal THEN
+ SELECT INTO context_org_list ARRAY_AGG(aou.id)
+ FROM actor.org_unit_full_path( circ_ou ) aou
+ JOIN actor.org_unit_type aout ON aou.ou_type = aout.id
+ WHERE aout.depth >= circ_limit_set.depth;
+ IF circ_limit_set.global THEN
+ WITH RECURSIVE descendant_depth AS (
+ SELECT ou.id,
+ ou.parent_ou
+ FROM actor.org_unit ou
+ WHERE ou.id IN (SELECT * FROM unnest(context_org_list))
+ UNION
+ SELECT ou.id,
+ ou.parent_ou
+ FROM actor.org_unit ou
+ JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
+ ) SELECT INTO context_org_list ARRAY_AGG(ou.id) FROM actor.org_unit ou JOIN descendant_depth USING (id);
+ END IF;
+ SELECT INTO items_out COUNT(DISTINCT circ.id)
+ FROM action.circulation circ
+ JOIN asset.copy copy ON (copy.id = circ.target_copy)
+ LEFT JOIN action.circulation_limit_group_map aclgm ON (circ.id = aclgm.circ)
+ WHERE circ.usr = match_user
+ AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
+ AND circ.checkin_time IS NULL
+ AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
+ AND (copy.circ_modifier IN (SELECT circ_mod FROM config.circ_limit_set_circ_mod_map WHERE limit_set = circ_limit_set.id)
+ OR copy.location IN (SELECT copy_loc FROM config.circ_limit_set_copy_loc_map WHERE limit_set = circ_limit_set.id)
+ OR aclgm.limit_group IN (SELECT limit_group FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id)
+ );
+ IF items_out >= circ_limit_set.items_out THEN
+ result.fail_part := 'config.circ_matrix_circ_mod_test';
+ result.success := FALSE;
+ done := TRUE;
+ RETURN NEXT result;
+ END IF;
+ END IF;
+ SELECT INTO result.limit_groups result.limit_groups || ARRAY_AGG(limit_group) FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id AND NOT check_only;
+ END LOOP;
+
+ -- If we passed everything, return the successful matchpoint
+ IF NOT done THEN
+ RETURN NEXT result;
+ END IF;
+
+ RETURN;
+END;
+$func$ LANGUAGE plpgsql;
+
+COMMIT;
+
+++ /dev/null
-BEGIN;
-
-INSERT INTO config.org_unit_setting_type
- (name, label, datatype, description, grp, update_perm, view_perm)
-VALUES (
- 'circ.permit_renew_when_exceeds_fines',
- oils_i18n_gettext(
- 'circ.permit_renew_when_exceeds_fines',
- 'Permit renewals when patron exceeds max fine threshold',
- 'coust',
- 'label'
- ),
- 'bool',
- oils_i18n_gettext(
- 'circ.permit_renew_when_exceeds_fines',
- 'Permit renewals even when the patron exceeds the maximum fine threshold',
- 'coust',
- 'description'
- ),
- 'opac',
- 93,
- NULL
-);
-
-CREATE OR REPLACE FUNCTION action.item_user_circ_test( circ_ou INT, match_item BIGINT, match_user INT, renewal BOOL ) RETURNS SETOF action.circ_matrix_test_result AS $func$
-DECLARE
- user_object actor.usr%ROWTYPE;
- standing_penalty config.standing_penalty%ROWTYPE;
- item_object asset.copy%ROWTYPE;
- item_status_object config.copy_status%ROWTYPE;
- item_location_object asset.copy_location%ROWTYPE;
- result action.circ_matrix_test_result;
- circ_test action.found_circ_matrix_matchpoint;
- circ_matchpoint config.circ_matrix_matchpoint%ROWTYPE;
- circ_limit_set config.circ_limit_set%ROWTYPE;
- hold_ratio action.hold_stats%ROWTYPE;
- penalty_type TEXT;
- items_out INT;
- context_org_list INT[];
- permit_renew TEXT;
- done BOOL := FALSE;
-BEGIN
- -- Assume success unless we hit a failure condition
- result.success := TRUE;
-
- -- Need user info to look up matchpoints
- SELECT INTO user_object * FROM actor.usr WHERE id = match_user AND NOT deleted;
-
- -- (Insta)Fail if we couldn't find the user
- IF user_object.id IS NULL THEN
- result.fail_part := 'no_user';
- result.success := FALSE;
- done := TRUE;
- RETURN NEXT result;
- RETURN;
- END IF;
-
- -- Need item info to look up matchpoints
- SELECT INTO item_object * FROM asset.copy WHERE id = match_item AND NOT deleted;
-
- -- (Insta)Fail if we couldn't find the item
- IF item_object.id IS NULL THEN
- result.fail_part := 'no_item';
- result.success := FALSE;
- done := TRUE;
- RETURN NEXT result;
- RETURN;
- END IF;
-
- SELECT INTO circ_test * FROM action.find_circ_matrix_matchpoint(circ_ou, item_object, user_object, renewal);
-
- circ_matchpoint := circ_test.matchpoint;
- result.matchpoint := circ_matchpoint.id;
- result.circulate := circ_matchpoint.circulate;
- result.duration_rule := circ_matchpoint.duration_rule;
- result.recurring_fine_rule := circ_matchpoint.recurring_fine_rule;
- result.max_fine_rule := circ_matchpoint.max_fine_rule;
- result.hard_due_date := circ_matchpoint.hard_due_date;
- result.renewals := circ_matchpoint.renewals;
- result.grace_period := circ_matchpoint.grace_period;
- result.buildrows := circ_test.buildrows;
-
- -- (Insta)Fail if we couldn't find a matchpoint
- IF circ_test.success = false THEN
- result.fail_part := 'no_matchpoint';
- result.success := FALSE;
- done := TRUE;
- RETURN NEXT result;
- RETURN;
- END IF;
-
- -- All failures before this point are non-recoverable
- -- Below this point are possibly overridable failures
-
- -- Fail if the user is barred
- IF user_object.barred IS TRUE THEN
- result.fail_part := 'actor.usr.barred';
- result.success := FALSE;
- done := TRUE;
- RETURN NEXT result;
- END IF;
-
- -- Fail if the item can't circulate
- IF item_object.circulate IS FALSE THEN
- result.fail_part := 'asset.copy.circulate';
- result.success := FALSE;
- done := TRUE;
- RETURN NEXT result;
- END IF;
-
- -- Fail if the item isn't in a circulateable status on a non-renewal
- IF NOT renewal AND item_object.status NOT IN ( 0, 7, 8 ) THEN
- result.fail_part := 'asset.copy.status';
- result.success := FALSE;
- done := TRUE;
- RETURN NEXT result;
- -- Alternately, fail if the item isn't checked out on a renewal
- ELSIF renewal AND item_object.status <> 1 THEN
- result.fail_part := 'asset.copy.status';
- result.success := FALSE;
- done := TRUE;
- RETURN NEXT result;
- END IF;
-
- -- Fail if the item can't circulate because of the shelving location
- SELECT INTO item_location_object * FROM asset.copy_location WHERE id = item_object.location;
- IF item_location_object.circulate IS FALSE THEN
- result.fail_part := 'asset.copy_location.circulate';
- result.success := FALSE;
- done := TRUE;
- RETURN NEXT result;
- END IF;
-
- -- Use Circ OU for penalties and such
- SELECT INTO context_org_list ARRAY_AGG(id) FROM actor.org_unit_full_path( circ_ou );
-
- IF renewal THEN
- penalty_type = '%RENEW%';
- ELSE
- penalty_type = '%CIRC%';
- END IF;
-
- FOR standing_penalty IN
- SELECT DISTINCT csp.*
- FROM actor.usr_standing_penalty usp
- JOIN config.standing_penalty csp ON (csp.id = usp.standing_penalty)
- WHERE usr = match_user
- AND usp.org_unit IN ( SELECT * FROM unnest(context_org_list) )
- AND (usp.stop_date IS NULL or usp.stop_date > NOW())
- AND csp.block_list LIKE penalty_type LOOP
-
- -- override PATRON_EXCEEDS_FINES penalty for renewals based on org setting
- IF renewal AND standing_penalty.name = 'PATRON_EXCEEDS_FINES' THEN
- SELECT INTO permit_renew value FROM actor.org_unit_ancestor_setting('circ.permit_renew_when_exceeds_fines', circ_ou);
- IF permit_renew IS NOT NULL AND permit_renew ILIKE 'true' THEN
- CONTINUE;
- END IF;
- END IF;
-
- result.fail_part := standing_penalty.name;
- result.success := FALSE;
- done := TRUE;
- RETURN NEXT result;
- END LOOP;
-
- -- Fail if the test is set to hard non-circulating
- IF circ_matchpoint.circulate IS FALSE THEN
- result.fail_part := 'config.circ_matrix_test.circulate';
- result.success := FALSE;
- done := TRUE;
- RETURN NEXT result;
- END IF;
-
- -- Fail if the total copy-hold ratio is too low
- IF circ_matchpoint.total_copy_hold_ratio IS NOT NULL THEN
- SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
- IF hold_ratio.total_copy_ratio IS NOT NULL AND hold_ratio.total_copy_ratio < circ_matchpoint.total_copy_hold_ratio THEN
- result.fail_part := 'config.circ_matrix_test.total_copy_hold_ratio';
- result.success := FALSE;
- done := TRUE;
- RETURN NEXT result;
- END IF;
- END IF;
-
- -- Fail if the available copy-hold ratio is too low
- IF circ_matchpoint.available_copy_hold_ratio IS NOT NULL THEN
- IF hold_ratio.hold_count IS NULL THEN
- SELECT INTO hold_ratio * FROM action.copy_related_hold_stats(match_item);
- END IF;
- IF hold_ratio.available_copy_ratio IS NOT NULL AND hold_ratio.available_copy_ratio < circ_matchpoint.available_copy_hold_ratio THEN
- result.fail_part := 'config.circ_matrix_test.available_copy_hold_ratio';
- result.success := FALSE;
- done := TRUE;
- RETURN NEXT result;
- END IF;
- END IF;
-
- -- Fail if the user has too many items out by defined limit sets
- FOR circ_limit_set IN SELECT ccls.* FROM config.circ_limit_set ccls
- JOIN config.circ_matrix_limit_set_map ccmlsm ON ccmlsm.limit_set = ccls.id
- WHERE ccmlsm.active AND ( ccmlsm.matchpoint = circ_matchpoint.id OR
- ( ccmlsm.matchpoint IN (SELECT * FROM unnest(result.buildrows)) AND ccmlsm.fallthrough )
- ) LOOP
- IF circ_limit_set.items_out > 0 AND NOT renewal THEN
- SELECT INTO context_org_list ARRAY_AGG(aou.id)
- FROM actor.org_unit_full_path( circ_ou ) aou
- JOIN actor.org_unit_type aout ON aou.ou_type = aout.id
- WHERE aout.depth >= circ_limit_set.depth;
- IF circ_limit_set.global THEN
- WITH RECURSIVE descendant_depth AS (
- SELECT ou.id,
- ou.parent_ou
- FROM actor.org_unit ou
- WHERE ou.id IN (SELECT * FROM unnest(context_org_list))
- UNION
- SELECT ou.id,
- ou.parent_ou
- FROM actor.org_unit ou
- JOIN descendant_depth ot ON (ot.id = ou.parent_ou)
- ) SELECT INTO context_org_list ARRAY_AGG(ou.id) FROM actor.org_unit ou JOIN descendant_depth USING (id);
- END IF;
- SELECT INTO items_out COUNT(DISTINCT circ.id)
- FROM action.circulation circ
- JOIN asset.copy copy ON (copy.id = circ.target_copy)
- LEFT JOIN action.circulation_limit_group_map aclgm ON (circ.id = aclgm.circ)
- WHERE circ.usr = match_user
- AND circ.circ_lib IN (SELECT * FROM unnest(context_org_list))
- AND circ.checkin_time IS NULL
- AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL)
- AND (copy.circ_modifier IN (SELECT circ_mod FROM config.circ_limit_set_circ_mod_map WHERE limit_set = circ_limit_set.id)
- OR copy.location IN (SELECT copy_loc FROM config.circ_limit_set_copy_loc_map WHERE limit_set = circ_limit_set.id)
- OR aclgm.limit_group IN (SELECT limit_group FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id)
- );
- IF items_out >= circ_limit_set.items_out THEN
- result.fail_part := 'config.circ_matrix_circ_mod_test';
- result.success := FALSE;
- done := TRUE;
- RETURN NEXT result;
- END IF;
- END IF;
- SELECT INTO result.limit_groups result.limit_groups || ARRAY_AGG(limit_group) FROM config.circ_limit_set_group_map WHERE limit_set = circ_limit_set.id AND NOT check_only;
- END LOOP;
-
- -- If we passed everything, return the successful matchpoint
- IF NOT done THEN
- RETURN NEXT result;
- END IF;
-
- RETURN;
-END;
-$func$ LANGUAGE plpgsql;
-
-COMMIT;
-
--- /dev/null
+== Optionally allow patrons to renew after hitting fine maximum ==
+
+When a patron hits the max fine limit, a standing penalty is applied to their account. By default, that penalty (PATRON_EXCEEDS_FINES)
+is configured to block renewals.
+
+This release adds a new org unit setting, _circ.permit_renew_when_exceeds_fines_. If enabled for a particular org unit, renewals are
+permitted (as long as all other circulation eligibility criteria are met).
+