[open-ils-commits] [GIT] Evergreen ILS branch master updated. 15c4db3b46d7c279ce5552ad55b08d3fe861bfb0

Evergreen Git git at git.evergreen-ils.org
Thu Jul 26 15:00:49 EDT 2018


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "Evergreen ILS".

The branch, master has been updated
       via  15c4db3b46d7c279ce5552ad55b08d3fe861bfb0 (commit)
       via  76ff2328fdb1ec5ef1ff19056fb5a610bdda70d8 (commit)
       via  3f249b1cc43dd59da8ac640767b74a151bed7cb5 (commit)
       via  a440cb0787455a97363eb045d4bd19a865ad19d2 (commit)
       via  71c46499d080f38c53bd3b8ee34852e973a2daf1 (commit)
       via  5caa4344f460f8496589b641854634cf799a6dca (commit)
       via  b46d4eaac2a6b023c2cde2a1642ef61cb8206548 (commit)
      from  827d0277af4405a5df1ddbbf5414c35e7250d3f2 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 15c4db3b46d7c279ce5552ad55b08d3fe861bfb0
Author: Bill Erickson <berickxx at gmail.com>
Date:   Thu Jul 26 14:40:00 2018 -0400

    LP#1766716 Stamping emergency closing DB upgrade
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql
index 5527d7d..b44ebbd 100644
--- a/Open-ILS/src/sql/Pg/002.schema.config.sql
+++ b/Open-ILS/src/sql/Pg/002.schema.config.sql
@@ -92,7 +92,7 @@ CREATE TRIGGER no_overlapping_deps
     BEFORE INSERT OR UPDATE ON config.db_patch_dependencies
     FOR EACH ROW EXECUTE PROCEDURE evergreen.array_overlap_check ('deprecates');
 
-INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1114', :eg_version); -- JBoyer/miker
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1115', :eg_version); -- miker/berick
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.emergency_closing.sql b/Open-ILS/src/sql/Pg/upgrade/1115.schema.emergency_closing.sql
similarity index 99%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.emergency_closing.sql
rename to Open-ILS/src/sql/Pg/upgrade/1115.schema.emergency_closing.sql
index f6630f2..d580601 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.emergency_closing.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1115.schema.emergency_closing.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
-SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1115', :eg_version);
 
 INSERT INTO permission.perm_list (id,code,description) VALUES ( 607, 'EMERGENCY_CLOSING', 'Create and manage Emergency Closings');
 

commit 76ff2328fdb1ec5ef1ff19056fb5a610bdda70d8
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Jul 11 16:51:59 2018 -0400

    LP#1766716: Adjust date display logic
    
    We were using the wrong date formating filter, egDueDate, and additionly the
    egOrgDateInContext filter was defaulting to the useless 'shortDate' format in
    cases where a format was not passed (such as when $root.egDateAndTimeFormat
    can't be found).  Both of these are addressed here.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/staff/admin/local/actor/closed_dates.tt2 b/Open-ILS/src/templates/staff/admin/local/actor/closed_dates.tt2
index 1c30d4e..0bb9bec 100644
--- a/Open-ILS/src/templates/staff/admin/local/actor/closed_dates.tt2
+++ b/Open-ILS/src/templates/staff/admin/local/actor/closed_dates.tt2
@@ -61,10 +61,10 @@
     <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}}
