</actions>
</permacrud>
</class>
+ <class id="aecc"
+ controller="open-ils.cstore open-ils.pcrud"
+ oils_obj:fieldmapper="action::emergency_closing_circulation"
+ oils_persist:tablename="action.emergency_closing_circulation"
+ reporter:label="Emergency Closing Circulation Entry"
+ oils_persist:readonly="true"
+ > <!-- This is not a view, but is managed via functions, so it's readonly. -->
+ <fields oils_persist:primary="id">
+ <field name="id" reporter:datatype="id" />
+ <field name="circulation" reporter:datatype="link" />
+ <field name="emergency_closing" reporter:datatype="link"/>
+ <field name="original_due_date" reporter:datatype="timestamp" />
+ <field name="process_time" reporter:datatype="timestamp" />
+ </fields>
+ <links>
+ <link field="circulation" reltype="has_a" key="id" map="" class="circ"/>
+ <link field="emergency_closing" reltype="has_a" key="id" map="" class="aec"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve/>
+ </actions>
+ </permacrud>
+ </class>
+ <class id="aecr"
+ controller="open-ils.cstore open-ils.pcrud"
+ oils_obj:fieldmapper="action::emergency_closing_reservation"
+ oils_persist:tablename="action.emergency_closing_reservation"
+ reporter:label="Emergency Closing Reservation Entry"
+ oils_persist:readonly="true"
+ > <!-- This is not a view, but is managed via functions, so it's readonly. -->
+ <fields oils_persist:primary="id">
+ <field name="id" reporter:datatype="id" />
+ <field name="reservation" reporter:datatype="link" />
+ <field name="emergency_closing" reporter:datatype="link"/>
+ <field name="original_end_time" reporter:datatype="timestamp" />
+ <field name="process_time" reporter:datatype="timestamp" />
+ </fields>
+ <links>
+ <link field="reservation" reltype="has_a" key="id" map="" class="bresv"/>
+ <link field="emergency_closing" reltype="has_a" key="id" map="" class="aec"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve/>
+ </actions>
+ </permacrud>
+ </class>
+ <class id="aech"
+ controller="open-ils.cstore open-ils.pcrud"
+ oils_obj:fieldmapper="action::emergency_closing_hold"
+ oils_persist:tablename="action.emergency_closing_hold"
+ reporter:label="Emergency Closing Hold Entry"
+ oils_persist:readonly="true"
+ > <!-- This is not a view, but is managed via functions, so it's readonly. -->
+ <fields oils_persist:primary="id">
+ <field name="id" reporter:datatype="id" />
+ <field name="hold" reporter:datatype="link" />
+ <field name="emergency_closing" reporter:datatype="link"/>
+ <field name="original_shelf_expire_time" reporter:datatype="timestamp" />
+ <field name="process_time" reporter:datatype="timestamp" />
+ </fields>
+ <links>
+ <link field="hold" reltype="has_a" key="id" map="" class="ahr"/>
+ <link field="emergency_closing" reltype="has_a" key="id" map="" class="aec"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve/>
+ </actions>
+ </permacrud>
+ </class>
+ <class id="aecs"
+ controller="open-ils.cstore open-ils.pcrud"
+ oils_obj:fieldmapper="action::emergency_closing_status"
+ oils_persist:tablename="action.emergency_closing_status"
+ reporter:label="Emergency Closing Status"
+ oils_persist:readonly="true"
+ >
+ <fields oils_persist:primary="id">
+ <field name="id" reporter:datatype="id" />
+ <field name="creator" reporter:datatype="link" />
+ <field name="create_time" reporter:datatype="timestamp" />
+ <field name="process_start_time" reporter:datatype="timestamp" />
+ <field name="process_end_time" reporter:datatype="timestamp" />
+ <field name="last_update_time" reporter:datatype="timestamp" />
+ <field name="circulations" reporter:datatype="int" />
+ <field name="circulations_complete" reporter:datatype="int" />
+ <field name="reservations" reporter:datatype="int" />
+ <field name="reservations_complete" reporter:datatype="int" />
+ <field name="holds" reporter:datatype="int" />
+ <field name="holds_complete" reporter:datatype="int" />
+ </fields>
+ <links>
+ <link field="id" reltype="has_a" key="id" map="" class="aec"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <retrieve/>
+ </actions>
+ </permacrud>
+ </class>
+ <class id="aec" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="action::emergency_closing" oils_persist:tablename="action.emergency_closing" reporter:label="Emergency Closing">
+ <fields oils_persist:primary="id" oils_persist:sequence="action.emergency_closing_id_seq">
+ <field name="id" reporter:datatype="id" />
+ <field name="creator" reporter:datatype="link" />
+ <field name="create_time" reporter:datatype="timestamp" />
+ <field name="process_start_time" reporter:datatype="timestamp" />
+ <field name="process_end_time" reporter:datatype="timestamp" />
+ <field name="last_update_time" reporter:datatype="timestamp" />
+ <field name="closing" oils_persist:virtual="true" reporter:datatype="link"/>
+ <field name="status" oils_persist:virtual="true" reporter:datatype="link"/>
+ <field name="circulations" oils_persist:virtual="true" reporter:datatype="link"/>
+ <field name="reservations" oils_persist:virtual="true" reporter:datatype="link"/>
+ <field name="holds" oils_persist:virtual="true" reporter:datatype="link"/>
+ </fields>
+ <links>
+ <link field="creator" reltype="has_a" key="id" map="" class="au"/>
+ <link field="closing" reltype="might_have" key="emergency_closing" map="" class="aoucd"/>
+ <link field="status" reltype="might_have" key="id" map="" class="aecs"/>
+ <link field="circulations" reltype="has_many" key="emergency_closing" map="" class="aecc"/>
+ <link field="reservations" reltype="has_many" key="emergency_closing" map="" class="aecr"/>
+ <link field="holds" reltype="has_many" key="emergency_closing" map="" class="aech"/>
+ </links>
+ <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+ <actions>
+ <create permission="EMERGENCY_CLOSING">
+ <context link="closing" field="org_unit" />
+ </create>
+ <retrieve/>
+ <update permission="EMERGENCY_CLOSING">
+ <context link="closing" field="org_unit" />
+ </update>
+ <delete permission="EMERGENCY_CLOSING">
+ <context link="closing" field="org_unit" />
+ </delete>
+ </actions>
+ </permacrud>
+ </class>
<class id="aoucd" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::org_unit::closed_date" oils_persist:tablename="actor.org_unit_closed" reporter:label="Closed Dates">
<fields oils_persist:primary="id" oils_persist:sequence="actor.org_unit_closed_id_seq">
<field name="close_end" reporter:datatype="timestamp" />
<field name="reason" reporter:datatype="text"/>
<field name="full_day" reporter:datatype="bool"/>
<field name="multi_day" reporter:datatype="bool"/>
+ <field name="emergency_closing" reporter:datatype="link"/>
</fields>
<links>
<link field="org_unit" reltype="has_a" key="id" map="" class="aou"/>
+ <link field="emergency_closing" reltype="has_a" key="id" map="" class="aec"/>
</links>
<permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
<actions>
use base 'OpenILS::Application';
use strict; use warnings;
use OpenSRF::EX qw(:try);
+use OpenSRF::Utils qw(:datetime);
+use DateTime;
+use DateTime::Format::ISO8601;
use OpenILS::Utils::CStoreEditor q/:funcs/;
+use OpenILS::Application::AppUtils;
+my $U = "OpenILS::Application::AppUtils";
sub initialize { return 1; }
+sub process_emergency {
+ my( $self, $conn, $auth, $date ) = @_;
+
+ my $e = new_editor(authtoken=>$auth);
+ return $e->event unless $e->checkauth;
+
+ return $e->die_event unless $e->allowed(
+ 'EMERGENCY_CLOSING', $date->org_unit);
+
+ my $id = ref($date->emergency_closing) ? $date->emergency_closing->id : $date->emergency_closing;
+
+ # Stage 1
+ $e->xact_begin;
+ my $rows = $e->json_query({
+ from => ['action.emergency_closing_stage_1', $id]
+ });
+ $e->xact_commit;
+ return unless ($rows && @$rows);
+
+ $conn->respond({stage => 'start', stats => $$rows[0]});
+
+ my $ses = OpenSRF::AppSession->create('open-ils.trigger');
+
+ # Stage 2 - circs
+ my $circs = $e->search_action_emergency_closing_circulation(
+ { emergency_closing => $id }
+ );
+ my $circ_total = scalar(@$circs);
+
+ my $mod = 1;
+ $mod = int($circ_total / 10) if ($circ_total >= 100);
+ $mod = int($circ_total / 100) if ($circ_total >= 1000);
+
+ my $count = 0;
+ for my $circ (@$circs) {
+ $e->xact_begin;
+ my $rows = $e->json_query({ from => ['action.emergency_closing_stage_2_circ', $circ->id] });
+ $e->xact_commit;
+ $count++;
+ $ses->request('open-ils.trigger.event.autocreate', 'checkout.due.emergency_closing', $circ, $e->requestor->ws_ou)
+ if (ref($rows) && @$rows && $U->is_true($$rows[0]{'action.emergency_closing_stage_2_circ'}));
+ $conn->respond({stage => 'circulations', circulations => [$count,$circ_total]})
+ if ($mod == 1 or !($circ_total % $mod));
+ }
+
+ # Stage 3 - holds
+ my $holds = $e->search_action_emergency_closing_hold(
+ { emergency_closing => $id }
+ );
+ my $hold_total = scalar(@$holds);
+
+ $mod = 1;
+ $mod = int($hold_total / 10) if ($hold_total >= 100);
+ $mod = int($hold_total / 100) if ($hold_total >= 1000);
+
+ $count = 0;
+ for my $hold (@$holds) {
+ $e->xact_begin;
+ my $rows = $e->json_query({ from => ['action.emergency_closing_stage_2_hold', $hold->id] });
+ $e->xact_commit;
+ $count++;
+ $ses->request('open-ils.trigger.event.autocreate', 'hold.shelf_expire.emergency_closing', $hold, $e->requestor->ws_ou)
+ if (ref($rows) && @$rows && $U->is_true($$rows[0]{'action.emergency_closing_stage_2_hold'}));
+ $conn->respond({stage => 'holds', holds => [$count,$hold_total]})
+ if ($mod == 1 or !($hold_total % $mod));
+ }
+
+ # Stage 2 - reservations
+ my $ress = $e->search_action_emergency_closing_reservation(
+ { emergency_closing => $id }
+ );
+ my $res_total = scalar(@$ress);
+
+ $mod = 1;
+ $mod = int($res_total / 10) if ($res_total >= 100);
+ $mod = int($res_total / 100) if ($res_total >= 1000);
+
+ $count = 0;
+ for my $res (@$ress) {
+ $e->xact_begin;
+ my $rows = $e->json_query({ from => ['action.emergency_closing_stage_2_reservation', $res->id] });
+ $e->xact_commit;
+ $count++;
+ $ses->request('open-ils.trigger.event.autocreate', 'booking.due.emergency_closing', $res, $e->requestor->ws_ou)
+ if (ref($rows) && @$rows && $U->is_true($$rows[0]{'action.emergency_closing_stage_2_reservation'}));
+ $conn->respond({stage => 'ress', ress => [$count,$res_total]})
+ if ($mod == 1 or !($res_total % $mod));
+ }
+
+ # Stage 3
+ my $eclosing = $e->retrieve_action_emergency_closing($id);
+ $eclosing->process_end_time('now');
+ $e->xact_begin;
+ $e->update_action_emergency_closing($eclosing);
+ $e->xact_commit;
+
+ return {stage => 'complete', complete => 1};
+}
+__PACKAGE__->register_method(
+ method => 'process_emergency',
+ api_name => 'open-ils.actor.org_unit.closed.process_emergency',
+ stream => 1,
+ max_bundle_count => 1,
+ signature => q/Processes an emergency closing/
+);
+
__PACKAGE__->register_method(
method => 'fetch_dates',
api_name => 'open-ils.actor.org_unit.closed.retrieve.all',
my $end = $$args{end_date} || '3000-01-01'; # Y3K, here I come..
my $dates = $e->search_actor_org_unit_closed_date(
- {
- close_start => { ">=" => $start },
- close_end => { "<=" => $end },
- org_unit => $org,
- }, { idlist => $$args{idlist} } ) or return $e->event;
+ [{
+ '-or' => [
+ { close_start => { ">=" => $start }, close_end => { "<=" => $end } },
+ { emergency => { "!=" => undef }, "+aec" => { process_end_time => { "=" => undef } } }
+ ],
+ org_unit => $org,
+ }, {flesh => 2,
+ flesh_fields => { aoucd => ['emergency_closing'], aec => ['status'] },
+ join => { "aec" => { type => "left" } },
+ limit => $$args{limit},
+ offset => $$args{offset}
+ }], { idlist => $$args{idlist} } ) or return $e->event;
if(!$$args{idlist} and @$dates) {
$dates = [ sort { $a->close_start cmp $b->close_start } @$dates ];
my $e = new_editor(authtoken=>$auth);
return $e->event unless $e->checkauth;
my $date = $e->retrieve_actor_org_unit_closed_date($id) or return $e->event;
+ $date->emergency_closing(
+ $e->retrieve_action_emergency_closing($date->emergency_closing)
+ ) if $date->emergency_closing;
return $date;
}
my $e = new_editor(authtoken=>$auth, xact => 1);
return $e->die_event unless $e->checkauth;
my $date = $e->retrieve_actor_org_unit_closed_date($id) or return $e->die_event;
+ if ($date->emergency_closing) {
+ return $e->die_event unless $e->allowed(
+ 'EMERGENCY_CLOSING', $date->org_unit);
+ }
return $e->die_event unless $e->allowed(
'actor.org_unit.closed_date.delete', $date->org_unit);
$e->delete_actor_org_unit_closed_date($date) or return $e->die_event;
);
sub create_date {
- my( $self, $conn, $auth, $date ) = @_;
+ my( $self, $conn, $auth, $date, $emergency ) = @_;
my $e = new_editor(authtoken=>$auth, xact =>1);
return $e->die_event unless $e->checkauth;
return $e->die_event unless $e->allowed(
'actor.org_unit.closed_date.create', $date->org_unit);
+ if ($emergency) {
+ return $e->die_event
+ unless $e->allowed('EMERGENCY_CLOSING', $date->org_unit);
+ $e->create_action_emergency_closing($emergency)
+ or return $e->die_event;
+ $date->emergency_closing($emergency->id);
+ }
+
$e->create_actor_org_unit_closed_date($date) or return $e->die_event;
my $newobj = $e->retrieve_actor_org_unit_closed_date($date->id)
or return $e->die_event;
+ $newobj->emergency_closing(
+ $e->retrieve_action_emergency_closing($newobj->emergency_closing)
+ ) if $emergency;
+
$e->commit;
return $newobj;
}
my $odate = $e->retrieve_actor_org_unit_closed_date($date->id)
or return $e->die_event;
+ if ($odate->emergency_closing) {
+ return $e->die_event unless $e->allowed(
+ 'EMERGENCY_CLOSING', $odate->org_unit);
+ }
+
return $e->die_event unless $e->allowed(
'actor.org_unit.closed_date.update', $odate->org_unit);
$e->update_actor_org_unit_closed_date($date) or return $e->die_event;
+
+ my $newobj = $e->retrieve_actor_org_unit_closed_date($date->id)
+ or return $e->die_event;
+
+ $newobj->emergency_closing(
+ $e->retrieve_action_emergency_closing($newobj->emergency_closing)
+ ) if $odate->emergency_closing;
+
$e->commit;
- return 1;
+ return $newobj;
+}
+
+
+__PACKAGE__->register_method(
+ method => 'is_probably_emergency_closing',
+ api_name => 'open-ils.actor.org_unit.closed_date.emergency_test',
+ signature => q/
+ Returns a truthy value if the closing start date is either in
+ the past or is nearer in the future than the longest configured
+ circulation duration.
+ @param auth An auth token
+ @param date A closed date object
+ /
+);
+sub is_probably_emergency_closing {
+ my( $self, $conn, $auth, $date ) = @_;
+ my $e = new_editor(authtoken=>$auth);
+ return $e->event unless $e->checkauth;
+
+ # First, when is it?
+ my $start_seconds = DateTime::Format::ISO8601->parse_datetime(
+ cleanse_ISO8601($date->close_start)
+ )->epoch;
+
+ # Is it in the past?
+ return 1 if ($start_seconds < time); # It is!
+
+ # No? Let's see if it's coming up sooner than
+ # the currently-furthest normal due date...
+ my $rules = $e->search_config_rules_circ_duration({
+ extended => {
+ '>' => {
+ transform => 'interval_pl_timestamptz',
+ params => ['now'],
+ value => $date->close_start
+ }
+ }
+ }); # That is basically: WHERE 'now'::timestamptz + extended > $date->close_start
+ # which translates to "the closed start happens earlier than the theoretically
+ # latest due date we could currently have, so it might need emergency
+ # treatment.
+
+ return scalar(@$rules); # No rows means "not emergency".
}
start is the first day the org is open going backwards from
'date'. end is the next day the org is open going
forward from 'date'.
+ @param auth An auth token
@param orgid The org unit in question
@param date The date to search
/
--- /dev/null
+/*
+ * Copyright (C) 2018 Equinox Open Library Initiative Inc.
+ * Mike Rylander <mrylander@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ */
+
+BEGIN;
+
+CREATE TABLE action.emergency_closing (
+ id SERIAL PRIMARY KEY,
+ creator INT NOT NULL REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ process_start_time TIMESTAMPTZ,
+ process_end_time TIMESTAMPTZ,
+ last_update_time TIMESTAMPTZ
+);
+
+ALTER TABLE actor.org_unit_closed
+ ADD COLUMN emergency_closing INT
+ REFERENCES action.emergency_closing (id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;
+
+CREATE TABLE action.emergency_closing_circulation (
+ id BIGSERIAL PRIMARY KEY,
+ emergency_closing INT NOT NULL REFERENCES action.emergency_closing (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ circulation INT NOT NULL REFERENCES action.circulation (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ original_due_date TIMESTAMPTZ,
+ process_time TIMESTAMPTZ
+);
+CREATE INDEX emergency_closing_circulation_emergency_closing_idx ON action.emergency_closing_circulation (emergency_closing);
+CREATE INDEX emergency_closing_circulation_circulation_idx ON action.emergency_closing_circulation (circulation);
+
+CREATE TABLE action.emergency_closing_reservation (
+ id BIGSERIAL PRIMARY KEY,
+ emergency_closing INT NOT NULL REFERENCES action.emergency_closing (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ reservation INT NOT NULL REFERENCES booking.reservation (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ original_end_time TIMESTAMPTZ,
+ process_time TIMESTAMPTZ
+);
+CREATE INDEX emergency_closing_reservation_emergency_closing_idx ON action.emergency_closing_reservation (emergency_closing);
+CREATE INDEX emergency_closing_reservation_reservation_idx ON action.emergency_closing_reservation (reservation);
+
+CREATE TABLE action.emergency_closing_hold (
+ id BIGSERIAL PRIMARY KEY,
+ emergency_closing INT NOT NULL REFERENCES action.emergency_closing (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ hold INT NOT NULL REFERENCES action.hold_request (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ original_shelf_expire_time TIMESTAMPTZ,
+ process_time TIMESTAMPTZ
+);
+CREATE INDEX emergency_closing_hold_emergency_closing_idx ON action.emergency_closing_hold (emergency_closing);
+CREATE INDEX emergency_closing_hold_hold_idx ON action.emergency_closing_hold (hold);
+
+CREATE OR REPLACE VIEW action.emergency_closing_status AS
+ SELECT e.*,
+ COALESCE(c.count, 0) AS circulations,
+ COALESCE(c.completed, 0) AS circulations_complete,
+ COALESCE(b.count, 0) AS reservations,
+ COALESCE(b.completed, 0) AS reservations_complete,
+ COALESCE(h.count, 0) AS holds,
+ COALESCE(h.completed, 0) AS holds_complete
+ FROM action.emergency_closing e
+ LEFT JOIN (SELECT emergency_closing, count(*) count, SUM((process_time IS NOT NULL)::INT) completed FROM action.emergency_closing_circulation GROUP BY 1) c ON (c.emergency_closing = e.id)
+ LEFT JOIN (SELECT emergency_closing, count(*) count, SUM((process_time IS NOT NULL)::INT) completed FROM action.emergency_closing_reservation GROUP BY 1) b ON (b.emergency_closing = e.id)
+ LEFT JOIN (SELECT emergency_closing, count(*) count, SUM((process_time IS NOT NULL)::INT) completed FROM action.emergency_closing_hold GROUP BY 1) h ON (h.emergency_closing = e.id)
+;
+
+CREATE OR REPLACE FUNCTION evergreen.find_next_open_time ( circ_lib INT, initial TIMESTAMPTZ, hourly BOOL DEFAULT FALSE, initial_time TIME DEFAULT NULL, dow_count INT DEFAULT 0 )
+ RETURNS TIMESTAMPTZ AS $$
+DECLARE
+ day_number INT;
+ plus_days INT;
+ final_time TEXT;
+ time_adjusted BOOL;
+ hoo_open TIME WITHOUT TIME ZONE;
+ hoo_close TIME WITHOUT TIME ZONE;
+ adjacent actor.org_unit_closed%ROWTYPE;
+ breakout INT := 0;
+BEGIN
+
+ IF dow_count > 6 THEN
+ RETURN initial;
+ END IF;
+
+ IF initial_time IS NULL THEN
+ initial_time := initial::TIME;
+ END IF;
+
+ final_time := (initial + '1 second'::INTERVAL)::TEXT;
+ LOOP
+ breakout := breakout + 1;
+
+ time_adjusted := FALSE;
+
+ IF dow_count > 0 THEN -- we're recursing, so check for HOO closing
+ day_number := EXTRACT(ISODOW FROM final_time::TIMESTAMPTZ) - 1;
+ plus_days := 0;
+ FOR i IN 1..7 LOOP
+ EXECUTE 'SELECT dow_' || day_number || '_open, dow_' || day_number || '_close FROM actor.hours_of_operation WHERE id = $1'
+ INTO hoo_open, hoo_close
+ USING circ_lib;
+
+ -- RAISE NOTICE 'initial time: %; dow: %; close: %',initial_time,day_number,hoo_close;
+
+ IF hoo_close = '00:00:00' THEN -- bah ... I guess we'll check the next day
+ day_number := (day_number + 1) % 7;
+ plus_days := plus_days + 1;
+ time_adjusted := TRUE;
+ CONTINUE;
+ END IF;
+
+ IF hoo_close IS NULL THEN -- no hours of operation ... assume no closing?
+ hoo_close := '23:59:59';
+ END IF;
+
+ EXIT;
+ END LOOP;
+
+ final_time := DATE(final_time::TIMESTAMPTZ + (plus_days || ' days')::INTERVAL)::TEXT;
+ IF hoo_close <> '00:00:00' AND hourly THEN -- Not a day-granular circ
+ final_time := final_time||' '|| hoo_close;
+ ELSE
+ final_time := final_time||' 23:59:59';
+ END IF;
+ END IF;
+
+ -- Loop through other closings
+ LOOP
+ SELECT * INTO adjacent FROM actor.org_unit_closed WHERE org_unit = circ_lib AND final_time::TIMESTAMPTZ between close_start AND close_end;
+ EXIT WHEN adjacent.id IS NULL;
+ time_adjusted := TRUE;
+ -- RAISE NOTICE 'recursing for closings with final_time: %',final_time;
+ final_time := evergreen.find_next_open_time(circ_lib, adjacent.close_end::TIMESTAMPTZ, hourly, initial_time, dow_count + 1)::TEXT;
+ END LOOP;
+
+ EXIT WHEN breakout > 100;
+ EXIT WHEN NOT time_adjusted;
+
+ END LOOP;
+
+ RETURN final_time;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE TYPE action.emergency_closing_stage_1_count AS (circulations INT, reservations INT, holds INT);
+CREATE OR REPLACE FUNCTION action.emergency_closing_stage_1 ( e_closing INT )
+ RETURNS SETOF action.emergency_closing_stage_1_count AS $$
+DECLARE
+ tmp INT;
+ touched action.emergency_closing_stage_1_count%ROWTYPE;
+BEGIN
+ -- First, gather circs
+ INSERT INTO action.emergency_closing_circulation (emergency_closing, circulation)
+ SELECT e_closing,
+ circ.id
+ FROM actor.org_unit_closed closing
+ JOIN action.emergency_closing ec ON (closing.emergency_closing = ec.id AND ec.id = e_closing)
+ JOIN action.circulation circ ON (
+ circ.circ_lib = closing.org_unit
+ AND circ.due_date BETWEEN closing.close_start AND (closing.close_end + '1s'::INTERVAL)
+ AND circ.xact_finish IS NULL
+ )
+ WHERE NOT EXISTS (SELECT 1 FROM action.emergency_closing_circulation t WHERE t.emergency_closing = e_closing AND t.circulation = circ.id);
+
+ GET DIAGNOSTICS tmp = ROW_COUNT;
+ touched.circulations := tmp;
+
+ INSERT INTO action.emergency_closing_reservation (emergency_closing, reservation)
+ SELECT e_closing,
+ res.id
+ FROM actor.org_unit_closed closing
+ JOIN action.emergency_closing ec ON (closing.emergency_closing = ec.id AND ec.id = e_closing)
+ JOIN booking.reservation res ON (
+ res.pickup_lib = closing.org_unit
+ AND res.end_time BETWEEN closing.close_start AND (closing.close_end + '1s'::INTERVAL)
+ )
+ WHERE NOT EXISTS (SELECT 1 FROM action.emergency_closing_reservation t WHERE t.emergency_closing = e_closing AND t.reservation = res.id);
+
+ GET DIAGNOSTICS tmp = ROW_COUNT;
+ touched.reservations := tmp;
+
+ INSERT INTO action.emergency_closing_hold (emergency_closing, hold)
+ SELECT e_closing,
+ hold.id
+ FROM actor.org_unit_closed closing
+ JOIN action.emergency_closing ec ON (closing.emergency_closing = ec.id AND ec.id = e_closing)
+ JOIN action.hold_request hold ON (
+ pickup_lib = closing.org_unit
+ AND hold.shelf_expire_time BETWEEN closing.close_start AND (closing.close_end + '1s'::INTERVAL)
+ AND hold.fulfillment_time IS NULL
+ AND hold.cancel_time IS NULL
+ )
+ WHERE NOT EXISTS (SELECT 1 FROM action.emergency_closing_hold t WHERE t.emergency_closing = e_closing AND t.hold = hold.id);
+
+ GET DIAGNOSTICS tmp = ROW_COUNT;
+ touched.holds := tmp;
+
+ UPDATE action.emergency_closing
+ SET process_start_time = NOW(),
+ last_update_time = NOW()
+ WHERE id = e_closing;
+
+ RETURN NEXT touched;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION action.emergency_closing_stage_2_hold ( hold_closing_entry INT )
+ RETURNS BOOL AS $$
+DECLARE
+ hold action.hold_request%ROWTYPE;
+ e_closing action.emergency_closing%ROWTYPE;
+ e_c_hold action.emergency_closing_hold%ROWTYPE;
+ closing actor.org_unit_closed%ROWTYPE;
+ day_number INT;
+ hoo_close TIME WITHOUT TIME ZONE;
+ plus_days INT;
+BEGIN
+ -- Gather objects involved
+ SELECT * INTO e_c_hold
+ FROM action.emergency_closing_hold
+ WHERE id = hold_closing_entry;
+
+ IF e_c_hold.process_time IS NOT NULL THEN
+ -- Already processed ... moving on
+ RETURN FALSE;
+ END IF;
+
+ SELECT * INTO e_closing
+ FROM action.emergency_closing
+ WHERE id = e_c_hold.emergency_closing;
+
+ IF e_closing.process_start_time IS NULL THEN
+ -- Huh... that's odd. And wrong.
+ RETURN FALSE;
+ END IF;
+
+ SELECT * INTO closing
+ FROM actor.org_unit_closed
+ WHERE emergency_closing = e_closing.id;
+
+ SELECT * INTO hold
+ FROM action.hold_request h
+ WHERE id = e_c_hold.hold;
+
+ -- Record the processing
+ UPDATE action.emergency_closing_hold
+ SET original_shelf_expire_time = hold.shelf_expire_time,
+ process_time = NOW()
+ WHERE id = hold_closing_entry;
+
+ UPDATE action.emergency_closing
+ SET last_update_time = NOW()
+ WHERE id = e_closing.id;
+
+ UPDATE action.hold_request
+ SET shelf_expire_time = evergreen.find_next_open_time(closing.org_unit, hold.shelf_expire_time, TRUE)
+ WHERE id = hold.id;
+
+ RETURN TRUE;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION action.emergency_closing_stage_2_circ ( circ_closing_entry INT )
+ RETURNS BOOL AS $$
+DECLARE
+ circ action.circulation%ROWTYPE;
+ e_closing action.emergency_closing%ROWTYPE;
+ e_c_circ action.emergency_closing_circulation%ROWTYPE;
+ closing actor.org_unit_closed%ROWTYPE;
+ adjacent actor.org_unit_closed%ROWTYPE;
+ bill money.billing%ROWTYPE;
+ last_bill money.billing%ROWTYPE;
+ day_number INT;
+ hoo_close TIME WITHOUT TIME ZONE;
+ plus_days INT;
+ avoid_negative BOOL;
+ extend_grace BOOL;
+ new_due_date TEXT;
+BEGIN
+ -- Gather objects involved
+ SELECT * INTO e_c_circ
+ FROM action.emergency_closing_circulation
+ WHERE id = circ_closing_entry;
+
+ IF e_c_circ.process_time IS NOT NULL THEN
+ -- Already processed ... moving on
+ RETURN FALSE;
+ END IF;
+
+ SELECT * INTO e_closing
+ FROM action.emergency_closing
+ WHERE id = e_c_circ.emergency_closing;
+
+ IF e_closing.process_start_time IS NULL THEN
+ -- Huh... that's odd. And wrong.
+ RETURN FALSE;
+ END IF;
+
+ SELECT * INTO closing
+ FROM actor.org_unit_closed
+ WHERE emergency_closing = e_closing.id;
+
+ SELECT * INTO circ
+ FROM action.circulation
+ WHERE id = e_c_circ.circulation;
+
+ -- Record the processing
+ UPDATE action.emergency_closing_circulation
+ SET original_due_date = circ.due_date,
+ process_time = NOW()
+ WHERE id = circ_closing_entry;
+
+ UPDATE action.emergency_closing
+ SET last_update_time = NOW()
+ WHERE id = e_closing.id;
+
+ SELECT value::BOOL INTO avoid_negative FROM actor.org_unit_ancestor_setting('bill.prohibit_negative_balance_on_overdues', circ.circ_lib);
+ SELECT value::BOOL INTO extend_grace FROM actor.org_unit_ancestor_setting('circ.grace.extend', circ.circ_lib);
+
+ new_due_date := evergreen.find_next_open_time( closing.org_unit, circ.due_date, EXTRACT(EPOCH FROM circ.duration)::INT % 86400 > 0 )::TEXT;
+ UPDATE action.circulation SET due_date = new_due_date::TIMESTAMPTZ WHERE id = circ.id;
+
+ -- Now, see if we need to get rid of some fines
+ SELECT * INTO last_bill
+ FROM money.billing b
+ WHERE b.xact = circ.id
+ AND NOT b.voided
+ AND b.btype = 1
+ ORDER BY billing_ts DESC
+ LIMIT 1;
+
+ FOR bill IN
+ SELECT *
+ FROM money.billing b
+ WHERE b.xact = circ.id
+ AND b.btype = 1
+ AND NOT b.voided
+ AND (
+ b.billing_ts BETWEEN closing.close_start AND new_due_date::TIMESTAMPTZ
+ OR (extend_grace AND last_bill.billing_ts <= new_due_date::TIMESTAMPTZ + circ.grace_period)
+ )
+ AND NOT EXISTS (SELECT 1 FROM money.account_adjustment a WHERE a.billing = b.id)
+ ORDER BY billing_ts
+ LOOP
+ IF avoid_negative THEN
+ PERFORM FROM money.materialized_billable_xact_summary WHERE id = circ.id AND balanced_owd < bill.amount;
+ EXIT WHEN FOUND; -- We can't go negative, and voiding this bill would do that...
+ END IF;
+
+ UPDATE money.billing
+ SET voided = TRUE,
+ void_time = NOW(),
+ note = COALESCE(note,'') || ' :: Voided by emergency closing handler'
+ WHERE id = bill.id;
+ END LOOP;
+
+ RETURN TRUE;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION action.emergency_closing_stage_2_reservation ( res_closing_entry INT )
+ RETURNS BOOL AS $$
+DECLARE
+ res booking.reservation%ROWTYPE;
+ e_closing action.emergency_closing%ROWTYPE;
+ e_c_res action.emergency_closing_reservation%ROWTYPE;
+ closing actor.org_unit_closed%ROWTYPE;
+ adjacent actor.org_unit_closed%ROWTYPE;
+ bill money.billing%ROWTYPE;
+ day_number INT;
+ hoo_close TIME WITHOUT TIME ZONE;
+ plus_days INT;
+ avoid_negative BOOL;
+ new_due_date TEXT;
+BEGIN
+ -- Gather objects involved
+ SELECT * INTO e_c_res
+ FROM action.emergency_closing_reservation
+ WHERE id = res_closing_entry;
+
+ IF e_c_res.process_time IS NOT NULL THEN
+ -- Already processed ... moving on
+ RETURN FALSE;
+ END IF;
+
+ SELECT * INTO e_closing
+ FROM action.emergency_closing
+ WHERE id = e_c_res.emergency_closing;
+
+ IF e_closing.process_start_time IS NULL THEN
+ -- Huh... that's odd. And wrong.
+ RETURN FALSE;
+ END IF;
+
+ SELECT * INTO closing
+ FROM actor.org_unit_closed
+ WHERE emergency_closing = e_closing.id;
+
+ SELECT * INTO res
+ FROM booking.reservation
+ WHERE id = e_c_res.reservation;
+
+ IF res.pickup_lib IS NULL THEN -- Need to be far enough along to have a pickup lib
+ RETURN FALSE;
+ END IF;
+
+ -- Record the processing
+ UPDATE action.emergency_closing_reservation
+ SET original_end_time = res.end_time,
+ process_time = NOW()
+ WHERE id = res_closing_entry;
+
+ UPDATE action.emergency_closing
+ SET last_update_time = NOW()
+ WHERE id = e_closing.id;
+
+ SELECT value::BOOL INTO avoid_negative FROM actor.org_unit_ancestor_setting('bill.prohibit_negative_balance_on_overdues', res.pickup_lib);
+
+ new_due_date := evergreen.find_next_open_time( closing.org_unit, res.end_time, EXTRACT(EPOCH FROM res.booking_interval)::INT % 86400 > 0 )::TEXT;
+ UPDATE booking.reservation SET end_time = new_due_date::TIMESTAMPTZ WHERE id = res.id;
+
+ -- Now, see if we need to get rid of some fines
+ FOR bill IN
+ SELECT *
+ FROM money.billing b
+ WHERE b.xact = res.id
+ AND b.btype = 1
+ AND NOT b.voided
+ AND b.billing_ts BETWEEN closing.close_start AND new_due_date::TIMESTAMPTZ
+ AND NOT EXISTS (SELECT 1 FROM money.account_adjustment a WHERE a.billing = b.id)
+ LOOP
+ IF avoid_negative THEN
+ PERFORM FROM money.materialized_billable_xact_summary WHERE id = res.id AND balanced_owd < bill.amount;
+ EXIT WHEN FOUND; -- We can't go negative, and voiding this bill would do that...
+ END IF;
+
+ UPDATE money.billing
+ SET voided = TRUE,
+ void_time = NOW(),
+ note = COALESCE(note,'') || ' :: Voided by emergency closing handler'
+ WHERE id = bill.id;
+ END LOOP;
+
+ RETURN TRUE;
+END;
+$$ LANGUAGE PLPGSQL;
+
+COMMIT;
+
INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('damaged','acp','Item marked damaged');
INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('checkout.damaged','circ','A circulating item is marked damaged and the patron is fined');
INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('renewal','circ','Item renewed to user');
+INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('checkout.due.emergency_closing','aecc','Circulation due date was adjusted by the Emergency Closing handler');
+INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('hold.shelf_expire.emergency_closing','aech','Hold shelf expire time was adjusted by the Emergency Closing handler');
+INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('booking.due.emergency_closing','aecr','Booking reservation return date was adjusted by the Emergency Closing handler');
-- and much more, I'm sure
( 605, 'UPDATE_COPY_ALERT', oils_i18n_gettext( 605,
'Update copy alerts', 'ppl', 'description' )),
( 606, 'DELETE_COPY_ALERT', oils_i18n_gettext( 606,
- 'Delete copy alerts', 'ppl', 'description' ))
+ 'Delete copy alerts', 'ppl', 'description' )),
+ ( 607, 'EMERGENCY_CLOSING', oils_i18n_gettext( 607,
+ 'Create and manage Emergency Closings', 'ppl', 'description' ))
;
SELECT SETVAL('permission.perm_list_id_seq'::TEXT, 1000);
080.schema.money.sql
090.schema.action.sql
095.schema.booking.sql
+
+096.schema.emergency_closing.sql
099.matrix_weights.sql
100.circ_matrix.sql
--- /dev/null
+BEGIN;
+
+SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO permission.perm_list (id,code,description) VALUES ( 607, 'EMERGENCY_CLOSING', 'Create and manage Emergency Closings');
+
+INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('checkout.due.emergency_closing','aecc','Circulation due date was adjusted by the Emergency Closing handler');
+INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('hold.shelf_expire.emergency_closing','aech','Hold shelf expire time was adjusted by the Emergency Closing handler');
+INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('booking.due.emergency_closing','aecr','Booking reservation return date was adjusted by the Emergency Closing handler');
+
+CREATE TABLE action.emergency_closing (
+ id SERIAL PRIMARY KEY,
+ creator INT NOT NULL REFERENCES actor.usr (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ create_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ process_start_time TIMESTAMPTZ,
+ process_end_time TIMESTAMPTZ,
+ last_update_time TIMESTAMPTZ
+);
+
+ALTER TABLE actor.org_unit_closed
+ ADD COLUMN emergency_closing INT
+ REFERENCES action.emergency_closing (id) ON DELETE RESTRICT DEFERRABLE INITIALLY DEFERRED;
+
+CREATE TABLE action.emergency_closing_circulation (
+ id BIGSERIAL PRIMARY KEY,
+ emergency_closing INT NOT NULL REFERENCES action.emergency_closing (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ circulation INT NOT NULL REFERENCES action.circulation (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ original_due_date TIMESTAMPTZ,
+ process_time TIMESTAMPTZ
+);
+CREATE INDEX emergency_closing_circulation_emergency_closing_idx ON action.emergency_closing_circulation (emergency_closing);
+CREATE INDEX emergency_closing_circulation_circulation_idx ON action.emergency_closing_circulation (circulation);
+
+CREATE TABLE action.emergency_closing_reservation (
+ id BIGSERIAL PRIMARY KEY,
+ emergency_closing INT NOT NULL REFERENCES action.emergency_closing (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ reservation INT NOT NULL REFERENCES booking.reservation (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ original_end_time TIMESTAMPTZ,
+ process_time TIMESTAMPTZ
+);
+CREATE INDEX emergency_closing_reservation_emergency_closing_idx ON action.emergency_closing_reservation (emergency_closing);
+CREATE INDEX emergency_closing_reservation_reservation_idx ON action.emergency_closing_reservation (reservation);
+
+CREATE TABLE action.emergency_closing_hold (
+ id BIGSERIAL PRIMARY KEY,
+ emergency_closing INT NOT NULL REFERENCES action.emergency_closing (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ hold INT NOT NULL REFERENCES action.hold_request (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
+ original_shelf_expire_time TIMESTAMPTZ,
+ process_time TIMESTAMPTZ
+);
+CREATE INDEX emergency_closing_hold_emergency_closing_idx ON action.emergency_closing_hold (emergency_closing);
+CREATE INDEX emergency_closing_hold_hold_idx ON action.emergency_closing_hold (hold);
+
+CREATE OR REPLACE VIEW action.emergency_closing_status AS
+ SELECT e.*,
+ COALESCE(c.count, 0) AS circulations,
+ COALESCE(c.completed, 0) AS circulations_complete,
+ COALESCE(b.count, 0) AS reservations,
+ COALESCE(b.completed, 0) AS reservations_complete,
+ COALESCE(h.count, 0) AS holds,
+ COALESCE(h.completed, 0) AS holds_complete
+ FROM action.emergency_closing e
+ LEFT JOIN (SELECT emergency_closing, count(*) count, SUM((process_time IS NOT NULL)::INT) completed FROM action.emergency_closing_circulation GROUP BY 1) c ON (c.emergency_closing = e.id)
+ LEFT JOIN (SELECT emergency_closing, count(*) count, SUM((process_time IS NOT NULL)::INT) completed FROM action.emergency_closing_reservation GROUP BY 1) b ON (b.emergency_closing = e.id)
+ LEFT JOIN (SELECT emergency_closing, count(*) count, SUM((process_time IS NOT NULL)::INT) completed FROM action.emergency_closing_hold GROUP BY 1) h ON (h.emergency_closing = e.id)
+;
+
+CREATE OR REPLACE FUNCTION evergreen.find_next_open_time ( circ_lib INT, initial TIMESTAMPTZ, hourly BOOL DEFAULT FALSE, initial_time TIME DEFAULT NULL, dow_count INT DEFAULT 0 )
+ RETURNS TIMESTAMPTZ AS $$
+DECLARE
+ day_number INT;
+ plus_days INT;
+ final_time TEXT;
+ time_adjusted BOOL;
+ hoo_open TIME WITHOUT TIME ZONE;
+ hoo_close TIME WITHOUT TIME ZONE;
+ adjacent actor.org_unit_closed%ROWTYPE;
+ breakout INT := 0;
+BEGIN
+
+ IF dow_count > 6 THEN
+ RETURN initial;
+ END IF;
+
+ IF initial_time IS NULL THEN
+ initial_time := initial::TIME;
+ END IF;
+
+ final_time := (initial + '1 second'::INTERVAL)::TEXT;
+ LOOP
+ breakout := breakout + 1;
+
+ time_adjusted := FALSE;
+
+ IF dow_count > 0 THEN -- we're recursing, so check for HOO closing
+ day_number := EXTRACT(ISODOW FROM final_time::TIMESTAMPTZ) - 1;
+ plus_days := 0;
+ FOR i IN 1..7 LOOP
+ EXECUTE 'SELECT dow_' || day_number || '_open, dow_' || day_number || '_close FROM actor.hours_of_operation WHERE id = $1'
+ INTO hoo_open, hoo_close
+ USING circ_lib;
+
+ -- RAISE NOTICE 'initial time: %; dow: %; close: %',initial_time,day_number,hoo_close;
+
+ IF hoo_close = '00:00:00' THEN -- bah ... I guess we'll check the next day
+ day_number := (day_number + 1) % 7;
+ plus_days := plus_days + 1;
+ time_adjusted := TRUE;
+ CONTINUE;
+ END IF;
+
+ IF hoo_close IS NULL THEN -- no hours of operation ... assume no closing?
+ hoo_close := '23:59:59';
+ END IF;
+
+ EXIT;
+ END LOOP;
+
+ final_time := DATE(final_time::TIMESTAMPTZ + (plus_days || ' days')::INTERVAL)::TEXT;
+ IF hoo_close <> '00:00:00' AND hourly THEN -- Not a day-granular circ
+ final_time := final_time||' '|| hoo_close;
+ ELSE
+ final_time := final_time||' 23:59:59';
+ END IF;
+ END IF;
+
+ -- Loop through other closings
+ LOOP
+ SELECT * INTO adjacent FROM actor.org_unit_closed WHERE org_unit = circ_lib AND final_time::TIMESTAMPTZ between close_start AND close_end;
+ EXIT WHEN adjacent.id IS NULL;
+ time_adjusted := TRUE;
+ -- RAISE NOTICE 'recursing for closings with final_time: %',final_time;
+ final_time := evergreen.find_next_open_time(circ_lib, adjacent.close_end::TIMESTAMPTZ, hourly, initial_time, dow_count + 1)::TEXT;
+ END LOOP;
+
+ EXIT WHEN breakout > 100;
+ EXIT WHEN NOT time_adjusted;
+
+ END LOOP;
+
+ RETURN final_time;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE TYPE action.emergency_closing_stage_1_count AS (circulations INT, reservations INT, holds INT);
+CREATE OR REPLACE FUNCTION action.emergency_closing_stage_1 ( e_closing INT )
+ RETURNS SETOF action.emergency_closing_stage_1_count AS $$
+DECLARE
+ tmp INT;
+ touched action.emergency_closing_stage_1_count%ROWTYPE;
+BEGIN
+ -- First, gather circs
+ INSERT INTO action.emergency_closing_circulation (emergency_closing, circulation)
+ SELECT e_closing,
+ circ.id
+ FROM actor.org_unit_closed closing
+ JOIN action.emergency_closing ec ON (closing.emergency_closing = ec.id AND ec.id = e_closing)
+ JOIN action.circulation circ ON (
+ circ.circ_lib = closing.org_unit
+ AND circ.due_date BETWEEN closing.close_start AND (closing.close_end + '1s'::INTERVAL)
+ AND circ.xact_finish IS NULL
+ )
+ WHERE NOT EXISTS (SELECT 1 FROM action.emergency_closing_circulation t WHERE t.emergency_closing = e_closing AND t.circulation = circ.id);
+
+ GET DIAGNOSTICS tmp = ROW_COUNT;
+ touched.circulations := tmp;
+
+ INSERT INTO action.emergency_closing_reservation (emergency_closing, reservation)
+ SELECT e_closing,
+ res.id
+ FROM actor.org_unit_closed closing
+ JOIN action.emergency_closing ec ON (closing.emergency_closing = ec.id AND ec.id = e_closing)
+ JOIN booking.reservation res ON (
+ res.pickup_lib = closing.org_unit
+ AND res.end_time BETWEEN closing.close_start AND (closing.close_end + '1s'::INTERVAL)
+ )
+ WHERE NOT EXISTS (SELECT 1 FROM action.emergency_closing_reservation t WHERE t.emergency_closing = e_closing AND t.reservation = res.id);
+
+ GET DIAGNOSTICS tmp = ROW_COUNT;
+ touched.reservations := tmp;
+
+ INSERT INTO action.emergency_closing_hold (emergency_closing, hold)
+ SELECT e_closing,
+ hold.id
+ FROM actor.org_unit_closed closing
+ JOIN action.emergency_closing ec ON (closing.emergency_closing = ec.id AND ec.id = e_closing)
+ JOIN action.hold_request hold ON (
+ pickup_lib = closing.org_unit
+ AND hold.shelf_expire_time BETWEEN closing.close_start AND (closing.close_end + '1s'::INTERVAL)
+ AND hold.fulfillment_time IS NULL
+ AND hold.cancel_time IS NULL
+ )
+ WHERE NOT EXISTS (SELECT 1 FROM action.emergency_closing_hold t WHERE t.emergency_closing = e_closing AND t.hold = hold.id);
+
+ GET DIAGNOSTICS tmp = ROW_COUNT;
+ touched.holds := tmp;
+
+ UPDATE action.emergency_closing
+ SET process_start_time = NOW(),
+ last_update_time = NOW()
+ WHERE id = e_closing;
+
+ RETURN NEXT touched;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION action.emergency_closing_stage_2_hold ( hold_closing_entry INT )
+ RETURNS BOOL AS $$
+DECLARE
+ hold action.hold_request%ROWTYPE;
+ e_closing action.emergency_closing%ROWTYPE;
+ e_c_hold action.emergency_closing_hold%ROWTYPE;
+ closing actor.org_unit_closed%ROWTYPE;
+ day_number INT;
+ hoo_close TIME WITHOUT TIME ZONE;
+ plus_days INT;
+BEGIN
+ -- Gather objects involved
+ SELECT * INTO e_c_hold
+ FROM action.emergency_closing_hold
+ WHERE id = hold_closing_entry;
+
+ IF e_c_hold.process_time IS NOT NULL THEN
+ -- Already processed ... moving on
+ RETURN FALSE;
+ END IF;
+
+ SELECT * INTO e_closing
+ FROM action.emergency_closing
+ WHERE id = e_c_hold.emergency_closing;
+
+ IF e_closing.process_start_time IS NULL THEN
+ -- Huh... that's odd. And wrong.
+ RETURN FALSE;
+ END IF;
+
+ SELECT * INTO closing
+ FROM actor.org_unit_closed
+ WHERE emergency_closing = e_closing.id;
+
+ SELECT * INTO hold
+ FROM action.hold_request h
+ WHERE id = e_c_hold.hold;
+
+ -- Record the processing
+ UPDATE action.emergency_closing_hold
+ SET original_shelf_expire_time = hold.shelf_expire_time,
+ process_time = NOW()
+ WHERE id = hold_closing_entry;
+
+ UPDATE action.emergency_closing
+ SET last_update_time = NOW()
+ WHERE id = e_closing.id;
+
+ UPDATE action.hold_request
+ SET shelf_expire_time = evergreen.find_next_open_time(closing.org_unit, hold.shelf_expire_time, TRUE)
+ WHERE id = hold.id;
+
+ RETURN TRUE;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION action.emergency_closing_stage_2_circ ( circ_closing_entry INT )
+ RETURNS BOOL AS $$
+DECLARE
+ circ action.circulation%ROWTYPE;
+ e_closing action.emergency_closing%ROWTYPE;
+ e_c_circ action.emergency_closing_circulation%ROWTYPE;
+ closing actor.org_unit_closed%ROWTYPE;
+ adjacent actor.org_unit_closed%ROWTYPE;
+ bill money.billing%ROWTYPE;
+ last_bill money.billing%ROWTYPE;
+ day_number INT;
+ hoo_close TIME WITHOUT TIME ZONE;
+ plus_days INT;
+ avoid_negative BOOL;
+ extend_grace BOOL;
+ new_due_date TEXT;
+BEGIN
+ -- Gather objects involved
+ SELECT * INTO e_c_circ
+ FROM action.emergency_closing_circulation
+ WHERE id = circ_closing_entry;
+
+ IF e_c_circ.process_time IS NOT NULL THEN
+ -- Already processed ... moving on
+ RETURN FALSE;
+ END IF;
+
+ SELECT * INTO e_closing
+ FROM action.emergency_closing
+ WHERE id = e_c_circ.emergency_closing;
+
+ IF e_closing.process_start_time IS NULL THEN
+ -- Huh... that's odd. And wrong.
+ RETURN FALSE;
+ END IF;
+
+ SELECT * INTO closing
+ FROM actor.org_unit_closed
+ WHERE emergency_closing = e_closing.id;
+
+ SELECT * INTO circ
+ FROM action.circulation
+ WHERE id = e_c_circ.circulation;
+
+ -- Record the processing
+ UPDATE action.emergency_closing_circulation
+ SET original_due_date = circ.due_date,
+ process_time = NOW()
+ WHERE id = circ_closing_entry;
+
+ UPDATE action.emergency_closing
+ SET last_update_time = NOW()
+ WHERE id = e_closing.id;
+
+ SELECT value::BOOL INTO avoid_negative FROM actor.org_unit_ancestor_setting('bill.prohibit_negative_balance_on_overdues', circ.circ_lib);
+ SELECT value::BOOL INTO extend_grace FROM actor.org_unit_ancestor_setting('circ.grace.extend', circ.circ_lib);
+
+ new_due_date := evergreen.find_next_open_time( closing.org_unit, circ.due_date, EXTRACT(EPOCH FROM circ.duration)::INT % 86400 > 0 )::TEXT;
+ UPDATE action.circulation SET due_date = new_due_date::TIMESTAMPTZ WHERE id = circ.id;
+
+ -- Now, see if we need to get rid of some fines
+ SELECT * INTO last_bill
+ FROM money.billing b
+ WHERE b.xact = circ.id
+ AND NOT b.voided
+ AND b.btype = 1
+ ORDER BY billing_ts DESC
+ LIMIT 1;
+
+ FOR bill IN
+ SELECT *
+ FROM money.billing b
+ WHERE b.xact = circ.id
+ AND b.btype = 1
+ AND NOT b.voided
+ AND (
+ b.billing_ts BETWEEN closing.close_start AND new_due_date::TIMESTAMPTZ
+ OR (extend_grace AND last_bill.billing_ts <= new_due_date::TIMESTAMPTZ + circ.grace_period)
+ )
+ AND NOT EXISTS (SELECT 1 FROM money.account_adjustment a WHERE a.billing = b.id)
+ ORDER BY billing_ts
+ LOOP
+ IF avoid_negative THEN
+ PERFORM FROM money.materialized_billable_xact_summary WHERE id = circ.id AND balanced_owd < bill.amount;
+ EXIT WHEN FOUND; -- We can't go negative, and voiding this bill would do that...
+ END IF;
+
+ UPDATE money.billing
+ SET voided = TRUE,
+ void_time = NOW(),
+ note = COALESCE(note,'') || ' :: Voided by emergency closing handler'
+ WHERE id = bill.id;
+ END LOOP;
+
+ RETURN TRUE;
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE OR REPLACE FUNCTION action.emergency_closing_stage_2_reservation ( res_closing_entry INT )
+ RETURNS BOOL AS $$
+DECLARE
+ res booking.reservation%ROWTYPE;
+ e_closing action.emergency_closing%ROWTYPE;
+ e_c_res action.emergency_closing_reservation%ROWTYPE;
+ closing actor.org_unit_closed%ROWTYPE;
+ adjacent actor.org_unit_closed%ROWTYPE;
+ bill money.billing%ROWTYPE;
+ day_number INT;
+ hoo_close TIME WITHOUT TIME ZONE;
+ plus_days INT;
+ avoid_negative BOOL;
+ new_due_date TEXT;
+BEGIN
+ -- Gather objects involved
+ SELECT * INTO e_c_res
+ FROM action.emergency_closing_reservation
+ WHERE id = res_closing_entry;
+
+ IF e_c_res.process_time IS NOT NULL THEN
+ -- Already processed ... moving on
+ RETURN FALSE;
+ END IF;
+
+ SELECT * INTO e_closing
+ FROM action.emergency_closing
+ WHERE id = e_c_res.emergency_closing;
+
+ IF e_closing.process_start_time IS NULL THEN
+ -- Huh... that's odd. And wrong.
+ RETURN FALSE;
+ END IF;
+
+ SELECT * INTO closing
+ FROM actor.org_unit_closed
+ WHERE emergency_closing = e_closing.id;
+
+ SELECT * INTO res
+ FROM booking.reservation
+ WHERE id = e_c_res.reservation;
+
+ IF res.pickup_lib IS NULL THEN -- Need to be far enough along to have a pickup lib
+ RETURN FALSE;
+ END IF;
+
+ -- Record the processing
+ UPDATE action.emergency_closing_reservation
+ SET original_end_time = res.end_time,
+ process_time = NOW()
+ WHERE id = res_closing_entry;
+
+ UPDATE action.emergency_closing
+ SET last_update_time = NOW()
+ WHERE id = e_closing.id;
+
+ SELECT value::BOOL INTO avoid_negative FROM actor.org_unit_ancestor_setting('bill.prohibit_negative_balance_on_overdues', res.pickup_lib);
+
+ new_due_date := evergreen.find_next_open_time( closing.org_unit, res.end_time, EXTRACT(EPOCH FROM res.booking_interval)::INT % 86400 > 0 )::TEXT;
+ UPDATE booking.reservation SET end_time = new_due_date::TIMESTAMPTZ WHERE id = res.id;
+
+ -- Now, see if we need to get rid of some fines
+ FOR bill IN
+ SELECT *
+ FROM money.billing b
+ WHERE b.xact = res.id
+ AND b.btype = 1
+ AND NOT b.voided
+ AND b.billing_ts BETWEEN closing.close_start AND new_due_date::TIMESTAMPTZ
+ AND NOT EXISTS (SELECT 1 FROM money.account_adjustment a WHERE a.billing = b.id)
+ LOOP
+ IF avoid_negative THEN
+ PERFORM FROM money.materialized_billable_xact_summary WHERE id = res.id AND balanced_owd < bill.amount;
+ EXIT WHEN FOUND; -- We can't go negative, and voiding this bill would do that...
+ END IF;
+
+ UPDATE money.billing
+ SET voided = TRUE,
+ void_time = NOW(),
+ note = COALESCE(note,'') || ' :: Voided by emergency closing handler'
+ WHERE id = bill.id;
+ END LOOP;
+
+ RETURN TRUE;
+END;
+$$ LANGUAGE PLPGSQL;
+
+COMMIT;
+
--- /dev/null
+[%
+ WRAPPER 'staff/base.tt2';
+ ctx.page_type = l('Closed Dates');
+ ctx.page_app = 'egAdminClosed';
+ ctx.page_ctrl = 'ClosedDates';
+%]
+
+[% BLOCK APP_JS %]
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/ui.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/admin/local/actor/closed_dates.js"></script>
+<link rel="stylesheet" href="[% ctx.base_path %]/staff/css/admin.css" />
+<script>
+ angular.module('egCoreMod').run(['egStrings', function(s) {
+ s.CONFIRM_CLOSED_DELETE = "[% l('Confirm closed date deletion') %]";
+ s.CONFIRM_CLOSED_DELETE_BODY = '[% l('Delete closing "{{reason}}" for {{org.name()}}?') %]';
+ s.POSSIBLE_EMERGENCY_CLOSING = "[% l('Possible Emergency Closing') %]";
+ s.EMERGENCY_CLOSING = "[% l('Emergency Closing') %]";
+ s.CREATING_CLOSINGS = "[% l('Creating closings') %]";
+ s.PROCESSING_EMERGENCY = "[% l('Processing Emergency Closing') %]";
+ }]);
+</script>
+[% END %]
+
+<div class="container-fluid" style="text-align:center">
+ <div class="alert alert-info alert-less-pad strong-text-2">
+ [% l('Closed Dates Editor') %]
+ </div>
+</div>
+
+<div class="row">
+ <div class="col-md-4">
+ <div class="form-group">
+ <label>[% l('Edit Closed Dates for: ') %] </label>
+ <eg-org-selector onchange="org_changed"
+ selected="context_org"></eg-org-selector>
+ </div>
+ </div>
+ <div class="col-md-4">
+ </div>
+ <div class="col-md-1">
+ <label>[% l('Date Filter: ') %] </label>
+ </div>
+ <div class="col-md-3">
+ <eg-date-input ng-model="date_filter"></eg-date-input>
+ </div>
+</div>
+
+<eg-grid
+ id-field="id"
+ grid-controls="gridControls"
+ items-provider="gridDataProvider"
+ features="-multiselect"
+ persist-key="admin.local.actor.closed_dates"
+ dateformat="{{$root.egDateAndTimeFormat}}">
+
+ <eg-grid-menu-item standalone="true" label="[% l('Add closing') %]" handler="create_aoucd"></eg-grid-action>
+ <eg-grid-menu-item standalone="true" label="[% l('Refresh') %]" handler="refresh_page"></eg-grid-action>
+
+ <eg-grid-action label="[% l('Edit closing') %]" handler="update_aoucd"></eg-grid-action>
+ <eg-grid-action label="[% l('Delete closing') %]" handler="delete_aoucd"></eg-grid-action>
+
+ <eg-grid-field label="[% l('Closing Start') %]" flex="1" path="close_start" visible>
+ {{item.close_start | egDueDate:$root.egDateAndTimeFormat:item.org_unit:item._duration}}
+ </eg-grid-field>
+ <eg-grid-field label="[% l('Closing End') %]" flex="1" path="close_end" visible>
+ {{item.close_end | egDueDate:$root.egDateAndTimeFormat:item.org_unit:item._duration}}
+ </eg-grid-field>
+ <eg-grid-field label="[% l('Reason for Closing') %]" flex="2" path="reason" visible></eg-grid-field>
+ <eg-grid-field label="[% l('Emergency Closing Processing Summary') %]" flex="2" path="emergency_closing.status" visible>
+ <span class="{{item._text_class}}">
+ [% l('Circulations: ') %]{{item.emergency_closing.status.circulations_complete}} / {{item.emergency_closing.status.circulations}} |
+ [% l('Holds: ') %]{{item.emergency_closing.status.holds_complete}} / {{item.emergency_closing.status.holds}} |
+ [% l('Reservations:') %]{{item.emergency_closing.status.reservations_complete}} / {{item.emergency_closing.status.reservations}}
+ </span>
+ </eg-grid-field>
+ <eg-grid-field label="[% l('Full Day') %]" path="full_day" datatype="bool" hidden></eg-grid-field>
+ <eg-grid-field label="[% l('Multiple Days') %]" path="multi_day" datatype="bool" hidden></eg-grid-field>
+ <eg-grid-field label="[% l('Emergency Closing Circulations') %]" path="emergency_closing.status.circulations" hidden>
+ <eg-grid-field label="[% l('Emergency Closing Holds') %]" path="emergency_closing.status.holds" hidden>
+ <eg-grid-field label="[% l('Emergency Closing Reservations') %]" path="emergency_closing.status.reservations" hidden>
+ <eg-grid-field label="[% l('Emergency Closing Circulations Completed') %]" path="emergency_closing.status.circulations_complete" hidden>
+ <eg-grid-field label="[% l('Emergency Closing Holds Completed') %]" path="emergency_closing.status.holds_complete" hidden>
+ <eg-grid-field label="[% l('Emergency Closing Reservations Completed') %]" path="emergency_closing.status.reservations_complete" hidden>
+</eg-grid>
+
+[% END %]
--- /dev/null
+<form ng-submit="ok(args)" role="form" class="form-validated">
+ <div class="modal-header">
+ <button type="button" class="close" ng-click="cancel()"
+ aria-hidden="true">×</button>
+ <h4 class="modal-title">[% l('Library Closing') %]</h4>
+ </div>
+ <div class="modal-body">
+
+ <div class="form-group row">
+ <div class="col-md-3">
+ <label for="org_unit">[% l('Library') %]</label>
+ </div>
+ <div class="col-md-9">
+ <eg-org-selector id="org_unit" selected="org_unit" onchange="update_org_unit"></eg-org-selector>
+ </div>
+ </div>
+
+ <div class="form-group row" ng-if="!is_update">
+ <div class="col-md-9">
+ <label for="apply_to_all">[% l('Apply to all of my libraries') %]</label>
+ </div>
+ <div class="col-md-3">
+ <input id="emergency" type="checkbox" ng-model="args.apply_to_all"/>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <div class="col-md-3">
+ <label for="full_day">[% l('Closing type') %]</label>
+ </div>
+ <div class="col-md-9">
+ <select class="form-control" ng-model="args.type">
+ <option value="full">[% l('One Full Day') %]</option>
+ <option value="multi">[% l('Multiple Days') %]</option>
+ <option value="detailed">[% l('Detailed') %]</option>
+ </select>
+ </div>
+ </div>
+
+ <div class="form-group row" ng-show="args.type == 'full'">
+ <div class="col-md-3">
+ <label for="day">[% l('Date') %]</label>
+ </div>
+ <div class="col-md-9">
+ <eg-date-input id="day" ng-model="args.start"></eg-date-input>
+ </div>
+ </div>
+
+ <div class="form-group row" ng-show="args.type != 'full'">
+ <div class="col-md-3">
+ <label for="start">[% l('Start') %]</label>
+ </div>
+ <div class="col-md-9">
+ <eg-date-input id="start" show-time-picker hide-time-picker="args.is_not_detailed" ng-model="args.start"></eg-date-input>
+ </div>
+ </div>
+
+ <div class="form-group row" ng-show="args.type != 'full'">
+ <div class="col-md-3">
+ <label for="end">[% l('End') %]</label>
+ </div>
+ <div class="col-md-9">
+ <eg-date-input id="end" show-time-picker hide-time-picker="args.is_not_detailed" ng-model="args.end"></eg-date-input>
+ </div>
+ </div>
+
+ <div class="form-group row">
+ <div class="col-md-3">
+ <label for="reason">[% l('Reason') %]</label>
+ </div>
+ <div class="col-md-9">
+ <input id="reason" ng-model="args.reason"/>
+ </div>
+ </div>
+
+ <div class="row alert alert-warning" ng-show="!is_update && is_emergency">
+ [% l('Possible Emergency Closing') %]
+ </div>
+
+ <div class="row alert alert-warning" ng-show="is_update && args.aec">
+ <h2>[% l('Emergency Closing') %]</h2>
+ <dl>
+ <dt>[% l('Circulations') %]</dt>
+ <dd>{{args.aec.status().circulations_complete()}} / {{args.aec.status().circulations()}}</dd>
+ <dt>[% l('Holds') %]</dt>
+ <dd>{{args.aec.status().holds_complete()}} / {{args.aec.status().holds()}}</dd>
+ <dt>[% l('Booking Reservations') %]</dt>
+ <dd>{{args.aec.status().reservations_complete()}} / {{args.aec.status().reservations()}}</dd>
+ </dl>
+ </div>
+
+ <div class="form-group row" ng-hide="is_update && !unprocessed">
+ <div class="col-md-3">
+ <label for="emergency">[% l('Emergency') %]</label>
+ </div>
+ <div class="col-md-9">
+ <input id="emergency" type="checkbox" ng-model="args.create_aec"/>
+ </div>
+ </div>
+
+ <div class="form-group row" ng-hide="is_update && !unprocessed">
+ <div class="col-md-3">
+ <label for="process">[% l('Process immediately') %]</label>
+ </div>
+ <div class="col-md-9">
+ <input id="process" ng-disabled="!args.create_aec" type="checkbox" ng-model="args.process_immediately"/>
+ </div>
+ </div>
+
+ </div>
+ <div class="modal-footer">
+ <input type="submit" class="btn btn-primary" value="[% l('OK') %]"/>
+ <button class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
+ </div>
+ </div> <!-- modal-content -->
+</form>
# create script / css refs to individual files instead of using
# compressed build files. Use this for development and debugging.
-EXPAND_WEB_IMPORTS = 1;
+EXPAND_WEB_IMPORTS = 0;
# path to build files (js, css, fonts). No / at end, because the user supplies it
WEB_BUILD_PATH = ctx.media_prefix _ '/js/ui/default/staff/build';
not line up horizontally very well with the date picker -->
<div>
<span>
- <uib-timepicker
- ng-if="showTimePicker"
+ <div uib-timepicker
+ ng-class="{hidden:!showTimePicker}"
ng-hide="hideTimePicker"
ng-model="ngModel"
ng-disabled="ngDisabled"
ng-required="ngRequired"
ng-blur="ngBlur"
ng-change="ngChange">
- </uib-timepicker>
+ </div>
</span>
</div>
--- /dev/null
+angular.module('egAdminClosed',
+ ['ngRoute','ui.bootstrap','egCoreMod','egUiMod','egGridMod','ngToast'])
+
+.config(['ngToastProvider', function(ngToastProvider) {
+ ngToastProvider.configure({
+ verticalPosition: 'bottom',
+ animation: 'fade'
+ });
+}])
+
+.controller('ClosedDates',
+ ['$scope','$q','$timeout','$location','$window','$uibModal','ngToast',
+ 'egCore','egGridDataProvider','egConfirmDialog','egProgressDialog','$timeout',
+function($scope , $q , $timeout , $location , $window , $uibModal , ngToast ,
+ egCore , egGridDataProvider , egConfirmDialog , egProgressDialog , $timeout) {
+
+ egCore.startup.go().then(function () {
+
+ $scope.context_org = egCore.org.get(egCore.auth.user().ws_ou());
+ $scope.date_filter = new Date();
+ });
+
+ $scope.closings = [];
+ var provider = egGridDataProvider.instance({
+ get : function(offset, count) {
+ $scope.refresh_generation = new Date().getTime();
+ $scope.closings = [];
+ var deferred = $q.defer();
+ egCore.startup.go().then(function(){egCore.pcrud.search(
+ 'aoucd',
+ { org_unit : $scope.context_org.id(),
+ "-or" : [
+ { close_end : { ">=" : $scope.date_filter.toISOString() } },
+ { "-and" : { emergency_closing : { "!=" : null }, "+aec" : { process_end_time : { "=" : null } } } }
+ ]
+ },
+ { order_by : { aoucd : 'close_start' },
+ limit : count,
+ offset: offset,
+ join : { "aec" : { type : "left" } },
+ flesh : 2,
+ flesh_fields : { aoucd : ['emergency_closing'], aec : ['status'] }
+ }
+ ).then(function () {
+ return $scope.closings;
+ }, null, function(cl) {
+ if (!cl) return deferred.resolve();
+
+ var i = egCore.idl.toHash(cl);
+
+ function refresh_emergency_status (status) {
+ if (status._generation == $scope.refresh_generation) {
+ egCore.pcrud.retrieve('aecs',status.id).then(function(s) {
+
+ status.circulations = s.circulations();
+ status.circulations_complete = s.circulations_complete();
+ status.holds = s.holds();
+ status.holds_complete = s.holds_complete();
+ status.reservations = s.reservations();
+ status.reservations_complete = s.reservations_complete();
+
+ if (s.process_start_time() && !s.process_end_time())
+ $timeout(refresh_emergency_status, 2000, true, status);
+ });
+ }
+ }
+
+ var now = new Date();
+ var s = new Date(i.close_start);
+ var e = new Date(i.close_end);
+ i._duration = ((e - s) / 1000) + 1;
+ i._duration = '' + i._duration + ' seconds';
+
+ if (i.emergency_closing) {
+ var x = i.emergency_closing.status.circulations - i.emergency_closing.status.circulations_complete;
+ x += i.emergency_closing.status.holds - i.emergency_closing.status.holds_complete;
+ x += i.emergency_closing.status.reservations - i.emergency_closing.status.reservations_complete;
+
+ if (i.emergency_closing.process_end_time) {
+ i._text_class = 'rounded bg-success';
+ } else { // still work to do!
+ i._text_class = 'rounded bg-primary';
+ i.emergency_closing.status._generation = $scope.refresh_generation;;
+ refresh_emergency_status(i.emergency_closing.status);
+ }
+ } else {
+ i._text_class = 'hidden';
+ }
+
+ $scope.closings.push(i);
+ return i;
+ }).then(deferred.resolve, null, deferred.notify)});
+
+ return deferred.promise;
+ }
+ });
+
+ $scope.gridDataProvider = provider;
+
+ $scope.refresh_page = function () {
+ $scope.closings = [];
+ $timeout(function(){provider.refresh()});
+ }
+
+ $scope.org_changed = $scope.refresh_page;
+ $scope.$watch('date_filter', $scope.refresh_page);
+
+ function spawn_editor(cl, action) {
+ var deferred = $q.defer();
+ $uibModal.open({
+ templateUrl: './admin/local/actor/edit_closed_dates',
+ backdrop: 'static',
+ controller:
+ ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
+ $scope.focusMe = true;
+ $scope.args = {};
+ $scope.args.create_aec = false;
+ $scope.args.apply_to_all = false;
+ $scope.args.process_immediately = false;
+ $scope.args.type = /^[t1]/.test(cl.multi_day()) ? 'multi' : /^[t1]/.test(cl.full_day()) ? 'full' : 'detailed';
+ $scope.args.is_not_detailed = $scope.args.type == 'detailed' ? false : true;
+ $scope.args.aoucd = cl;
+ $scope.args.aec = cl.emergency_closing();
+
+ $scope.unprocessed = true;
+ if ($scope.args.aec) {
+ $scope.args.aoucd.emergency_closing($scope.args.aec.id()); // detatch for now
+ $scope.unprocessed = $scope.args.aec.process_start_time() ? false : true;
+ $scope.args.create_aec = $scope.unprocessed;;
+ }
+
+ $scope.org_unit = egCore.org.get(cl.org_unit());
+ $scope.args.start = new Date(cl.close_start());
+ $scope.args.end = new Date(cl.close_end());
+ $scope.args.reason = cl.reason();
+ $scope.is_update = action == 'update';
+
+ $scope.ok = function(args) { $uibModalInstance.close(args) }
+ $scope.cancel = function () { $uibModalInstance.dismiss() }
+
+ $scope.is_emergency = $scope.aec ? true : false;
+ $scope.check_if_emergency = function () {
+ if ($scope.args.aoucd.emergency_closing()) {
+ ngToast.danger(egCore.strings.EMERGENCY_CLOSING);
+ $scope.is_emergency = true;
+ return $scope.is_emergency;
+ }
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.org_unit.closed_date.emergency_test',
+ egCore.auth.token(), $scope.args.aoucd
+ ).then(function (res) {
+ $scope.duration_rule_count = parseInt(res);
+ if ($scope.duration_rule_count) {
+ ngToast.danger(egCore.strings.POSSIBLE_EMERGENCY_CLOSING);
+ $scope.is_emergency = true;
+ } else {
+ $scope.is_emergency = false;
+ }
+ });
+ return $scope.is_emergency;
+ }
+
+ $scope.update_org_unit = function () { $scope.args.aoucd.org_unit($scope.org_unit.id()) }
+
+ $scope.$watch('args.create_aec', function (n) {
+ if (n) {
+ if (!$scope.args.aec) $scope.args.aec = new egCore.idl.aec();
+ if (!$scope.args.aec.creator()) $scope.args.aec.creator(egCore.auth.user().id());
+ } else {
+ if (!cl.emergency_closing()) $scope.args.aec = null;
+ }
+ });
+ $scope.$watch('args.type', function (n) { $scope.args.is_not_detailed = n != 'detailed' });
+ $scope.$watch('args.start', function (n) { $scope.args.aoucd.close_start(n.toISOString()); if (n) $scope.check_if_emergency() });
+ $scope.$watch('args.end', function (n) { $scope.args.aoucd.close_end(n.toISOString()) });
+ $scope.$watch('args.reason', function (n) { $scope.args.aoucd.reason(n) });
+ }]
+ }).result.then(function(args) {
+
+ var start = args.start;
+ var end = args.end;
+
+ args.aoucd.full_day(0);
+ args.aoucd.multi_day(0);
+
+ if (args.type == 'full') {
+ args.aoucd.full_day(1);
+ end = new Date(start);
+ }
+
+ if (args.type == 'multi') {
+ args.aoucd.full_day(1);
+ args.aoucd.multi_day(1);
+ }
+
+ if (args.type == 'multi' || args.type == 'full') {
+
+ start.setHours(0);
+ start.setMinutes(0);
+ start.setSeconds(0);
+
+ end.setHours(23);
+ end.setMinutes(59);
+ end.setSeconds(59);
+ }
+
+ args.aoucd.close_start(start.toISOString());
+ args.aoucd.close_end(end.toISOString());
+
+ if (action == 'create') {
+ var new_aoucd_list = [];
+ var libraries = [args.aoucd.org_unit()];
+
+ if (args.apply_to_all)
+ libraries = egCore.org.descendants(args.aoucd.org_unit(), true);
+
+ egProgressDialog.open({
+ label : egCore.strings.CREATING_CLOSINGS,
+ value : 0,
+ max : libraries.length
+ });
+
+ function make_next () {
+ var l = libraries.shift();
+
+ if (!l) {
+ egProgressDialog.close();
+ $scope.refresh_page();
+ deferred.resolve([new_aoucd_list,args]);
+ } else {
+ args.aoucd.org_unit(l);
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.org_unit.closed.create',
+ egCore.auth.token(), args.aoucd, args.aec
+ ).then(function (new_aoucd) {
+ new_aoucd_list.push(new_aoucd);
+ make_next();
+ });
+ }
+ }
+
+ make_next();
+ } else {
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.org_unit.closed.update',
+ egCore.auth.token(), args.aoucd
+ ).then(function(new_aoucd) { deferred.resolve([new_aoucd,args]); });
+ }
+ });
+ return deferred.promise;
+ }
+
+ $scope.create_aoucd = function() {
+ var cl = new egCore.idl.aoucd();
+ cl.isnew(1);
+ cl.full_day(1);
+ cl.org_unit($scope.context_org.id());
+ cl.close_start(new Date().toISOString());
+ cl.close_end(cl.close_start());
+
+ spawn_editor(cl, 'create').then(function(content) {
+ if (content && content[0] && content[1] && content[1].process_immediately) {
+
+ function process_next () {
+ var new_cl = content[0].shift();
+
+ if (!new_cl) {
+ $scope.refresh_page();
+ } else {
+ egProgressDialog.open({label : egCore.strings.PROCESSING_EMERGENCY});
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.org_unit.closed.process_emergency',
+ egCore.auth.token(), new_cl
+ ).then(
+ function () {
+ egProgressDialog.close();
+ $scope.gridControls.refresh();
+ process_next();
+ },
+ null,
+ function (status) {
+ if (status.stage != 'start' && status.stage != 'complete') {
+ egProgressDialog.update({
+ value : status[status.stage][0],
+ max : status[status.stage][1],
+ });
+ }
+ }
+ );
+ }
+ }
+
+ process_next();
+ } else {
+ $scope.refresh_page();
+ }
+ });
+ }
+
+ $scope.update_aoucd = function(selected) {
+ if (!selected || !selected.length) return;
+
+ egCore.pcrud.retrieve('aoucd', selected[0].id, {
+ join : { "aec" : { type : "left" } },
+ flesh : 2,
+ flesh_fields : { aoucd : ['emergency_closing'], aec : ['status'] }
+ }).then(function(cl) {
+ spawn_editor(cl, 'update').then(function(content) {
+ $scope.gridControls.refresh();
+ if (content && content[0] && content[1] && content[1].process_immediately) {
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.org_unit.closed.process_emergency',
+ egCore.auth.token(), content[0]
+ );
+ }
+ });
+ });
+ }
+
+ $scope.delete_aoucd = function(selected) {
+ if (!selected || !selected.length) return;
+
+ egCore.pcrud.retrieve('aoucd', selected[0].id).then(function(cl) {
+ egConfirmDialog.open(
+ egCore.strings.CONFIRM_CLOSED_DELETE,
+ egCore.strings.CONFIRM_CLOSED_DELETE_BODY,
+ { reason : cl.reason(), org : egCore.org.get(cl.org_unit()) }
+ ).result.then(function() {
+ egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.org_unit.closed.delete',
+ egCore.auth.token(), cl
+ ).then(function() {
+ $scope.gridControls.refresh();
+ });
+ });
+ });
+ }
+
+ $scope.gridControls = {
+ activateItem : function (item) {
+ $scope.update_aoucd([item]);
+ }
+ };
+
+}])
+
});
}
+
+ if (!egAuth.user()) return $q.when();
+
var deferred = $q.defer();
ou_id = ou_id || egAuth.user().ws_ou();
var here = (ou_id == egAuth.user().ws_ou());
var default_format = 'mediumDate';
egCore.org.settings(['format.date']).then(function(set) {
- default_format = set['format.date'];
+ if (set) default_format = set['format.date'];
scope.date_format = (scope.dateFormat) ?
scope.dateFormat :
default_format;