my $needed_for_something = 0; # formerly "needed_for_hold"
+ my $sf_use = $self->editor->json_query({ from => ['smart_float.in_use'] });
+ my $smart_float_in_use = $U->is_true($sf_use->[0]->{'smart_float.in_use'}) if $sf_use;
+ my $sf_libs = $self->editor->json_query({ from => ['smart_float.libs', $self->circ_lib] });
+ my $is_smart_float_lib = $U->is_true($sf_libs->[0]->{'smart_float.libs'}) if $sf_libs;
+
if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit
if (!$self->remote_hold) {
}
}
}
-
- if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
+
+ if ($smart_float_in_use) { # Begin Smart Float
+ if( $suppress_transit or ( ($circ_lib == $self->circ_lib and !$is_smart_float_lib) and not ($self->hold_as_transit and $self->remote_hold) ) ) {
# copy is where it needs to be, either for hold or reshelving
-
$self->checkin_handle_precat();
return if $self->bail_out;
-
} else {
# copy needs to transit "home", or stick here if it's a floating copy
my $can_float = 0;
$can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res;
}
if ($can_float) { # Yep, floating, stick here
+ my $sf_dest = $self->editor->json_query({ from => ['smart_float.destination', $self->circ_lib, $self->copy->id] })->[0];
+ $self->copy->circ_lib( [values %$sf_dest]->[0] );
+ $self->update_copy;
+ $logger->error("SMARTFLOAT: floating checkin lib: ".$self->circ_lib." copy id: ".$self->copy->id." new lib ".[values %$sf_dest]->[0]);
+ if ($self->copy->circ_lib != $self->circ_lib) { # if transit needed
+ $self->checkin_build_copy_transit($self->copy->circ_lib);
+ return if $self->bail_out;
+ $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $self->copy->circ_lib));
+ }
+ } else {
+ $logger->error("SMARTFLOAT: manual float is ".$self->manual_float." copy ".$self->copy->id." circlib is ".$self->copy->circ_lib." float is ".$self->copy->floating->name." checkin lib ".$self->circ_lib);
+ if ($self->copy->circ_lib != $self->circ_lib) { # if transit needed
+ $self->checkin_build_copy_transit($self->copy->circ_lib);
+ return if $self->bail_out;
+ $self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
+ }
+ }
+ } # end Smart Float
+ } else {
+ # smart float not in use
+ if( $suppress_transit or ( $circ_lib == $self->circ_lib and not ($self->hold_as_transit and $self->remote_hold) ) ) {
+ # copy is where it needs to be, either for hold or reshelving
+ $self->checkin_handle_precat();
+ return if $self->bail_out;
+ } else {
+ # copy needs to transit "home", or stick here if it's a floating copy
+ my $can_float = 0;
+ if ($self->copy->floating && ($self->manual_float || !$U->is_true($self->copy->floating->manual)) && !$self->remote_hold) { # copy is potentially floating?
+ my $res = $self->editor->json_query(
+ { from => [
+ 'evergreen.can_float',
+ $self->copy->floating->id,
+ $self->copy->circ_lib,
+ $self->circ_lib
+ ]
+ }
+ );
+ $can_float = $U->is_true($res->[0]->{'evergreen.can_float'}) if $res;
+ }
+ if ($can_float) { # Yep, floating, stick here
$self->checkin_changed(1);
$self->copy->circ_lib( $self->circ_lib );
$self->update_copy;
$self->push_events(OpenILS::Event->new('ROUTE_ITEM', org => $circ_lib));
}
}
- }
+ } # end smart float not in use
+ } # end unless needed for something
} else { # no-op checkin
- if ($U->is_true( $self->copy->floating )) { # XXX floating items still stick where they are even with no-op checkin?
+ if ($smart_float_in_use) {
+ if ($U->is_true( $self->copy->floating ) and $is_smart_float_lib) { # XXX floating items still stick where they are even with no-op checkin?
$self->checkin_changed(1);
$self->copy->circ_lib( $self->circ_lib );
$self->update_copy;
}
+ } else {
+ # smart float not in use
+ }
}
if($self->claims_never_checked_out and
--- /dev/null
+Create schema smart_float;
+
+-- !!! config.smart_float holds Smart Float configuration parameters !!!
+DROP TABLE IF EXISTS config.smart_float;
+CREATE TABLE config.smart_float (
+id SERIAL PRIMARY KEY,
+name TEXT,
+active BOOL NOT NULL DEFAULT FALSE,
+dups_threshold INT NOT NULL DEFAULT 0,
+org_unit INT NOT NULL DEFAULT 9,
+shelf_location INT NOT NULL,
+shelf_is_group BOOL NOT NULL DEFAULT FALSE,
+items_allowed INT,
+homing_threshold INT NOT NULL DEFAULT 0,
+homing_lifespan TEXT DEFAULT '1 day',
+CONSTRAINT org_unit_fkey FOREIGN KEY (org_unit) REFERENCES actor.org_unit (id) DEFERRABLE INITIALLY DEFERRED,
+CONSTRAINT shelf_location_fkey FOREIGN KEY (shelf_location) REFERENCES asset.copy_location (id) DEFERRABLE INITIALLY DEFERRED,
+CONSTRAINT org_plus_loc UNIQUE(org_unit,shelf_location)
+);
+
+-- !!! smart_float.dups !!!
+CREATE OR REPLACE FUNCTION smart_float.dups(bcode text)
+ RETURNS TABLE(clib integer, numdups bigint)
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ RETURN QUERY with sflibs as (
+ select org_unit from config.smart_float where active is true group by 1
+),
+duplibs as (
+select circ_lib,count(circ_lib) from asset.copy where barcode != bcode and status in (0,7) and call_number in (select id from asset.call_number where record in (select record from asset.call_number join asset.copy on call_number.id=copy.call_number and copy.barcode=bcode)) group by 1
+)
+select sflibs.org_unit,coalesce(duplibs.count,0) as count from sflibs left join duplibs on sflibs.org_unit=duplibs.circ_lib;
+END;
+$function$;
+
+-- !!! smart_float.opens_and_dups !!!
+CREATE OR REPLACE FUNCTION smart_float.opens_and_dups(bcode text, cloc integer)
+ RETURNS TABLE(clib integer, taken bigint, allowed integer, open bigint, dups bigint, dups_threshold integer)
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ RETURN QUERY select copy.circ_lib,count(copy.id),sf.items_allowed,(sf.items_allowed-count(copy.id)) as spaces,(select numdups from smart_float.dups(bcode) where smart_float_dups.clib=copy.circ_lib) as dups, sf.dups_threshold from asset.copy join config.smart_float sf on (copy.circ_lib=sf.org_unit and copy.location=sf.shelf_location and sf.active is true) where copy.location=cloc and copy.status in (0,7) group by circ_lib,sf.items_allowed,sf.dups_threshold;
+END;
+$function$;
+
+
+DROP TABLE IF EXISTS smart_float.metrics;
+CREATE TABLE smart_float.metrics (
+id SERIAL PRIMARY KEY,
+sf_time TIMESTAMP DEFAULT now(),
+copy_id BIGINT,
+prev_lib INT,
+owning_lib INT,
+chkin_lib INT,
+open_space INT,
+duplicates INT,
+new_lib INT,
+op_code TEXT,
+sla_name TEXT DEFAULT NULL,
+sla_dups_threshold INT DEFAULT NULL,
+sla_org_unit INT DEFAULT NULL,
+sla_shelf_location INT DEFAULT NULL,
+sla_items_allowed INT DEFAULT NULL,
+sla_homing_threshold INT DEFAULT NULL,
+sla_homing_lifespan TEXT DEFAULT NULL,
+homing_circs_needed INT DEFAULT NULL,
+homing_days_to_go INT DEFAULT NULL
+);
+
+DROP TABLE IF EXISTS smart_float.cap_store;
+CREATE TABLE smart_float.cap_store (
+id SERIAL PRIMARY KEY,
+branch text,
+taken integer,
+allowed integer,
+open integer,
+percent_capacity numeric,
+name text,
+shelf_loc integer,
+cap_date date
+);
+
+
+CREATE OR REPLACE FUNCTION smart_float.save_metric(cpid bigint,pvlib int,onlib int,cklib int,opens int,dups int,nwlib int,opcd text,shelf_loc_atts config.smart_float, hcn int, hdtg int)
+ RETURNS void
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ IF shelf_loc_atts IS NULL THEN
+ INSERT INTO smart_float.metrics (copy_id,prev_lib,owning_lib,chkin_lib,open_space,duplicates,new_lib,op_code)
+ VALUES(cpid,pvlib,onlib,cklib,opens,dups,nwlib,opcd);
+ ELSE
+ INSERT INTO smart_float.metrics
+ VALUES(default,default,cpid,pvlib,onlib,cklib,opens,dups,nwlib,opcd,$9.name,$9.dups_threshold,$9.org_unit,$9.shelf_location,$9.items_allowed,$9.homing_threshold,$9.homing_lifespan,hcn,hdtg);
+ END IF;
+
+END;
+$function$;
+
+
+
+-- !!! smart_float.destination !!!
+CREATE OR REPLACE FUNCTION smart_float.destination(chkin_org integer, copy_id bigint)
+ RETURNS integer
+ LANGUAGE plpgsql
+AS $function$
+
+DECLARE
+ copy_rec asset.copy%ROWTYPE;
+ shelf_loc_atts config.smart_float%ROWTYPE;
+ origin_loc_atts config.smart_float%ROWTYPE;
+ origin_owner INT;
+ send_lib INT;
+ homing_threshold_met BOOL;
+ circs_needed INT;
+ days_to_go INT;
+ open_space INT;
+ duplicates INT;
+ opcode TEXT;
+
+BEGIN
+ SELECT INTO copy_rec * FROM asset.copy WHERE id = copy_id;
+ SELECT INTO origin_owner owning_lib from asset.call_number join asset.copy on call_number.id=copy.call_number where copy.id=copy_id;
+ SELECT INTO shelf_loc_atts * FROM config.smart_float WHERE org_unit = chkin_org and shelf_location = copy_rec.location and active is true;
+ IF NOT FOUND THEN
+ -- no smart_float config for this org and shelf, so dumb float it here
+ PERFORM smart_float.save_metric(copy_rec.id,copy_rec.circ_lib,origin_owner,chkin_org,NULL,NULL,chkin_org,'not_active',NULL,NULL,NULL);
+ RETURN chkin_org;
+ END IF;
+
+ SELECT INTO origin_loc_atts * FROM config.smart_float WHERE org_unit = origin_owner and shelf_location = copy_rec.location;
+ SELECT INTO homing_threshold_met,circs_needed,days_to_go * FROM smart_float.homing_threshold_met(copy_id,origin_owner,origin_loc_atts.homing_threshold,origin_loc_atts.homing_lifespan,copy_rec.active_date);
+
+ IF homing_threshold_met IS FALSE THEN
+ -- send home, homing threshold not met
+ PERFORM smart_float.save_metric(copy_rec.id,copy_rec.circ_lib,origin_owner,chkin_org,NULL,NULL,NULL,'homed',origin_loc_atts.*,circs_needed,days_to_go);
+ RETURN origin_owner;
+ END IF;
+
+ -- can item float to checkin lib? aka the shelving location is not full and has less than dups_threshold duplicate titles?
+ opcode := 'floated';
+ SELECT INTO send_lib,open_space,duplicates o.clib,o.open,d.numdups from smart_float.openings(copy_rec.location) as o join smart_float.dups(copy_rec.barcode) as d on d.clib=o.clib where o.clib=chkin_org and o.open>0 and (d.numdups<o.dups_threshold or d.numdups=0);
+ IF NOT FOUND THEN
+ -- can item bounce to another lib?
+ opcode := 'bounced';
+ SELECT INTO send_lib,open_space,duplicates o.clib,o.open,d.numdups from smart_float.openings(copy_rec.location) as o join smart_float.dups(copy_rec.barcode) as d on d.clib=o.clib where o.open>0 and (d.numdups<o.dups_threshold or d.numdups=0) AND evergreen.can_float(copy_rec.floating,copy_rec.circ_lib,o.clib) is TRUE order by o.open desc limit 1;
+ IF NOT FOUND THEN
+ -- can item float here or bounce elsewhere regardless of dups?
+ opcode := 'open_only_bounced';
+ SELECT INTO send_lib,open_space clib,open from smart_float.openings(copy_rec.location) where open>0 AND evergreen.can_float(copy_rec.floating,copy_rec.circ_lib,clib) is TRUE order by open desc limit 1;
+ IF NOT FOUND THEN
+ -- otherwise no sense in transiting item
+ -- floating to checkin lib because it can't go anywhere else
+ opcode := 'nogo_floated';
+ send_lib := chkin_org;
+ END IF;
+ END IF;
+ END IF;
+ PERFORM smart_float.save_metric(copy_rec.id,copy_rec.circ_lib,origin_owner,chkin_org,open_space,duplicates,send_lib,opcode,shelf_loc_atts.*,NULL,NULL);
+ RETURN send_lib;
+END;
+$function$;
+
+-- !!! smart_float.homing_threshold_met !!!
+CREATE OR REPLACE FUNCTION smart_float.homing_threshold_met(cpid bigint, owning integer, threshold integer, lifespan text, active timestamp with time zone)
+ RETURNS TABLE(homing_met BOOL, circs_needed INT, days_to_go INT)
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ RETURN QUERY SELECT (((select count(*) from action.all_circulation where target_copy=cpid and circ_lib=owning and phone_renewal='f' and desk_renewal='f' and opac_renewal='f') >= threshold) AND (select active + lifespan::interval < now())),
+(threshold - (select count(*) from action.all_circulation where target_copy=cpid and circ_lib=owning and phone_renewal='f' and desk_renewal='f' and opac_renewal='f'))::INT,
+(select max(dtg)
+from (
+ select date_part('Days',(select (active + lifespan::INTERVAL) - now()))::INT as dtg
+ union select 0
+) d);
+END;
+$function$;
+
+-- !!! smart_float.openings !!!
+CREATE OR REPLACE FUNCTION smart_float.openings(cloc integer)
+ RETURNS TABLE(clib integer, taken bigint, allowed integer, open bigint, dups_threshold integer)
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ RETURN QUERY with tcs as ( Select library,arriving from smart_float.shelving_location_non_hold_transit_vectors(cloc)
+)
+select copy.circ_lib,count(copy.id),sf.items_allowed,(sf.items_allowed-count(copy.id))-tcs.arriving as spaces, sf.dups_threshold from asset.copy join config.smart_float sf on (copy.circ_lib=sf.org_unit and copy.location=sf.shelf_location) join tcs on tcs.library=copy.circ_lib where copy.location=cloc and copy.status in (0,7) group by circ_lib,sf.items_allowed,sf.dups_threshold,tcs.arriving;
+END;
+$function$;
+
+-- !!! smart_float.capacities !!!
+CREATE OR REPLACE FUNCTION smart_float.capacities()
+ RETURNS TABLE(branch text, taken integer, allowed integer, spaces integer, percent_capacity numeric, name text, shelf_loc integer)
+ LANGUAGE plpgsql
+AS $function$
+DECLARE
+ ts INT;
+BEGIN
+ FOR ts IN select distinct(shelf_location) from config.smart_float order by 1 LOOP
+ RETURN QUERY
+ select aou.shortname,count(copy.id)::INT,sf.items_allowed,(sf.items_allowed-count(copy.id))::INT as spaces, round(count(copy.id)/sf.items_allowed::numeric*100.0,1), sf.name, sf.shelf_location from asset.copy join config.smart_float sf on (copy.circ_lib=sf.org_unit and copy.location=sf.shelf_location and sf.items_allowed != 0) join actor.org_unit aou on copy.circ_lib=aou.id where copy.location=ts and copy.status in (0,7) group by shortname,sf.items_allowed,sf.name,sf.shelf_location order by sf.name,aou.shortname;
+ END LOOP;
+END;
+$function$;
+
+
+-- !!! smart_float.shelving_location_non_hold_transit_vectors !!!
+CREATE OR REPLACE FUNCTION smart_float.shelving_location_non_hold_transit_vectors(sloc integer)
+ RETURNS TABLE(library integer, departing bigint, arriving bigint, transit_vector bigint)
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+RETURN QUERY with transits as (
+ select atc.id,source_send_time, dest_recv_time,target_copy,source,dest from action.transit_copy atc join asset.copy cp on atc.target_copy=cp.id and cp.location=sloc where dest_recv_time is null EXCEPT select id,source_send_time, dest_recv_time,target_copy,source,dest from action.hold_transit_copy where dest_recv_time is null
+),
+tfrom as (
+ select source from_lib,count(source) from_count from transits group by 1 order by 1
+),
+tto as (
+ select dest to_lib,count(dest) to_count from transits group by 1 order by 1
+),
+orgs as (
+ select id from actor.org_unit where id between 10 and 17
+)
+select orgs.id as lib,coalesce(from_count,0),coalesce(to_count,0),coalesce(to_count,0)-coalesce(from_count,0) as transit_vector from orgs left join tto on orgs.id=to_lib left join tfrom on orgs.id=from_lib;
+END;
+$function$;
+
+CREATE OR REPLACE FUNCTION smart_float.libs(chkin_lib INT)
+ RETURNS BOOLEAN
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ RETURN chkin_lib in (select org_unit from config.smart_float where active is true group by 1);
+END;
+
+CREATE OR REPLACE FUNCTION smart_float.in_use()
+ RETURNS boolean
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ RETURN TRUE IN (SELECT active FROM config.smart_float);
+END;
+$function$;
+
+-- !!! need some new indexes
+create index concurrently cp_location_idx on asset.copy (location);
+create index concurrently cp_smart_float_dup_counts_idx on asset.copy (barcode);