From 6b53189a9134d7366d4fd9bb7f0a8b29ee304df5 Mon Sep 17 00:00:00 2001 From: Lebbeous Fogle-Weekley Date: Thu, 13 Dec 2012 14:45:41 -0500 Subject: [PATCH] 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 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 --- Open-ILS/examples/fm_IDL.xml | 26 ++ .../lib/OpenILS/Application/Circ/Holds.pm | 8 +- .../Application/Storage/Publisher/action.pm | 202 +++++++++++- Open-ILS/src/sql/Pg/002.schema.config.sql | 27 +- Open-ILS/src/sql/Pg/005.schema.actors.sql | 2 +- Open-ILS/src/sql/Pg/090.schema.action.sql | 21 +- Open-ILS/src/sql/Pg/800.fkeys.sql | 1 + Open-ILS/src/sql/Pg/950.data.seed-values.sql | 107 +++++- .../upgrade/XXXX.schema.org_prox_adjust.sql | 21 +- ...XXYY.schema.custom-best-hold-selection.sql | 148 +++++++++ .../conify/global/config/best_hold_order.tt2 | 119 +++++++ .../js/dojo/openils/conify/BestHoldOrder.js | 307 ++++++++++++++++++ .../web/js/dojo/openils/conify/nls/conify.js | 7 +- .../js/dojo/openils/widget/AutoFieldWidget.js | 1 + Open-ILS/web/opac/locale/en-US/lang.dtd | 1 + .../staff_client/chrome/content/main/menu.js | 4 + .../chrome/content/main/menu_frame_menus.xul | 4 + .../custom-best-hold-selection.txt | 16 + .../Circ/custom-best-hold-selection.txt | 247 ++++++++++++++ 19 files changed, 1240 insertions(+), 29 deletions(-) create mode 100644 Open-ILS/src/sql/Pg/upgrade/XXYY.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/web/js/dojo/openils/conify/BestHoldOrder.js create mode 100644 docs/RELEASE_NOTES_NEXT/custom-best-hold-selection.txt create mode 100644 docs/TechRef/Circ/custom-best-hold-selection.txt diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 771ab0b2a3..f6565735cb 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 + + + + + + + + + + + + + + + + + + + + + + + + + + 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 40cf63e5a6..03f6c982af 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 60fe6b784c..05dedcf629 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 10ad215185..4f84162f23 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 176f4654d1..e6d8cd6102 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 3f7bb6f6de..fd82cf0281 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 36e9438482..fb7b07d9c7 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 37e0d3245c..51d608fbd8 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 d9eb082a5b..43876d80cd 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 0000000000..55e390682f --- /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 0000000000..14d8bd76f3 --- /dev/null +++ b/Open-ILS/src/templates/conify/global/config/best_hold_order.tt2 @@ -0,0 +1,119 @@ +[% WRAPPER base.tt2 %] + +

[% l('Best-Hold Selection Sort Order') %]

+ + +
+ [% l('Interface loading') %] + +
+ + + + + + +[% 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 0000000000..52d2862e20 --- /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