+      {{item.close_start | egOrgDateInContext:item._format: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}}
+      {{item.close_end | egOrgDateInContext:item._format: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>
diff --git a/Open-ILS/web/js/ui/default/staff/admin/local/actor/closed_dates.js b/Open-ILS/web/js/ui/default/staff/admin/local/actor/closed_dates.js
index 78da7fd..992cde4 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/local/actor/closed_dates.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/local/actor/closed_dates.js
@@ -70,6 +70,7 @@ function($scope , $q , $timeout , $location , $window , $uibModal , ngToast ,
             var e = new Date(i.close_end);
             i._duration = ((e - s) / 1000) + 1;
             i._duration = '' + i._duration + ' seconds';
+            i._format = $scope.$root.egDateAndTimeFormat;
 
             if (i.emergency_closing) {
                 var x = i.emergency_closing.status.circulations - i.emergency_closing.status.circulations_complete;
diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js
index 0368923..6916319 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -248,7 +248,7 @@ function($timeout , $parse) {
 
     function eg_context_date_filter (date, format, orgID, interval) {
         var fmt = format;
-        if (!fmt) fmt = 'shortDate';
+        if (!fmt) fmt = 'short';
 
         // if this is a simple, one-word format, and it doesn't say "Date" in it...
         if (['short','medium','long','full'].filter(function(x){return fmt == x}).length > 0 && interval) {

commit 3f249b1cc43dd59da8ac640767b74a151bed7cb5
Author: Bill Erickson <berickxx at gmail.com>
Date:   Mon Jul 9 11:55:07 2018 -0400

    LP#1766716 Closed dates API emergency filter fix
    
    Fix closed dates emergency => emergency_closing DB column name thinko.
    
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/ClosedDates.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/ClosedDates.pm
index 2c3722c..bc87abe 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/ClosedDates.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/ClosedDates.pm
@@ -141,7 +141,7 @@ sub fetch_dates {
         [{ 
             '-or' => [
                 { close_start => { ">=" => $start }, close_end => { "<=" => $end } },
-                { emergency => { "!=" => undef }, "+aec" => { process_end_time => { "=" => undef } } }
+                { emergency_closing => { "!=" => undef }, "+aec" => { process_end_time => { "=" => undef } } }
             ],
             org_unit => $org,
         }, {flesh        => 2,

commit a440cb0787455a97363eb045d4bd19a865ad19d2
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed May 9 12:28:00 2018 -0400

    LP#1766716: (follow-up) expand CSS imports for now
    
    The patches for bug 1739803 ended up removing the minification
    of the CSS and constructing evergreen-staff-client-deps.0.0.1.min.css,
    so consequently setting EXPAND_WEB_IMPORTS to 0 currently does not
    work as expected. Unless that regression (if it is one) gets
    fixed, stick with EXPAND_WEB_IMPORTS = 1 for now.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/staff/config.tt2 b/Open-ILS/src/templates/staff/config.tt2
index 1253912..5d8b172 100644
--- a/Open-ILS/src/templates/staff/config.tt2
+++ b/Open-ILS/src/templates/staff/config.tt2
@@ -5,7 +5,7 @@ EVERGREEN_VERSION='0.0.1'
 
 # create script / css refs to individual files instead of using
 # compressed build files.  Use this for development and debugging.
-EXPAND_WEB_IMPORTS = 0; 
+EXPAND_WEB_IMPORTS = 1; 
 
 # 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';

commit 71c46499d080f38c53bd3b8ee34852e973a2daf1
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Apr 24 16:45:31 2018 -0400

    LP#1766716: Release notes
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/EmergencyClosingHandler.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/EmergencyClosingHandler.adoc
new file mode 100644
index 0000000..6fa487b
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Circulation/EmergencyClosingHandler.adoc
@@ -0,0 +1,22 @@
+
+Emergency Closing Handler
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Staff are provided with interfaces and mechanisms to create library closings that, in addition to affecting future circulation and booking due dates, and hold shelf expirations, will automatically move existing circulation and booking due dates and hold shelf expiration times. This new functionality is conceptually described as Emergency Closings and business logic implementing it as the Emergency Closing Handler. It contains additions and adjustments to the user interface, business logic, and database layers. Access to this functionality is available through the Closed Dates Editor interface in the staff client which has been ported to AngularJS.
+
+Overview
+++++++++
+
+This development has created new business logic code to inspect, in real time, existing circulation, booking, and hold records, and modify such date and time stamps so that the circulation, booking, or hold will end in the same state it would have if the closing had existed at the time the circulation or booking occurred, or the hold was placed and captured. Of specific note, hourly loans will have their due date adjusted to be the end of the day following the closing.
+
+When the Emergency Closing is saved, any fines accrued during the closing may be voided, as settings dictate, with the exception of circulations that have been marked as LOST or LONG OVERDUE. That is, even for LOST and LONG OVERDUE circulations with due dates that fall within the Emergency Closing, no fine adjustment will be applied. Emergency Closing processing is permanent, and cannot be rolled back.
+
+This functionality is explicitly initiated by staff action. If staff do not request an Emergency Closing, existing circulations, bookings, and holds will not be processed and adjusted. However, if staff request any Closing that starts nearer in time than the length of the longest circulation duration configured for use in the Evergreen instance they will be prompted with the option to create the closing as an Emergency Closing.
+
+Action/Trigger hooks have been created for circulations and bookings that are adjusted by the Emergency Closing Handler. These will facilitate the creation of notifications to patrons that the due date has changed and to alert them to potential changes in accrued fines.
+
+Booking start dates are explicitly ignored in this implementation. Because an Emergency Closing is, by its nature, an unexpected event, it will be up to staff to address any bookings which intersect with a new Emergency Closings. Reports can be used to identify booking start dates that overlap with a closing and that may require staff intervention.
+
+Staff requsting and Emergency Closing must have the new EMERGENCY_CLOSING permission.
+Some text describing the feature.
+

commit 5caa4344f460f8496589b641854634cf799a6dca
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Feb 27 16:49:57 2018 -0500

    LP#1766716: Emergency Closing handler
    
    New Feature Summary
    
    Staff are provided with interfaces and mechanisms to create library closings
    that, in addition to affecting future circulation and booking due dates, and
    hold shelf expirations, will automatically move existing circulation and
    booking due dates and hold shelf expiration times.  This new functionality is
    conceptually described as Emergency Closings and business logic implementing it
    as the Emergency Closing Handler. It contains additions and adjustments to the
    user interface, business logic, and database layers.  Access to this
    functionality is available through the Closed Dates Editor interface in the
    staff client which has been ported to AngularJS.
    
    Overview
    
    This development has created new business logic code to inspect, in real time,
    existing circulation, booking, and hold records, and modify such date and time
    stamps so that the circulation, booking, or hold will end in the same state it
    would have if the closing had existed at the time the circulation or booking
    occurred, or the hold was placed and captured.  Of specific note, hourly loans
    will have their due date adjusted to be the end of the day following the
    closing.
    
    When the Emergency Closing is saved, any fines accrued during the closing may
    be voided, as settings dictate, with the exception of circulations that have
    been marked as LOST or LONG OVERDUE.  That is, even for LOST and LONG OVERDUE
    circulations with due dates that fall within the Emergency Closing, no fine
    adjustment will be applied.  Emergency Closing processing is permanent, and
    cannot be rolled back.
    
    This functionality is explicitly initiated by staff action.  If staff do not
    request an Emergency Closing, existing circulations, bookings, and holds will
    not be processed and adjusted.  However, if staff request any Closing that
    starts nearer in time than the length of the longest circulation duration
    configured for use in the Evergreen instance they will be prompted with the
    option to create the closing as an Emergency Closing.
    
    Action/Trigger hooks have been created for circulations and bookings that are
    adjusted by the Emergency Closing Handler.  These will facilitate the creation
    of notifications to patrons that the due date has changed and to alert them to
    potential changes in accrued fines.
    
    Booking start dates are explicitly ignored in this implementation.  Because an
    Emergency Closing is, by its nature, an unexpected event, it will be up to
    staff to address any bookings which intersect with a new Emergency Closings.
    Reports can be used to identify booking start dates that overlap with a closing
    and that may require staff intervention.
    
    Staff requsting and Emergency Closing must have the new EMERGENCY_CLOSING
    permission.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index b272b9f..aae6422 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -3275,6 +3275,145 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
             </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" />
@@ -3284,9 +3423,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 			<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>
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/ClosedDates.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/ClosedDates.pm
index 46337f5..2c3722c 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/ClosedDates.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor/ClosedDates.pm
@@ -2,10 +2,121 @@ package OpenILS::Application::Actor::ClosedDates;
 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',
@@ -27,11 +138,18 @@ sub fetch_dates {
     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 ];
@@ -53,6 +171,9 @@ sub fetch_date {
     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;
 }
 
@@ -70,6 +191,10 @@ sub delete_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;
@@ -89,7 +214,7 @@ __PACKAGE__->register_method(
 );
 
 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;
@@ -97,11 +222,23 @@ sub create_date {
     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;
 }
@@ -124,13 +261,69 @@ sub edit_date {
     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".
 }
 
 
@@ -142,6 +335,7 @@ __PACKAGE__->register_method(
         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
     /
diff --git a/Open-ILS/src/sql/Pg/096.schema.emergency_closing.sql b/Open-ILS/src/sql/Pg/096.schema.emergency_closing.sql
new file mode 100644
index 0000000..88abf35
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/096.schema.emergency_closing.sql
@@ -0,0 +1,457 @@
+/*
+ * Copyright (C) 2018  Equinox Open Library Initiative Inc.
+ * Mike Rylander <mrylander at 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;
+
diff --git a/Open-ILS/src/sql/Pg/400.schema.action_trigger.sql b/Open-ILS/src/sql/Pg/400.schema.action_trigger.sql
index d9f292e..50fa9d6 100644
--- a/Open-ILS/src/sql/Pg/400.schema.action_trigger.sql
+++ b/Open-ILS/src/sql/Pg/400.schema.action_trigger.sql
@@ -54,6 +54,9 @@ INSERT INTO action_trigger.hook (key,core_type,description) VALUES ('format.po.p
 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
 
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 7f7687d..4213a0c 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -1903,7 +1903,9 @@ INSERT INTO permission.perm_list ( id, code, description ) VALUES
  ( 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);
diff --git a/Open-ILS/src/sql/Pg/sql_file_manifest b/Open-ILS/src/sql/Pg/sql_file_manifest
index 6e01622..97c92a0 100644
--- a/Open-ILS/src/sql/Pg/sql_file_manifest
+++ b/Open-ILS/src/sql/Pg/sql_file_manifest
@@ -29,6 +29,8 @@ FTS_CONFIG_FILE
 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
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.emergency_closing.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.emergency_closing.sql
new file mode 100644
index 0000000..f6630f2
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.emergency_closing.sql
@@ -0,0 +1,449 @@
+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;
+
diff --git a/Open-ILS/src/templates/staff/admin/local/actor/closed_dates.tt2 b/Open-ILS/src/templates/staff/admin/local/actor/closed_dates.tt2
new file mode 100644
index 0000000..1c30d4e
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/local/actor/closed_dates.tt2
@@ -0,0 +1,87 @@
+[%
+  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 %]
diff --git a/Open-ILS/src/templates/staff/admin/local/actor/edit_closed_dates.tt2 b/Open-ILS/src/templates/staff/admin/local/actor/edit_closed_dates.tt2
new file mode 100644
index 0000000..e2579d2
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/local/actor/edit_closed_dates.tt2
@@ -0,0 +1,116 @@
+<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>
diff --git a/Open-ILS/src/templates/staff/config.tt2 b/Open-ILS/src/templates/staff/config.tt2
index 5d8b172..1253912 100644
--- a/Open-ILS/src/templates/staff/config.tt2
+++ b/Open-ILS/src/templates/staff/config.tt2
@@ -5,7 +5,7 @@ EVERGREEN_VERSION='0.0.1'
 
 # 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';
diff --git a/Open-ILS/src/templates/staff/share/t_datetime.tt2 b/Open-ILS/src/templates/staff/share/t_datetime.tt2
index 903214d..1f505db 100644
--- a/Open-ILS/src/templates/staff/share/t_datetime.tt2
+++ b/Open-ILS/src/templates/staff/share/t_datetime.tt2
@@ -29,15 +29,15 @@
       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>
 
diff --git a/Open-ILS/web/js/ui/default/staff/admin/local/actor/closed_dates.js b/Open-ILS/web/js/ui/default/staff/admin/local/actor/closed_dates.js
new file mode 100644
index 0000000..78da7fd
--- /dev/null
+++ b/Open-ILS/web/js/ui/default/staff/admin/local/actor/closed_dates.js
@@ -0,0 +1,352 @@
+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]);
+        }
+    };
+
+}])
+
diff --git a/Open-ILS/web/js/ui/default/staff/services/org.js b/Open-ILS/web/js/ui/default/staff/services/org.js
index 93efc44..84731f4 100644
--- a/Open-ILS/web/js/ui/default/staff/services/org.js
+++ b/Open-ILS/web/js/ui/default/staff/services/org.js
@@ -121,6 +121,9 @@ function($q,  egEnv,  egAuth,  egNet , $injector) {
                 });
         }
 
+
+        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());
diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js
index 3562e4a..0368923 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -1239,7 +1239,7 @@ function($uibModal , $interpolate , egCore) {
 
                 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;

commit b46d4eaac2a6b023c2cde2a1642ef61cb8206548
Author: Mike Rylander <mrylander at gmail.com>
Date:   Fri Mar 16 09:52:50 2018 -0400

    LP#1766716 Supporting functionality added to date/time picker and progress dialog
    
    Here the date/time picker is given the ability to dynamically hide the time
    picking component when show-time-picker is in effect.  This allows an
    interface to use the time picker based on ephemeral state information and to
    hide it when not useful.
    
    Additionally, the progress dialog now takes an optional label property that
    can be passed to both open() and update(), so that textual information can
    be provided along with the numeric and visual progress data.
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Bill Erickson <berickxx at gmail.com>

diff --git a/Open-ILS/src/templates/staff/share/t_datetime.tt2 b/Open-ILS/src/templates/staff/share/t_datetime.tt2
index 492fa1c..903214d 100644
--- a/Open-ILS/src/templates/staff/share/t_datetime.tt2
+++ b/Open-ILS/src/templates/staff/share/t_datetime.tt2
@@ -30,7 +30,8 @@
   <div>
     <span>
       <uib-timepicker
-        ng-show="showTimePicker"
+        ng-if="showTimePicker"
+        ng-hide="hideTimePicker"
         ng-model="ngModel"
         ng-disabled="ngDisabled"
         ng-required="ngRequired"
diff --git a/Open-ILS/src/templates/staff/share/t_progress_dialog.tt2 b/Open-ILS/src/templates/staff/share/t_progress_dialog.tt2
index 2df60d2..6b37845 100644
--- a/Open-ILS/src/templates/staff/share/t_progress_dialog.tt2
+++ b/Open-ILS/src/templates/staff/share/t_progress_dialog.tt2
@@ -7,6 +7,12 @@
   <div class="modal-body">
     <div class="row eg-modal-progress">
 
+      <div ng-if="data.label">
+        <div class="col-md-12">
+          <h2>{{data.label}}</h2>
+        </div>
+      </div>
+
       <div ng-if="data.hasvalue() && data.hasmax()">
         <!-- determinate progress bar.  shows max/value progress -->
         <div class="col-md-10">
diff --git a/Open-ILS/web/js/ui/default/staff/services/ui.js b/Open-ILS/web/js/ui/default/staff/services/ui.js
index 95a1f48..3562e4a 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -412,6 +412,8 @@ function($timeout , $parse) {
             egProgressData.max = args.max;
         if (args.value != undefined) 
             egProgressData.value = args.value;
+        if (args.label != undefined) 
+            egProgressData.label = args.label;
     }
 
     // Increment the current value.  If no amount is specified,
@@ -1199,6 +1201,7 @@ function($uibModal , $interpolate , egCore) {
                 ngDisabled : '=',
                 ngRequired : '=',
                 hideDatePicker : '=',
+                hideTimePicker : '=?',
                 dateFormat : '=?',
                 outOfRange : '=?',
                 focusMe : '=?'

-----------------------------------------------------------------------

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |  141 ++++++
 .../lib/OpenILS/Application/Actor/ClosedDates.pm   |  208 +++++++++-
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 .../src/sql/Pg/096.schema.emergency_closing.sql    |  457 ++++++++++++++++++++
 Open-ILS/src/sql/Pg/400.schema.action_trigger.sql  |    3 +
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |    4 +-
 Open-ILS/src/sql/Pg/sql_file_manifest              |    2 +
 .../Pg/upgrade/1115.schema.emergency_closing.sql   |  449 +++++++++++++++++++
 .../staff/admin/local/actor/closed_dates.tt2       |   87 ++++
 .../staff/admin/local/actor/edit_closed_dates.tt2  |  116 +++++
 Open-ILS/src/templates/staff/share/t_datetime.tt2  |    7 +-
 .../templates/staff/share/t_progress_dialog.tt2    |    6 +
 .../staff/admin/local/actor/closed_dates.js        |  353 +++++++++++++++
 Open-ILS/web/js/ui/default/staff/services/org.js   |    3 +
 Open-ILS/web/js/ui/default/staff/services/ui.js    |    7 +-
 .../Circulation/EmergencyClosingHandler.adoc       |   22 +
 16 files changed, 1853 insertions(+), 14 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/096.schema.emergency_closing.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1115.schema.emergency_closing.sql
 create mode 100644 Open-ILS/src/templates/staff/admin/local/actor/closed_dates.tt2
 create mode 100644 Open-ILS/src/templates/staff/admin/local/actor/edit_closed_dates.tt2
 create mode 100644 Open-ILS/web/js/ui/default/staff/admin/local/actor/closed_dates.js
 create mode 100644 docs/RELEASE_NOTES_NEXT/Circulation/EmergencyClosingHandler.adoc


hooks/post-receive
-- 
Evergreen ILS


More information about the open-ils-commits mailing list