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

Evergreen Git git at git.evergreen-ils.org
Wed Feb 28 10:35:00 EST 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  f9159c9de6c2e90c8983d02854c0a3831daf9b1a (commit)
       via  452448300f976b5230691e3eeac4127decf9f0e7 (commit)
       via  f736176ddae994baefa62c8427f3f441f585e39e (commit)
       via  8eea84211b439453b6423c0415a7ea099ddbcdce (commit)
       via  290a336fde9810cfd1be0df364634d629c77dc67 (commit)
       via  3724a2cf6b8ba4e6191ed19b48102b22d2766c2a (commit)
       via  a6185f08b2598080bbd164c3b2f0107ab622804a (commit)
       via  c0dcdd11ce6402ac35d7b561164acd58b28beb83 (commit)
       via  c9fb95e59db76815e05ad124a4290439df6f8819 (commit)
       via  5b75e9b01c94c861a5ede9e76f34ac10ec3a4a8f (commit)
       via  619e9059cd6980a7cac987cc33a8618540f7ea4c (commit)
       via  3784351aed2356b70dc82116d2c1dfff0c6894b7 (commit)
       via  85b5d3af913839515a4f20bc732cbbb23ae2d963 (commit)
       via  56679c9b7b96c818935b9004c41a76587e679db9 (commit)
       via  3e548a2893f8521278f6bdf331d35f14e1c17a75 (commit)
       via  ae68e4ff7d79d78b45a4c164305b7994982758f5 (commit)
       via  d29a5ffb17974e881db93caf1960c6fce37e1284 (commit)
       via  6f1f2a4faf180bd7861bfd0ac349c85d774d4562 (commit)
      from  87846cfc29f7aa3de474ee31db41eafd7918fad0 (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 f9159c9de6c2e90c8983d02854c0a3831daf9b1a
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Feb 28 10:33:50 2018 -0500

    Stamping upgrade scripts for new copy alerts
    
    Signed-off-by: Mike Rylander <mrylander 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 2a72ffe..5dd5f78 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 ('1094', :eg_version); -- miker/kmlussier
+INSERT INTO config.upgrade_log (version, applied_to) VALUES ('1098', :eg_version); -- gmcharlt/miker
 
 CREATE TABLE config.bib_source (
 	id		SERIAL	PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql b/Open-ILS/src/sql/Pg/upgrade/1095.schema.copy_alerts.sql
similarity index 98%
rename from Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql
rename to Open-ILS/src/sql/Pg/upgrade/1095.schema.copy_alerts.sql
index 993b679..ceaba70 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1095.schema.copy_alerts.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('1095', :eg_version);
+
 CREATE OR REPLACE FUNCTION asset.copy_state (cid BIGINT) RETURNS TEXT AS $$
 DECLARE
     last_circ_stop	TEXT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql b/Open-ILS/src/sql/Pg/upgrade/1096.data.stock_copy_alert_types.sql
similarity index 98%
rename from Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql
rename to Open-ILS/src/sql/Pg/upgrade/1096.data.stock_copy_alert_types.sql
index ae79a9c..4a91997 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1096.data.stock_copy_alert_types.sql
@@ -1,5 +1,7 @@
 BEGIN;
 
+SELECT evergreen.upgrade_deps_block_check('1096', :eg_version);
+
 -- staff-usable alert types with no location awareness
 INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew)
 VALUES (1, 1, TRUE, 'Normal checkout', 'NORMAL', 'CHECKOUT', FALSE);
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.data.yaous_for_open_circ_exists_fine_handling.sql b/Open-ILS/src/sql/Pg/upgrade/1097.data.yaous_for_open_circ_exists_fine_handling.sql
similarity index 95%
rename from Open-ILS/src/sql/Pg/upgrade/ZZZZ.data.yaous_for_open_circ_exists_fine_handling.sql
rename to Open-ILS/src/sql/Pg/upgrade/1097.data.yaous_for_open_circ_exists_fine_handling.sql
index 4235527..d3a706d 100644
--- a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.data.yaous_for_open_circ_exists_fine_handling.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1097.data.yaous_for_open_circ_exists_fine_handling.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
---- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1097', :eg_version);
 
 INSERT INTO config.org_unit_setting_type
     (name, grp, label, description, datatype)
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZA.data.move_legacy_copy_alerts.sql b/Open-ILS/src/sql/Pg/upgrade/1098.data.move_legacy_copy_alerts.sql
similarity index 90%
rename from Open-ILS/src/sql/Pg/upgrade/ZZZA.data.move_legacy_copy_alerts.sql
rename to Open-ILS/src/sql/Pg/upgrade/1098.data.move_legacy_copy_alerts.sql
index f430cbf..fa7fa0c 100644
--- a/Open-ILS/src/sql/Pg/upgrade/ZZZA.data.move_legacy_copy_alerts.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/1098.data.move_legacy_copy_alerts.sql
@@ -1,6 +1,6 @@
 BEGIN;
 
---- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+SELECT evergreen.upgrade_deps_block_check('1098', :eg_version);
 
 \qecho Copying copy alert messages to normal checkout copy alerts...
 INSERT INTO asset.copy_alert (alert_type, copy, note, create_staff)

commit 452448300f976b5230691e3eeac4127decf9f0e7
Author: Mike Rylander <mrylander at gmail.com>
Date:   Wed Feb 28 10:17:10 2018 -0500

    LP#1676608: Stop hold capture when an alert-force copy status change is requested
    
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
index f191884..f749b92 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
@@ -1690,6 +1690,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
                 ok : function(the_next_status) {
                         if (the_next_status !== null) {
                             params.next_copy_status = [ the_next_status ];
+                            params.capture = 'nocapture';
                         }
                      },
                 cancel : function() {}

commit f736176ddae994baefa62c8427f3f441f585e39e
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Tue Feb 27 17:23:22 2018 -0500

    LP#1676608: don't sound the klaxon for unusual copy statuses during checkin
    
    During a successful checkin, play the success sound if an
    unexpected copy status is noted in the success message -- it
    may have been set via a copy alert.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
index f349f6c..f191884 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
@@ -1493,8 +1493,8 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
                         return $q.when(final_resp);
 
                     default:
-                        egCore.audio.play('error.checkin.unknown');
-                        console.error('Unhandled checkin copy status: ' 
+                        egCore.audio.play('success.checkin');
+                        console.debug('Unusual checkin copy status (may have been set via copy alert): '
                             + copy.status().id() + ' : ' + copy.status().name());
                         return $q.when(final_resp);
                 }

commit 8eea84211b439453b6423c0415a7ea099ddbcdce
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Fri Feb 23 16:54:51 2018 -0500

    LP#1676608: inject copy alert dialog in two more places
    
    The copy alert dialog (if an item has relevant copy alerts)
    is now injected into the dialog box series when handling
    checkouts of items that are in transit or where the patron
    record has overridable conditions that would block the checkout.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
index 91ad8fb..f349f6c 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
@@ -365,7 +365,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
 
         switch(evt[0].textcode) {
             case 'COPY_NOT_AVAILABLE':
-                return service.copy_not_avail_dialog(evt[0], params, options);
+                return service.copy_not_avail_dialog(evt, params, options);
             case 'COPY_ALERT_MESSAGE':
                 return service.copy_alert_dialog(evt[0], params, options, 'checkout');
             default: 
@@ -718,6 +718,13 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
         if (!angular.isArray(evt)) evt = [evt];
 
         egCore.audio.play('warning.circ.event_override');
+        var copy_alert = evt.filter(function(e) {
+            return e.textcode == 'COPY_ALERT_MESSAGE';
+        });
+        evt = evt.filter(function(e) {
+            return e.textcode !== 'COPY_ALERT_MESSAGE';
+        });
+
         return $uibModal.open({
             templateUrl: './circ/share/t_event_override_dialog',
             backdrop: 'static',
@@ -740,6 +747,10 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
             function() {
                 options.override = true;
 
+                if (copy_alert.length > 0) {
+                    return service.copy_alert_dialog(copy_alert, params, options, action);
+                }
+
                 if (action == 'checkin') {
                     return service.checkin(params, options);
                 }
@@ -756,7 +767,16 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
     }
 
     service.copy_not_avail_dialog = function(evt, params, options) {
-        if (angular.isArray(evt)) evt = evt[0];
+        if (!angular.isArray(evt)) evt = [evt];
+
+        var copy_alert = evt.filter(function(e) {
+            return e.textcode == 'COPY_ALERT_MESSAGE';
+        });
+        evt = evt.filter(function(e) {
+            return e.textcode !== 'COPY_ALERT_MESSAGE';
+        });
+        evt = evt[0];
+
         return $uibModal.open({
             templateUrl: './circ/share/t_copy_not_avail_dialog',
             backdrop: 'static',
@@ -776,6 +796,11 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAl
         }).result.then(
             function() {
                 options.override = true;
+
+                if (copy_alert.length > 0) {
+                    return service.copy_alert_dialog(copy_alert, params, options, 'checkout');
+                }
+
                 return service.checkout(params, options);
             }
         );

commit 290a336fde9810cfd1be0df364634d629c77dc67
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Feb 22 17:02:11 2018 -0500

    LP#1676608: provide DB update script to convert legacy copy alert messages
    
    Legacy copy alert messages are moved to new normal checkout and
    normal checkin copy alerts. This patch also converts the foreign
    key relationship from asset.copy_alert to asset.copy to a "fake"
    one using a constraint trigger.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/sql/Pg/040.schema.asset.sql b/Open-ILS/src/sql/Pg/040.schema.asset.sql
index d17dcfa..24dda3c 100644
--- a/Open-ILS/src/sql/Pg/040.schema.asset.sql
+++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql
@@ -1065,7 +1065,7 @@ CREATE TABLE actor.copy_alert_suppress (
 CREATE TABLE asset.copy_alert (
     id      bigserial   primary key,
     alert_type  int     not null references config.copy_alert_type (id) on delete cascade,
-    copy        bigint  not null references asset.copy (id) on delete cascade,
+    copy        bigint  not null,
     temp        bool    not null default false,
     create_time timestamptz not null default now(),
     create_staff    bigint  not null references actor.usr (id) on delete set null,
diff --git a/Open-ILS/src/sql/Pg/800.fkeys.sql b/Open-ILS/src/sql/Pg/800.fkeys.sql
index 5835ee3..c396119 100644
--- a/Open-ILS/src/sql/Pg/800.fkeys.sql
+++ b/Open-ILS/src/sql/Pg/800.fkeys.sql
@@ -157,6 +157,22 @@ BEGIN
 END;
 $f$ LANGUAGE PLPGSQL VOLATILE COST 50;
 
+CREATE OR REPLACE FUNCTION evergreen.asset_copy_alert_copy_inh_fkey() RETURNS TRIGGER AS $f$
+BEGIN
+        PERFORM 1 FROM asset.copy WHERE id = NEW.copy;
+        IF NOT FOUND THEN
+                RAISE foreign_key_violation USING MESSAGE = FORMAT(
+                        $$Referenced asset.copy id not found, copy:%s$$, NEW.copy
+                );
+        END IF;
+        RETURN NEW;
+END;
+$f$ LANGUAGE PLPGSQL VOLATILE COST 50;
+
+CREATE CONSTRAINT TRIGGER inherit_asset_copy_alert_copy_fkey
+        AFTER UPDATE OR INSERT ON asset.copy_alert
+        DEFERRABLE FOR EACH ROW EXECUTE PROCEDURE evergreen.asset_copy_alert_copy_inh_fkey();
+
 CREATE CONSTRAINT TRIGGER inherit_asset_copy_tag_copy_map_copy_fkey
         AFTER UPDATE OR INSERT ON asset.copy_tag_copy_map
         DEFERRABLE FOR EACH ROW EXECUTE PROCEDURE evergreen.asset_copy_tag_copy_map_copy_inh_fkey();
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 44dba2d..ea80115 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -17211,9 +17211,9 @@ VALUES (
 
 -- staff-usable alert types with no location awareness
 INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew)
-VALUES (1, 1, FALSE, 'Normal checkout', 'NORMAL', 'CHECKOUT', FALSE);
+VALUES (1, 1, TRUE, 'Normal checkout', 'NORMAL', 'CHECKOUT', FALSE);
 INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew)
-VALUES (2, 1, FALSE, 'Normal checkin', 'NORMAL', 'CHECKIN', FALSE);
+VALUES (2, 1, TRUE, 'Normal checkin', 'NORMAL', 'CHECKIN', FALSE);
 INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew)
 VALUES (3, 1, FALSE, 'Normal renewal', 'NORMAL', 'CHECKIN', TRUE);
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql
index db4cd25..993b679 100644
--- a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql
@@ -69,6 +69,22 @@ CREATE TABLE config.copy_alert_type (
 );
 SELECT SETVAL('config.copy_alert_type_id_seq'::TEXT, 100);
 
+CREATE OR REPLACE FUNCTION evergreen.asset_copy_alert_copy_inh_fkey() RETURNS TRIGGER AS $f$
+BEGIN
+        PERFORM 1 FROM asset.copy WHERE id = NEW.copy;
+        IF NOT FOUND THEN
+                RAISE foreign_key_violation USING MESSAGE = FORMAT(
+                        $$Referenced asset.copy id not found, copy:%s$$, NEW.copy
+                );
+        END IF;
+        RETURN NEW;
+END;
+$f$ LANGUAGE PLPGSQL VOLATILE COST 50;
+
+CREATE CONSTRAINT TRIGGER inherit_asset_copy_alert_copy_fkey
+        AFTER UPDATE OR INSERT ON asset.copy_alert
+        DEFERRABLE FOR EACH ROW EXECUTE PROCEDURE evergreen.asset_copy_alert_copy_inh_fkey();
+
 CREATE TABLE actor.copy_alert_suppress (
     id          serial primary key,
     org         int not null references actor.org_unit (id) on delete cascade,
@@ -78,7 +94,7 @@ CREATE TABLE actor.copy_alert_suppress (
 CREATE TABLE asset.copy_alert (
     id      bigserial   primary key,
     alert_type  int     not null references config.copy_alert_type (id) on delete cascade,
-    copy        bigint  not null references asset.copy (id) on delete cascade,
+    copy        bigint  not null,
     temp        bool    not null default false,
     create_time timestamptz not null default now(),
     create_staff    bigint  not null references actor.usr (id) on delete set null,
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql
index cc09016..ae79a9c 100644
--- a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql
+++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql
@@ -2,9 +2,9 @@ BEGIN;
 
 -- staff-usable alert types with no location awareness
 INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew)
-VALUES (1, 1, FALSE, 'Normal checkout', 'NORMAL', 'CHECKOUT', FALSE);
+VALUES (1, 1, TRUE, 'Normal checkout', 'NORMAL', 'CHECKOUT', FALSE);
 INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew)
-VALUES (2, 1, FALSE, 'Normal checkin', 'NORMAL', 'CHECKIN', FALSE);
+VALUES (2, 1, TRUE, 'Normal checkin', 'NORMAL', 'CHECKIN', FALSE);
 INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew)
 VALUES (3, 1, FALSE, 'Normal renewal', 'NORMAL', 'CHECKIN', TRUE);
 
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZA.data.move_legacy_copy_alerts.sql b/Open-ILS/src/sql/Pg/upgrade/ZZZA.data.move_legacy_copy_alerts.sql
new file mode 100644
index 0000000..f430cbf
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/ZZZA.data.move_legacy_copy_alerts.sql
@@ -0,0 +1,23 @@
+BEGIN;
+
+--- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+\qecho Copying copy alert messages to normal checkout copy alerts...
+INSERT INTO asset.copy_alert (alert_type, copy, note, create_staff)
+SELECT 1, id, alert_message, 1
+FROM asset.copy
+WHERE alert_message IS NOT NULL
+AND   alert_message <> '';
+
+\qecho Copying copy alert messages to normal checkin copy alerts...
+INSERT INTO asset.copy_alert (alert_type, copy, note, create_staff)
+SELECT 2, id, alert_message, 1
+FROM asset.copy
+WHERE alert_message IS NOT NULL
+AND   alert_message <> '';
+
+\qecho Clearing legacy copy alert field; this may take a while
+UPDATE asset.copy SET alert_message = NULL
+WHERE alert_message IS NOT NULL;
+
+COMMIT;

commit 3724a2cf6b8ba4e6191ed19b48102b22d2766c2a
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Feb 22 16:34:15 2018 -0500

    LP#1676608: tweak defaults for alerts in vol/copy editor
    
    Changed the defaults key for new-style copy alerts to be the same
    as what was used for legacy copy alert messages; this means that
    users who set editor defaults prior to upgrading are more likely
    to have the Copy Alerts button be active out of the box.
    
    Also moved the location of Add/Edit Copy Alerts in the defaults
    tab.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
index 0af5072..5bdf826 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
@@ -385,7 +385,7 @@
                     </button>
                     <button
                       class="btn btn-default"
-                      ng-disabled="!defaults.copy_alerts"
+                      ng-disabled="!defaults.attributes.alerts"
                       ng-click="copy_alerts_dialog(workingGridControls.selectedItems())"
                       type="button">
                         [% l('Copy Alerts') %]
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
index d1c1f0e..9430b6c 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
@@ -103,7 +103,7 @@
                 <div class="col-xs-6">
                     <label>
                         <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.alerts"/>
-                        [% l('Alerts') %]
+                        [% l('Add/Edit Copy Alerts') %]
                     </label>
                 </div>
             </div>
@@ -310,14 +310,6 @@
                 </div>
             </div>
 
-            <div class="row">
-                <div class="col-xs-6">
-                    <label>
-                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.copy_alerts"/>
-                        [% l('Add/Edit Copy Alerts') %]
-                    </label>
-                </div>
-            </div>
         </div>
 
         <div class="col-md-4">
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index b15c551..dcf9792 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -926,7 +926,6 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         statcats : true,
         copy_notes : true,
         copy_tags : true,
-        copy_alerts : true,
         attributes : {
             status : true,
             loan_duration : true,
@@ -946,7 +945,8 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             location : true,
             holdable : true,
             age_protect : true,
-            floating : true
+            floating : true,
+            alerts : true
         }
     };
 

commit a6185f08b2598080bbd164c3b2f0107ab622804a
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Feb 22 16:17:43 2018 -0500

    LP#1676608: remove legacy copy alerts field from vol/copy editor
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
index 3d60493..0af5072 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
@@ -98,7 +98,7 @@
             <b>[% l('Status') %]</b>
         </div>
         <div class="col-md-4">
-            <b>[% l('Copy Alert') %]</b>
+            <b>[% l('Statistical Categories') %]</b>
         </div>
     </div>
 
@@ -138,7 +138,6 @@
                 <div class="col-md-6">
                     <b>[% l('Reference?') %]</b>
                 </div>
-                
             </div>
 
             <div class="row">
@@ -422,33 +421,13 @@
         </div>
 
         <div class="col-md-4">
-            
-            <div class="row" >
-                <div class="col-xs-12">
-                    <div class="row">
-                        <div class="nullable col-xs-12">
-                            <input class="form-control" type="text" ng-model="working.alert_message"
-                                ng-disabled="!defaults.attributes.alert_message" placeholder="Alert message" />
-                        </div>
-                    </div>
-                </div>
-            </div>
-
-            <div class="row pad-vert"></div>
             <div class="row">
                 <div class="col-xs-12">
-                    <div class="row bg-info">
-                        <div class="col-xs-12">
-                            <b>Statistical Categories</b>
-                        </div>
-                    </div>
-                    <div class="row">
-			    <select class="form-control" ng-disabled="!defaults.statcats"
-				ng-model="working.statcat_filter"
-				ng-options="o.id() as o.shortname() for o in statcat_filter_list">
-			      <option value="">[% l('Filter by Library') %]</option>
-			    </select>
-                    </div>
+                    <select class="form-control" ng-disabled="!defaults.statcats"
+                        ng-model="working.statcat_filter"
+                        ng-options="o.id() as o.shortname() for o in statcat_filter_list">
+                      <option value="">[% l('Filter by Library') %]</option>
+                    </select>
                 </div>
             </div>
 
@@ -474,7 +453,6 @@
                     </div>
                 </div>
             </div>
-
         </div>
 
     </div>
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
index 77316e6..d1c1f0e 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
@@ -102,9 +102,8 @@
                 </div>
                 <div class="col-xs-6">
                     <label>
-                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.alert_message"/>
-                        <!-- <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.alerts"/> -->
-                        [% l('Alert Message') %]
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.attributes.alerts"/>
+                        [% l('Alerts') %]
                     </label>
                 </div>
             </div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 03cde64..b15c551 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -946,8 +946,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             location : true,
             holdable : true,
             age_protect : true,
-            floating : true,
-            alert_message : true
+            floating : true
         }
     };
 
@@ -1746,7 +1745,6 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         createSimpleUpdateWatcher('mint_condition');
         createSimpleUpdateWatcher('opac_visible');
         createSimpleUpdateWatcher('ref');
-        createSimpleUpdateWatcher('alert_message');
 
         $scope.saveCompletedCopies = function (and_exit) {
             var cnHash = {};
@@ -2534,7 +2532,6 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                 createSimpleUpdateWatcher('mint_condition');
                 createSimpleUpdateWatcher('opac_visible');
                 createSimpleUpdateWatcher('ref');
-                createSimpleUpdateWatcher('alert_message');
 
                 $scope.suffix_list = [];
                 itemSvc.get_suffixes(egCore.auth.user().ws_ou()).then(function(list){

commit c0dcdd11ce6402ac35d7b561164acd58b28beb83
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Feb 22 16:07:44 2018 -0500

    LP#1676608: tweak copy alert type manager
    
    The "Next Status" selector is now disabled unless the event type is
    'Checkin'.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2 b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2
index d4c7c02..6eb816d 100644
--- a/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2
+++ b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2
@@ -49,7 +49,8 @@
       <div class="form-group">
         <label for="edit-alert-next-statuses">[% l('Next Status') %]</label>
         <select id="edit-alert-next-statuses" class="form-control" focus-me='focusMe'
-                multiple="multiple" ng-model="record.next_status">
+                multiple="multiple" ng-model="record.next_status"
+                ng-disabled="record.event != 'CHECKIN'">
             <option ng-repeat="s in ccs" value="{{s.id()}}">{{s.name()}}</option>
         </select>
       </div>

commit c9fb95e59db76815e05ad124a4290439df6f8819
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Feb 22 15:44:52 2018 -0500

    LP#1676608: allow focus-me to accept constant
    
    A handfull of cases have arisen, including but not limited to
    the copy alerts feature, where focus-me is set to simply "true"
    rather than being bound to to a scope variable. This patch formalizes
    this and gets rid of the following console warning:
    
    angular.min.js:119 TypeError: model.assign is not a function
        at ui.js:23
        at angular.min.js:160
        at f (angular.min.js:45)
        at angular.min.js:48
    (anonymous) @ angular.min.js:119
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

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 fb48ea0..b39c33b 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -20,7 +20,8 @@ function($timeout , $parse) {
             });
             element.bind('blur', function() {
                 $timeout(function() {
-                    scope.$apply(model.assign(scope, false));
+                    if (model.assign && typeof model.assign == 'function')
+                        scope.$apply(model.assign(scope, false));
                 });
             })
         }

commit 5b75e9b01c94c861a5ede9e76f34ac10ec3a4a8f
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Feb 22 15:25:10 2018 -0500

    LP#1676608: properly auto-override during missing checkout
    
    This patch ensures that suppressing the checkout of missing copy alert
    does, in fact, suppress the alert.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
index 927e3f0..8dbfcc2 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
@@ -47,7 +47,7 @@ my %COPY_ALERT_OVERRIDES = (
     "CLAIMSRETURNED\tCHECKIN" => ['CIRC_CLAIMS_RETURNED'],
     "LOST\tCHECKOUT" => ['OPEN_CIRCULATION_EXISTS'],
     "LONGOVERDUE\tCHECKOUT" => ['OPEN_CIRCULATION_EXISTS'],
-    "MISSING\tCHECKOUT" => ['COPY_STATUS_MISSING'],
+    "MISSING\tCHECKOUT" => ['COPY_NOT_AVAILABLE'],
     "DAMAGED\tCHECKOUT" => ['COPY_NOT_AVAILABLE'],
     "LOST_AND_PAID\tCHECKOUT" => ['COPY_NOT_AVAILABLE', 'OPEN_CIRCULATION_EXISTS']
 );

commit 619e9059cd6980a7cac987cc33a8618540f7ea4c
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Thu Feb 22 14:25:19 2018 -0500

    LP#1676608: update seed data for new installations
    
    Prior to this patch, only the DB update scripts new about the
    stock copy alert types and new OU settings.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

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 6dec723..44dba2d 100644
--- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql
+++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql
@@ -17205,6 +17205,99 @@ VALUES (
     'integer'
 );
 
+--
+-- seed data for new-style copy alerts
+--
+
+-- staff-usable alert types with no location awareness
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew)
+VALUES (1, 1, FALSE, 'Normal checkout', 'NORMAL', 'CHECKOUT', FALSE);
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew)
+VALUES (2, 1, FALSE, 'Normal checkin', 'NORMAL', 'CHECKIN', FALSE);
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew)
+VALUES (3, 1, FALSE, 'Normal renewal', 'NORMAL', 'CHECKIN', TRUE);
+
+-- copy alerts upon checkin or renewal of exceptional copy statuses are not active by
+-- default; they're meant to be turned once a site is ready to fully
+-- commit to using the webstaff client for circulation
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (4, 1, FALSE, 'Checkin of lost copy', 'LOST', 'CHECKIN');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (5, 1, FALSE, 'Checkin of missing copy', 'MISSING', 'CHECKIN');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (6, 1, FALSE, 'Checkin of lost-and-paid copy', 'LOST_AND_PAID', 'CHECKIN');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (7, 1, FALSE, 'Checkin of damaged copy', 'DAMAGED', 'CHECKIN');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (8, 1, FALSE, 'Checkin of claims-returned copy', 'CLAIMSRETURNED', 'CHECKIN');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (9, 1, FALSE, 'Checkin of long overdue copy', 'LONGOVERDUE', 'CHECKIN');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (10, 1, FALSE, 'Checkin of claims-never-checked-out copy', 'CLAIMSNEVERCHECKEDOUT', 'CHECKIN');
+
+-- copy alerts upon checkout of exceptional copy statuses are not active by
+-- default; they're meant to be turned once a site is ready to fully
+-- commit to using the webstaff client for circulation
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (11, 1, FALSE, 'Checkout of lost copy', 'LOST', 'CHECKOUT');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (12, 1, FALSE, 'Checkout of missing copy', 'MISSING', 'CHECKOUT');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (13, 1, FALSE, 'Checkout of lost-and-paid copy', 'LOST_AND_PAID', 'CHECKOUT');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (14, 1, FALSE, 'Checkout of damaged copy', 'DAMAGED', 'CHECKOUT');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (15, 1, FALSE, 'Checkout of claims-returned copy', 'CLAIMSRETURNED', 'CHECKOUT');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (16, 1, FALSE, 'Checkout of long overdue copy', 'LONGOVERDUE', 'CHECKOUT');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (17, 1, FALSE, 'Checkout of claims-never-checked-out copy', 'CLAIMSNEVERCHECKEDOUT', 'CHECKOUT');
+
+-- staff-usable alert types based on location
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_circ)
+VALUES (18, 1, FALSE, 'Normal checkout at circ lib', 'NORMAL', 'CHECKOUT', FALSE, TRUE);
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_circ)
+VALUES (19, 1, FALSE, 'Normal checkin at circ lib', 'NORMAL', 'CHECKIN', FALSE, TRUE);
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_circ)
+VALUES (20, 1, FALSE, 'Normal renewal at circ lib', 'NORMAL', 'CHECKIN', TRUE, TRUE);
+
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_owning)
+VALUES (21, 1, FALSE, 'Normal checkout at owning lib', 'NORMAL', 'CHECKOUT', FALSE, TRUE);
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_owning)
+VALUES (22, 1, FALSE, 'Normal checkin at owning lib', 'NORMAL', 'CHECKIN', FALSE, TRUE);
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_owning)
+VALUES (23, 1, FALSE, 'Normal renewal at owning lib', 'NORMAL', 'CHECKIN', TRUE, TRUE);
+
+-- OU settings related to copy alerts
+INSERT INTO config.org_unit_setting_type
+    (name, grp, label, description, datatype)
+    VALUES
+        ('circ.copy_alerts.forgive_fines_on_lost_checkin',
+         'circ',
+         oils_i18n_gettext('circ.copy_alerts.forgive_fines_on_lost_checkin',
+            'Forgive fines when checking out a lost item and copy alert is suppressed?',
+            'coust', 'label'),
+         oils_i18n_gettext('circ.copy_alerts.forgive_fines_on_lost_checkin',
+            'Controls whether fines are automatically forgiven when checking out an '||
+            'item that has been marked as lost, and the corresponding copy alert has been '||
+            'suppressed.',
+            'coust', 'description'),
+        'bool');
+
+INSERT INTO config.org_unit_setting_type
+    (name, grp, label, description, datatype)
+    VALUES
+        ('circ.copy_alerts.forgive_fines_on_long_overdue_checkin',
+         'circ',
+         oils_i18n_gettext('circ.copy_alerts.forgive_fines_on_long_overdue_checkin',
+            'Forgive fines when checking out a long-overdue item and copy alert is suppressed?',
+            'coust', 'label'),
+         oils_i18n_gettext('circ.copy_alerts.forgive_fines_on_lost_checkin',
+            'Controls whether fines are automatically forgiven when checking out an '||
+            'item that has been marked as lost, and the corresponding copy alert has been '||
+            'suppressed.',
+            'coust', 'description'),
+        'bool');
 
 INSERT INTO acq.edi_attr (key, label) VALUES
     ('INCLUDE_PO_NAME', 

commit 3784351aed2356b70dc82116d2c1dfff0c6894b7
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Feb 7 16:52:20 2018 -0500

    LP#1676608: tweak labeling at circ/at owning library fields
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2 b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2
index 727bfd9..d4c7c02 100644
--- a/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2
+++ b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2
@@ -69,16 +69,16 @@
         </select>
       </div>
       <div class="form-group">
-        <label for="at-circ-selector">[% l('At Circulation Library?') %]</label>
+        <label for="at-circ-selector">[% l('Allow Only At Circulation Library?') %]</label>
         <select id="at-circ-selector" class="form-control" ng-model="record.at_circ">
-            <option value="">[% l('Do not care') %]</option>
+            <option value="">[% l('No') %]</option>
             <option value="t">[% l('Yes') %]</option>
         </select>
       </div>
       <div class="form-group">
-        <label for="at-owning-selector">[% l('At Owning Library?') %]</label>
+        <label for="at-owning-selector">[% l('Allow Only At Owning Library?') %]</label>
         <select id="at-owning-selector" class="form-control" ng-model="record.at_owning">
-            <option value="">[% l('Do not care') %]</option>
+            <option value="">[% l('No') %]</option>
             <option value="t">[% l('Yes') %]</option>
         </select>
       </div>

commit 85b5d3af913839515a4f20bc732cbbb23ae2d963
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Feb 7 16:41:55 2018 -0500

    LP#1676608: add some sorting to copy alert dialogs
    
    Copy alert types in drop-downs are now sorted by name
    and copy alerts themselves are now consistently sorted
    by ID (which will have the affected of putting the most
    recent alert at the bottom in the copy alert manager dialog).
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/staff/admin/local/autoGridEditor/acas.tt2 b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/acas.tt2
index eab1609..7733249 100644
--- a/Open-ILS/src/templates/staff/admin/local/autoGridEditor/acas.tt2
+++ b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/acas.tt2
@@ -13,7 +13,7 @@
         <label for="copy-alert-type-selector">[% l('Alert Type') %]</label>
         <select id="copy-alert-type-selector" class="form-control"
           ng-model="record.alert_type"
-          ng-options="at.id() as at.name() for at in ccat">
+          ng-options="at.id() as at.name() for at in ccat | orderBy:'name()'">
         </select>
       </div>
       <div class="form-group">
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_copy_alerts.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_copy_alerts.tt2
index b86fef3..3095d51 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_copy_alerts.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_copy_alerts.tt2
@@ -10,7 +10,7 @@
           <label for="copy-alert-type-selector"> [% l('Type') %]</label>
           <select id="copy-alert-type-selector" class="form-control"
             ng-model="copy_alert.alert_type"
-            ng-options="at.id() as at.name() for at in alert_types">
+            ng-options="at.id() as at.name() for at in alert_types | orderBy:'name()'">
           </select>
         </div>
         <div class="col-md-3">
@@ -51,7 +51,7 @@
         </div>
       </div>
 
-      <div class="row" ng-repeat="a in copy_alert_list" ng-init="temp = (a.temp() == 't'); note = a.note(); acked = (a.ack_time() !== null); alert_type = a.alert_type().id()">
+      <div class="row" ng-repeat="a in copy_alert_list | orderBy:'id()'" ng-init="temp = (a.temp() == 't'); note = a.note(); acked = (a.ack_time() !== null); alert_type = a.alert_type().id()">
         <div class="col-md-12">
           <div class="row">
             <div class="col-md-6 form-inline">
@@ -59,7 +59,7 @@
               <select id="copy-alert-type-select-{{a.id()}}" class="form-control"
                       ng-model="alert_type"
                       ng-change="a.alert_type(alert_type) && a.ischanged(1)"
-                      ng-options="at.id() as at.name() for at in alert_types">
+                      ng-options="at.id() as at.name() for at in alert_types | orderBy:'name()'">
               </select>
             </div>
             <div class="col-md-3">
diff --git a/Open-ILS/src/templates/staff/share/t_add_copy_alert_dialog.tt2 b/Open-ILS/src/templates/staff/share/t_add_copy_alert_dialog.tt2
index 0b97a3f..48b556d 100644
--- a/Open-ILS/src/templates/staff/share/t_add_copy_alert_dialog.tt2
+++ b/Open-ILS/src/templates/staff/share/t_add_copy_alert_dialog.tt2
@@ -10,7 +10,7 @@
           <label for="copy-alert-type-selector"> [% l('Type') %]</label>
           <select id="copy-alert-type-selector" class="form-control"
             ng-model="copy_alert.alert_type"
-            ng-options="at.id() as at.name() for at in alert_types">
+            ng-options="at.id() as at.name() for at in alert_types | orderBy:'name()'">
           </select>
         </div>
         <div class="col-md-3">
diff --git a/Open-ILS/src/templates/staff/share/t_copy_alert_editor_dialog.tt2 b/Open-ILS/src/templates/staff/share/t_copy_alert_editor_dialog.tt2
index aad1a6b..9e12317 100644
--- a/Open-ILS/src/templates/staff/share/t_copy_alert_editor_dialog.tt2
+++ b/Open-ILS/src/templates/staff/share/t_copy_alert_editor_dialog.tt2
@@ -5,7 +5,7 @@
       <h4 class="modal-title">[% l('Manage Copy Alerts') %]</h4>
     </div>
     <div class="modal-body">
-      <div class="row" ng-repeat="a in copy_alert_list" ng-init="temp = (a.temp() == 't'); note = a.note(); acked = (a.ack_time() !== null); alert_type = a.alert_type().id()">
+      <div class="row" ng-repeat="a in copy_alert_list | orderBy:'id()'" ng-init="temp = (a.temp() == 't'); note = a.note(); acked = (a.ack_time() !== null); alert_type = a.alert_type().id()">
         <div class="col-md-12">
           <div class="row">
             <div class="col-md-6 form-inline">
@@ -13,7 +13,7 @@
               <select id="copy-alert-type-select-{{a.id()}}" class="form-control"
                       ng-model="alert_type"
                       ng-change="a.alert_type(alert_type) && a.ischanged(1)"
-                      ng-options="at.id() as at.name() for at in alert_types">
+                      ng-options="at.id() as at.name() for at in alert_types | orderBy:'name()'">
               </select>
             </div>
             <div class="col-md-3">

commit 56679c9b7b96c818935b9004c41a76587e679db9
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Feb 7 16:30:04 2018 -0500

    LP#1676608: conditionally enable manage copy alerts button on item status details
    
    The 'Manage' button for copy alerts on the Item Status Detail view
    is now enabled only if there is at least one active copy alert. This
    patch also tweaks how the copy alert dialogs sequence record update
    and modal-closing so that the caller can be sure that the transaction
    has committed.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2 b/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2
index a5dbbdd..3ef0b35 100644
--- a/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2
+++ b/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2
@@ -180,7 +180,7 @@
     <div class="flex-cell">[% l('Copy Alerts') %]</div>
     <div class="flex-cell" id="item-status-alert-msg">
       <button class="btn btn-default" ng-click="addCopyAlerts(copy.id())" >[% l('Add') %]</button>
-      <button class="btn btn-default" ng-click="manageCopyAlerts(copy.id())" >[% l('Manage') %]</button>
+      <button class="btn btn-default" ng-disabled="!copy_alert_count" ng-click="manageCopyAlerts(copy.id())" >[% l('Manage') %]</button>
     </div>
     <!-- hack to adjust positioning -->
     <div class="flex-cell"></div>
diff --git a/Open-ILS/web/js/ui/default/staff/cat/item/app.js b/Open-ILS/web/js/ui/default/staff/cat/item/app.js
index 2a3d7a7..1effdca 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/item/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/item/app.js
@@ -566,6 +566,9 @@ function($scope , $q , $location , $routeParams , $timeout , $window , egCore ,
 
     // use the cached record info
     if (itemSvc.copy) {
+        $scope.copy_alert_count = itemSvc.copy.copy_alerts().filter(function(aca) {
+            return !aca.ack_time();
+        }).length;
         $scope.recordId = itemSvc.copy.call_number().record().id();
         $scope.args.recordId = $scope.recordId;
         $scope.args.cnId = itemSvc.copy.call_number().id();
@@ -588,6 +591,9 @@ function($scope , $q , $location , $routeParams , $timeout , $window , egCore ,
         // regardless of whether it matches the current item.
         if (!barcode && itemSvc.copy && itemSvc.copy.id() == copyId) {
             $scope.copy = itemSvc.copy;
+            $scope.copy_alert_count = itemSvc.copy.copy_alerts().filter(function(aca) {
+                return !aca.ack_time();
+            }).length;
             $scope.recordId = itemSvc.copy.call_number().record().id();
             $scope.args.recordId = $scope.recordId;
             $scope.args.cnId = itemSvc.copy.call_number().id();
@@ -620,6 +626,10 @@ function($scope , $q , $location , $routeParams , $timeout , $window , egCore ,
 
 
             $scope.copy = copy;
+            $scope.copy_alert_count = copy.copy_alerts().filter(function(aca) {
+                return !aca.ack_time();
+            }).length;
+console.debug($scope.copy_alert_count);
             $scope.recordId = copy.call_number().record().id();
             $scope.args.recordId = $scope.recordId;
             $scope.args.cnId = itemSvc.copy.call_number().id();
@@ -969,12 +979,14 @@ function($scope , $q , $location , $routeParams , $timeout , $window , egCore ,
 
     $scope.addCopyAlerts = function(copy_id) {
         egCirc.add_copy_alerts([copy_id]).then(function() {
-            // update grid items?
+            // force a refresh
+            loadCopy($scope.copy.barcode()).then(loadTabData);
         });
     }
     $scope.manageCopyAlerts = function(copy_id) {
         egCirc.manage_copy_alerts([copy_id]).then(function() {
-            // update grid items?
+            // force a refresh
+            loadCopy($scope.copy.barcode()).then(loadTabData);
         });
     }
 
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 1a3f822..fb48ea0 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -683,11 +683,15 @@ function($uibModal , $interpolate , egCore) {
                                 copy_alerts.push( a );
                             });
                             if (copy_alerts.length > 0) {
-                                egCore.pcrud.apply(copy_alerts);
+                                egCore.pcrud.apply(copy_alerts).finally(function() {
+                                    if (args.ok) args.ok();
+                                    $uibModalInstance.close()
+                                });
                             }
+                        } else {
+                            if (args.ok) args.ok();
+                            $uibModalInstance.close()
                         }
-                        if (args.ok) args.ok();
-                        $uibModalInstance.close()
                     }
                     $scope.cancel = function() {
                         if (args.cancel) args.cancel();
@@ -804,10 +808,14 @@ function($uibModal , $interpolate , egCore) {
                             }
                         });
                         if (acks.length > 0) {
-                            egCore.pcrud.apply(acks);
+                            egCore.pcrud.apply(acks).finally(function() {
+                                if (args.ok) args.ok($scope.params.the_next_status);
+                                $uibModalInstance.close()
+                            });
+                        } else {
+                            if (args.ok) args.ok($scope.params.the_next_status);
+                            $uibModalInstance.close()
                         }
-                        if (args.ok) args.ok($scope.params.the_next_status);
-                        $uibModalInstance.close()
                     }
                     $scope.cancel = function() {
                         if (args.cancel) args.cancel();
@@ -870,8 +878,9 @@ function($uibModal , $interpolate , egCore) {
                     });
 
                     $scope.ok = function() {
-                        egCore.pcrud.apply($scope.copy_alert_list);
-                        $uibModalInstance.close()
+                        egCore.pcrud.apply($scope.copy_alert_list).finally(function() {
+                            $uibModalInstance.close();
+                        });
                     }
                     $scope.cancel = function() {
                         if (args.cancel) args.cancel();

commit 3e548a2893f8521278f6bdf331d35f14e1c17a75
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Feb 7 12:20:47 2018 -0500

    LP#1676608: fix glitch with egCore.pcrud.apply()
    
    pcrud.apply() is supposed to skip any entries in the list
    of CUD actions that don't explicitly mark themselves as
    being a creation, update, or deletion, but didn't manage
    to fully skip them. As a consequence, the browser console
    would note complaints that open-ils.pcrud.apply.$IDLCLASS
    methods do not exist.
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/web/js/ui/default/staff/services/pcrud.js b/Open-ILS/web/js/ui/default/staff/services/pcrud.js
index 72cb94e..95e3aa2 100644
--- a/Open-ILS/web/js/ui/default/staff/services/pcrud.js
+++ b/Open-ILS/web/js/ui/default/staff/services/pcrud.js
@@ -278,6 +278,7 @@ angular.module('egCoreMod')
                 if (action == 'apply') {
                     // object does not need updating; move along
                     this._CUD_next_request();
+                    return;
                 }
             }
 

commit ae68e4ff7d79d78b45a4c164305b7994982758f5
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Feb 7 11:59:43 2018 -0500

    LP#1676608: fix positioning of copy alert buttons on Item Status Detail view
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2 b/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2
index 9e37939..a5dbbdd 100644
--- a/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2
+++ b/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2
@@ -178,10 +178,17 @@
 
   <div class="flex-row">
     <div class="flex-cell">[% l('Copy Alerts') %]</div>
-    <div id="item-status-alert-msg">
+    <div class="flex-cell" id="item-status-alert-msg">
       <button class="btn btn-default" ng-click="addCopyAlerts(copy.id())" >[% l('Add') %]</button>
       <button class="btn btn-default" ng-click="manageCopyAlerts(copy.id())" >[% l('Manage') %]</button>
     </div>
+    <!-- hack to adjust positioning -->
+    <div class="flex-cell"></div>
+    <div class="flex-cell"></div>
+    <div class="flex-cell"></div>
+    <div class="flex-cell"></div>
+    <div class="flex-cell"></div>
+    <div class="flex-cell"></div>
   </div>
 
 </div>

commit d29a5ffb17974e881db93caf1960c6fce37e1284
Author: Galen Charlton <gmc at equinoxinitiative.org>
Date:   Wed Feb 7 11:48:29 2018 -0500

    LP#1676608: add release notes
    
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/docs/RELEASE_NOTES_NEXT/Circulation/Copy_Alerts_And_Suppresion_Matrix.adoc b/docs/RELEASE_NOTES_NEXT/Circulation/Copy_Alerts_And_Suppresion_Matrix.adoc
new file mode 100644
index 0000000..66539b6
--- /dev/null
+++ b/docs/RELEASE_NOTES_NEXT/Circulation/Copy_Alerts_And_Suppresion_Matrix.adoc
@@ -0,0 +1,23 @@
+Copy Alerts and Suppression Matrix
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The Copy Alerts feature allows library staff to add customized alert
+messages to copies. The copy alerts will appear when a specific event
+takes place, such as when the copy is checked in, checked out, or
+renewed. Alerts can be temporary or persistent: temporary alerts will be
+disabled after the initial alert and acknowledgement from staff, while
+persistent alerts will display each time the alert event takes place.
+Copy Alerts can be configured to display at the circulating or owning
+library only or, alternatively, when the library at which the alert
+event takes place is not the circulating or owning library.  Copy Alerts
+can also be configured to provide options for the next copy status that
+should be applied to an item.  Library administrators will have the
+ability to create and customize Copy Alert Types and to suppress copy
+alerts at specific org units.
+
+Copy alerts can be added via the volume/creator and the check in,
+check out, and renew pages.  Copy alerts can also be managed at the
+item status page.
+
+Copy alert types can be managed via the Copy Alert Types page in
+Local Administration, and suppression of them can be adminstered
+via the Copy Alert Suppression page under Local Administration.

commit 6f1f2a4faf180bd7861bfd0ac349c85d774d4562
Author: Mike Rylander <mrylander at gmail.com>
Date:   Tue Oct 20 10:10:28 2015 -0400

    LP#1676608: copy alert and suppression matrix
    
    The Copy Alerts feature allows library staff to add customized alert
    messages to copies. The copy alerts will appear when a specific event
    takes place, such as when the copy is checked in, checked out, or
    renewed. Alerts can be temporary or persistent: temporary alerts will be
    disabled after the initial alert and acknowledgement from staff, while
    persistent alerts will display each time the alert event takes place.
    Copy Alerts can be configured to display at the circulating or owning
    library only or, alternatively, when the library at which the alert
    event takes place is not the circulating or owning library.  Copy Alerts
    can also be configured to provide options for the next copy status that
    should be applied to an item.  Library administrators will have the
    ability to create and customize Copy Alert Types and to suppress copy
    alerts at specific org units.
    
    Copy alerts can be added via the volume/creator and the check in,
    check out, and renew pages.  Copy alerts can also be managed at the
    item status page.
    
    Copy alert types can be managed via the Copy Alert Types page in
    Local Administration, and suppression of them can be adminstered
    via the Copy Alert Suppression page under Local Administration.
    
    Co-authored-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>
    Signed-off-by: Galen Charlton <gmc at equinoxinitiative.org>
    Signed-off-by: Mike Rylander <mrylander at gmail.com>

diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml
index 19bce3d..04f3420 100644
--- a/Open-ILS/examples/fm_IDL.xml
+++ b/Open-ILS/examples/fm_IDL.xml
@@ -7204,6 +7204,7 @@ SELECT  usr,
 			<field reporter:label="Last Captured Hold" name="last_captured_hold" oils_persist:virtual="true" reporter:datatype="link"/>
 			<field reporter:label="Has Holds" name="holds_count" oils_persist:virtual="true" reporter:datatype="link"/>
 			<field reporter:label="Copy Tags" name="tags" oils_persist:virtual="true" reporter:datatype="link"/>
+			<field reporter:label="Copy Alerts" name="copy_alerts" oils_persist:virtual="true" reporter:datatype="link"/>
 		</fields>
 		<links>
 			<link field="age_protect" reltype="has_a" key="id" map="" class="crahp"/>
@@ -7230,6 +7231,7 @@ SELECT  usr,
 			<link field="floating" reltype="has_a" key="id" map="" class="cfg"/>
 			<link field="holds_count" reltype="might_have" key="id" map="" class="hasholdscount"/>
 			<link field="tags" reltype="has_many" key="copy" map="" class="acptcm"/>
+			<link field="copy_alerts" reltype="has_many" key="copy" map="" class="aca"/>
 		</links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
@@ -7247,6 +7249,109 @@ SELECT  usr,
         </permacrud>
 	</class>
 
+	<class id="ccat" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::copy_alert_type" oils_persist:tablename="config.copy_alert_type" reporter:label="Copy Alert Type" oils_persist:restrict_primary="100">
+		<fields oils_persist:primary="id" oils_persist:sequence="config.copy_alert_type_id_seq">
+			<field reporter:label="Id" name="id" reporter:selector="name" reporter:datatype="id"/>
+			<field reporter:label="Scope Org Unit" name="scope_org"  reporter:datatype="org_unit"/>
+			<field reporter:label="Active" name="active" reporter:datatype="bool" />
+			<field reporter:label="Name" name="name" reporter:datatype="text" />
+			<field reporter:label="State" name="state" reporter:datatype="text"/>
+			<field reporter:label="Event" name="event" reporter:datatype="text" />
+			<field reporter:label="During Renewal" name="in_renew" reporter:datatype="bool" />
+			<field reporter:label="Allow At Copy Circ Lib" name="at_circ" reporter:datatype="bool"/>
+			<field reporter:label="Allow At Copy Owning Lib" name="at_owning" reporter:datatype="bool"/>
+			<field reporter:label="Invert allowed locations" name="invert_location" reporter:datatype="bool"/>
+			<field reporter:label="Next Statuses" name="next_status" reporter:datatype="text"/>
+		</fields>
+		<links>
+			<link field="scope_org" reltype="has_a" key="id" map="" class="aou"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_COPY_ALERT_TYPE CREATE_COPY_ALERT_TYPE" context_field="scope_org"/>
+                <retrieve/>
+                <update permission="ADMIN_COPY_ALERT_TYPE UPDATE_COPY_ALERT_TYPE" context_field="scope_org"/>
+                <delete permission="ADMIN_COPY_ALERT_TYPE DELETE_COPY_ALERT_TYPE" context_field="scope_org"/>
+            </actions>
+        </permacrud>
+	</class>
+
+	<class id="acas" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="actor::copy_alert_suppress" oils_persist:tablename="actor.copy_alert_suppress" reporter:label="Copy Alert Suppression">
+		<fields oils_persist:primary="id" oils_persist:sequence="actor.copy_alert_suppress_id_seq">
+			<field reporter:label="Id" name="id" reporter:datatype="id"/>
+			<field reporter:label="Org Unit" name="org"  reporter:datatype="org_unit"/>
+			<field reporter:label="Alert Type" name="alert_type" reporter:datatype="link" />
+		</fields>
+		<links>
+			<link field="org" reltype="has_a" key="id" map="" class="aou"/>
+			<link field="alert_type" reltype="has_a" key="id" map="" class="ccat"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_COPY_ALERT_SUPPRESS CREATE_COPY_ALERT_SUPPRESS" context_field="org"/>
+                <retrieve/>
+                <update permission="ADMIN_COPY_ALERT_SUPPRESS UPDATE_COPY_ALERT_SUPPRESS" context_field="org"/>
+                <delete permission="ADMIN_COPY_ALERT_SUPPRESS DELETE_COPY_ALERT_SUPPRESS" context_field="org"/>
+            </actions>
+        </permacrud>
+	</class>
+
+	<class id="aca" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::copy_alert" oils_persist:tablename="asset.copy_alert" reporter:label="Copy Alert">
+		<fields oils_persist:primary="id" oils_persist:sequence="asset.copy_alert_id_seq">
+			<field reporter:label="Id" name="id" reporter:datatype="id"/>
+			<field reporter:label="Alert Type" name="alert_type" reporter:datatype="link" />
+			<field reporter:label="Copy" name="copy"  reporter:datatype="link"/>
+			<field reporter:label="Temporary" name="temp" reporter:datatype="bool" />
+			<field reporter:label="Create Date/Time" name="create_time" reporter:datatype="timestamp" />
+			<field reporter:label="Creator" name="create_staff" reporter:datatype="link"/>
+			<field reporter:label="Note" name="note" reporter:datatype="text"/>
+			<field reporter:label="Acknowledge Date/Time" name="ack_time" reporter:datatype="timestamp" />
+			<field reporter:label="Acknowledger" name="ack_staff" reporter:datatype="link"/>
+		</fields>
+		<links>
+			<link field="alert_type" reltype="has_a" key="id" map="" class="ccat"/>
+			<link field="copy" reltype="has_a" key="id" map="" class="acp"/>
+			<link field="create_staff" reltype="has_a" key="id" map="" class="au"/>
+			<link field="ack_staff" reltype="has_a" key="id" map="" class="au"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_COPY_ALERT CREATE_COPY_ALERT" global_required="true"/>
+                <retrieve permission="ADMIN_COPY_ALERT VIEW_COPY_ALERT" global_required="true"/>
+                <update permission="ADMIN_COPY_ALERT UPDATE_COPY_ALERT" global_required="true"/>
+                <delete permission="ADMIN_COPY_ALERT DELETE_COPY_ALERT" global_required="true"/>
+            </actions>
+        </permacrud>
+	</class>
+
+	<class id="aaca" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::active_copy_alert" oils_persist:tablename="asset.active_copy_alert" reporter:label="Active Copy Alert" oils_persist:readonly="true">
+		<fields oils_persist:primary="id">
+			<field reporter:label="Id" name="id" reporter:datatype="id"/>
+			<field reporter:label="Alert Type" name="alert_type" reporter:datatype="link" />
+			<field reporter:label="Copy" name="copy"  reporter:datatype="link"/>
+			<field reporter:label="Temporary" name="temp" reporter:datatype="bool" />
+			<field reporter:label="Create Date/Time" name="create_time" reporter:datatype="timestamp" />
+			<field reporter:label="Creator" name="create_staff" reporter:datatype="link"/>
+			<field reporter:label="Note" name="note" reporter:datatype="text"/>
+			<field reporter:label="Acknowledge Date/Time" name="ack_time" reporter:datatype="timestamp" />
+			<field reporter:label="Acknowledger" name="ack_staff" reporter:datatype="link"/>
+		</fields>
+		<links>
+			<link field="alert_type" reltype="has_a" key="id" map="" class="ccat"/>
+			<link field="copy" reltype="has_a" key="id" map="" class="acp"/>
+			<link field="create_staff" reltype="has_a" key="id" map="" class="au"/>
+			<link field="ack_staff" reltype="has_a" key="id" map="" class="au"/>
+		</links>
+        <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+            <actions>
+                <create permission="ADMIN_COPY_ALERT CREATE_COPY_ALERT" global_required="true"/>
+                <retrieve permission="ADMIN_COPY_ALERT VIEW_COPY_ALERT" global_required="true"/>
+                <update permission="ADMIN_COPY_ALERT UPDATE_COPY_ALERT" global_required="true"/>
+                <delete permission="ADMIN_COPY_ALERT DELETE_COPY_ALERT" global_required="true"/>
+            </actions>
+        </permacrud>
+	</class>
+
 	<class id="act" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="asset::copy_template" oils_persist:tablename="asset.copy_template" reporter:label="Asset Copy Template">
 		<fields oils_persist:primary="id" oils_persist:sequence="asset.copy_template_id_seq">
 			<field reporter:label="ID" name="id" reporter:datatype="id" reporter:selector="name" />
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
index 4c40d65..5ed0e25 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/AppUtils.pm
@@ -2279,6 +2279,14 @@ sub unique_unnested_numbers {
     );
 }
 
+# Given a list of numbers, turn them into a PG array, skipping undef's
+sub intarray2pgarray {
+    my $class = shift;
+    no warnings 'numeric';
+
+    return '{' . join( ',', map(int, grep { defined && /^\d+$/ } @_) ) . '}';
+}
+
 # Check if a transaction should be left open or closed. Close the
 # transaction if it should be closed or open it otherwise. Returns
 # undef on success or a failure event.
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
index 14a52d4..e52d94f 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Cat/AssetCommon.pm
@@ -258,6 +258,39 @@ sub update_copy_notes {
     return undef;
 }
 
+sub update_copy_alerts {
+    my($class, $editor, $copy) = @_;
+
+    return undef if $copy->isdeleted;
+
+    my $evt;
+    my $incoming_copy_alerts = $copy->copy_alerts;
+
+    for my $incoming_copy_alert (@$incoming_copy_alerts) { 
+        next unless $incoming_copy_alert;
+
+        if ($incoming_copy_alert->isnew) {
+            next if ($incoming_copy_alert->isdeleted); # if it was added and deleted in the same session
+
+            my $new_copy_alert = Fieldmapper::asset::copy_alert->new();
+            $new_copy_alert->copy( $copy->id );
+            $new_copy_alert->temp( $incoming_copy_alert->temp );
+            $new_copy_alert->ack_time( $incoming_copy_alert->ack_time );
+            $new_copy_alert->note( $incoming_copy_alert->note );
+            $new_copy_alert->alert_type( $incoming_copy_alert->alert_type );
+            $new_copy_alert->create_staff( $incoming_copy_alert->create_staff || $editor->requestor->id );
+            $incoming_copy_alert = $editor->create_asset_copy_alert($new_copy_alert)
+                or return $editor->event;
+        } elsif ($incoming_copy_alert->ischanged) {
+            $incoming_copy_alert = $editor->update_asset_copy_alert($incoming_copy_alert)
+        } elsif ($incoming_copy_alert->isdeleted) {
+            $incoming_copy_alert = $editor->delete_asset_copy_alert($incoming_copy_alert->id)
+        }
+    
+    }
+
+    return undef;
+}
 
 sub update_copy_tags {
     my($class, $editor, $copy) = @_;
@@ -303,8 +336,6 @@ sub update_copy_tags {
     return undef;
 }
 
-
-
 sub update_copy {
     my($class, $editor, $override, $vol, $copy, $retarget_holds, $force_delete_empty_bib) = @_;
 
@@ -415,9 +446,13 @@ sub update_fleshed_copies {
 
         my $notes = $copy->notes;
         $copy->clear_notes;
+
         my $tags = $copy->tags;
         $copy->clear_tags;
 
+        my $copy_alerts = $copy->copy_alerts;
+        $copy->clear_copy_alerts;
+
         if( $copy->isdeleted ) {
             $evt = $class->delete_copy($editor, $override, $vol, $copy, $retarget_holds, $force_delete_empty_bib);
             return $evt if $evt;
@@ -445,6 +480,9 @@ sub update_fleshed_copies {
         $copy->tags( $tags );
         $evt = $class->update_copy_tags($editor, $copy);
 
+        $copy->copy_alerts( $copy_alerts );
+        $evt = $class->update_copy_alerts($editor, $copy);
+
         return $evt if $evt;
     }
 
diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
index ce05bc0..927e3f0 100644
--- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
+++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Circ/Circulate.pm
@@ -40,6 +40,18 @@ my $MK_ENV_FLESH = {
     flesh_fields => {acp => ['call_number','parts','floating'], acn => ['record']}
 };
 
+# table of cases where suppressing a system-generated copy alerts
+# should generate an override of an old-style event
+my %COPY_ALERT_OVERRIDES = (
+    "CLAIMSRETURNED\tCHECKOUT" => ['CIRC_CLAIMS_RETURNED'],
+    "CLAIMSRETURNED\tCHECKIN" => ['CIRC_CLAIMS_RETURNED'],
+    "LOST\tCHECKOUT" => ['OPEN_CIRCULATION_EXISTS'],
+    "LONGOVERDUE\tCHECKOUT" => ['OPEN_CIRCULATION_EXISTS'],
+    "MISSING\tCHECKOUT" => ['COPY_STATUS_MISSING'],
+    "DAMAGED\tCHECKOUT" => ['COPY_NOT_AVAILABLE'],
+    "LOST_AND_PAID\tCHECKOUT" => ['COPY_NOT_AVAILABLE', 'OPEN_CIRCULATION_EXISTS']
+);
+
 sub initialize {}
 
 __PACKAGE__->register_method(
@@ -155,6 +167,7 @@ sub run_method {
     my( $self, $conn, $auth, $args ) = @_;
     translate_legacy_args($args);
     $args->{override_args} = { all => 1 } unless defined $args->{override_args};
+    $args->{new_copy_alerts} ||= $self->api_level > 1 ? 1 : 0;
     my $api = $self->api_name;
 
     my $circulator = 
@@ -227,13 +240,13 @@ sub run_method {
 
     $circulator->is_renewal(1) if $api =~ /renew/;
     $circulator->is_checkin(1) if $api =~ /checkin/;
+    $circulator->is_checkout(1) if $api =~ /checkout/;
+    $circulator->override(1) if $api =~ /override/o;
 
     $circulator->mk_env();
     $circulator->noop(1) if $circulator->claims_never_checked_out;
 
     return circ_events($circulator) if $circulator->bail_out;
-    
-    $circulator->override(1) if $api =~ /override/o;
 
     if( $api =~ /checkout\.permit/ ) {
         $circulator->do_permit();
@@ -260,7 +273,6 @@ sub run_method {
         return $data;
 
     } elsif( $api =~ /checkout/ ) {
-        $circulator->is_checkout(1);
         $circulator->do_checkout();
 
     } elsif( $circulator->is_res_checkin ) {
@@ -270,7 +282,6 @@ sub run_method {
         $circulator->do_checkin();
 
     } elsif( $api =~ /renew/ ) {
-        $circulator->is_renewal(1);
         $circulator->do_renew();
     }
 
@@ -404,6 +415,12 @@ my @AUTOLOAD_FIELDS = qw/
     copy
     copy_id
     copy_barcode
+    new_copy_alerts
+    user_copy_alerts
+    system_copy_alerts
+    overrides_per_copy_alerts
+    next_copy_status
+    copy_state
     patron
     patron_id
     patron_barcode
@@ -640,10 +657,233 @@ sub save_trimmed_copy {
     }
 }
 
+sub collect_user_copy_alerts {
+    my $self = shift;
+    my $e = $self->editor;
+
+    if($self->copy) {
+        my $alerts = $e->search_asset_copy_alert([
+            {copy => $self->copy->id, ack_time => undef},
+            {flesh => 1, flesh_fields => { aca => [ qw/ alert_type / ] }}
+        ]);
+        if (ref $alerts eq "ARRAY") {
+            $logger->info("circulator: found " . scalar(@$alerts) . " alerts for copy " .
+                $self->copy->id);
+            $self->user_copy_alerts($alerts);
+        }
+    }
+}
+
+sub filter_user_copy_alerts {
+    my $self = shift;
+
+    my $e = $self->editor;
+
+    if(my $alerts = $self->user_copy_alerts) {
+
+        my $suppress_orgs = $U->get_org_full_path($self->circ_lib);
+        my $suppressions = $e->search_actor_copy_alert_suppress(
+            {org => $suppress_orgs}
+        );
+
+        my @final_alerts;
+        foreach my $a (@$alerts) {
+            # filter on event type
+            if (defined $a->alert_type) {
+                next if ($a->alert_type->event eq 'CHECKIN' && !$self->is_checkin && !$self->is_renewal);
+                next if ($a->alert_type->event eq 'CHECKOUT' && !$self->is_checkout && !$self->is_renewal);
+                next if (defined $a->alert_type->in_renew && $U->is_true($a->alert_type->in_renew) && !$self->is_renewal);
+                next if (defined $a->alert_type->in_renew && !$U->is_true($a->alert_type->in_renew) && $self->is_renewal);
+            }
+
+            # filter on suppression
+            next if (grep { $a->alert_type->id == $_->alert_type} @$suppressions);
+
+            # filter on "only at circ lib"
+            if (defined $a->alert_type->at_circ) {
+                my $copy_circ_lib = (ref $self->copy->circ_lib) ?
+                    $self->copy->circ_lib->id : $self->copy->circ_lib;
+                my $orgs = $U->get_org_descendants($copy_circ_lib);
+
+                if ($U->is_true($a->alert_type->invert_location)) {
+                    next if (grep {$_ == $self->circ_lib} @$orgs);
+                } else {
+                    next unless (grep {$_ == $self->circ_lib} @$orgs);
+                }
+            }
+
+            # filter on "only at owning lib"
+            if (defined $a->alert_type->at_owning) {
+                my $copy_owning_lib = (ref $self->volume->owning_lib) ?
+                    $self->volume->owning_lib->id : $self->volume->owning_lib;
+                my $orgs = $U->get_org_descendants($copy_owning_lib);
+
+                if ($U->is_true($a->alert_type->invert_location)) {
+                    next if (grep {$_ == $self->circ_lib} @$orgs);
+                } else {
+                    next unless (grep {$_ == $self->circ_lib} @$orgs);
+                }
+            }
+
+            $a->alert_type->next_status([$U->unique_unnested_numbers($a->alert_type->next_status)]);
+
+            push @final_alerts, $a;
+        }
+
+        $self->user_copy_alerts(\@final_alerts);
+    }
+}
+
+sub generate_system_copy_alerts {
+    my $self = shift;
+    return unless($self->copy);
+
+    # don't create system copy alerts if the copy
+    # is in a normal state; we're assuming that there's
+    # never a need to generate a popup for each and every
+    # checkin or checkout of normal items. If this assumption
+    # proves false, then we'll need to add a way to explicitly specify
+    # that a copy alert type should never generate a system copy alert
+    return if $self->copy_state eq 'NORMAL';
+
+    my $e = $self->editor;
+
+    my $suppress_orgs = $U->get_org_full_path($self->circ_lib);
+    my $suppressions = $e->search_actor_copy_alert_suppress(
+        {org => $suppress_orgs}
+    );
+
+    # events we care about ...
+    my $event = [];
+    push(@$event, 'CHECKIN') if $self->is_checkin;
+    push(@$event, 'CHECKOUT') if $self->is_checkout;
+    return unless scalar(@$event);
+
+    my $alert_orgs = $U->get_org_ancestors($self->circ_lib);
+    my $alert_types = $e->search_config_copy_alert_type({
+        active    => 't',
+        scope_org => $alert_orgs,
+        event     => $event,
+        state => $self->copy_state,
+        '-or' => [ { in_renew => $self->is_renewal }, { in_renew => undef } ],
+    });
+
+    my @final_types;
+    foreach my $a (@$alert_types) {
+        # filter on "only at circ lib"
+        if (defined $a->at_circ) {
+            my $copy_circ_lib = (ref $self->copy->circ_lib) ?
+                $self->copy->circ_lib->id : $self->copy->circ_lib;
+            my $orgs = $U->get_org_descendants($copy_circ_lib);
+
+            if ($U->is_true($a->invert_location)) {
+                next if (grep {$_ == $self->circ_lib} @$orgs);
+            } else {
+                next unless (grep {$_ == $self->circ_lib} @$orgs);
+            }
+        }
+
+        # filter on "only at owning lib"
+        if (defined $a->at_owning) {
+            my $copy_owning_lib = (ref $self->volume->owning_lib) ?
+                $self->volume->owning_lib->id : $self->volume->owning_lib;
+            my $orgs = $U->get_org_descendants($copy_owning_lib);
+
+            if ($U->is_true($a->invert_location)) {
+                next if (grep {$_ == $self->circ_lib} @$orgs);
+            } else {
+                next unless (grep {$_ == $self->circ_lib} @$orgs);
+            }
+        }
+
+        push @final_types, $a;
+    }
+
+    if (@final_types) {
+        $logger->info("circulator: found " . scalar(@final_types) . " system alert types for copy" .
+            $self->copy->id);
+    }
+
+    my @alerts;
+    
+    # keep track of conditions corresponding to suppressed
+    # system alerts, as these may be used to overridee
+    # certain old-style-events
+    my %auto_override_conditions = ();
+    foreach my $t (@final_types) {
+        if ($t->next_status) {
+            if (grep { $t->id == $_->alert_type } @$suppressions) {
+                $t->next_status([]);
+            } else {
+                $t->next_status([$U->unique_unnested_numbers($t->next_status)]);
+            }
+        }
+
+        my $alert = new Fieldmapper::asset::copy_alert ();
+        $alert->alert_type($t->id);
+        $alert->copy($self->copy->id);
+        $alert->temp(1);
+        $alert->create_staff($e->requestor->id);
+        $alert->create_time('now');
+        $alert->ack_staff($e->requestor->id);
+        $alert->ack_time('now');
+
+        $alert = $e->create_asset_copy_alert($alert);
+
+        next unless $alert;
+
+        $alert->alert_type($t->clone);
+
+        push(@{$self->next_copy_status}, @{$t->next_status}) if ($t->next_status);
+        if (grep {$_->alert_type == $t->id} @$suppressions) {
+            $auto_override_conditions{join("\t", $t->state, $t->event)} = 1;
+        }
+        push(@alerts, $alert) unless (grep {$_->alert_type == $t->id} @$suppressions);
+    }
+
+    $self->system_copy_alerts(\@alerts);
+    $self->overrides_per_copy_alerts(\%auto_override_conditions);
+}
+
+sub add_overrides_from_system_copy_alerts {
+    my $self = shift;
+    my $e = $self->editor;
+
+    foreach my $condition (keys %{$self->overrides_per_copy_alerts()}) {
+        if (exists $COPY_ALERT_OVERRIDES{$condition}) {
+            $self->override(1);
+            push @{$self->override_args->{events}}, @{ $COPY_ALERT_OVERRIDES{$condition} };
+            # special handling for long-overdue and lost checkouts
+            if (grep { $_ eq 'OPEN_CIRCULATION_EXISTS' } @{ $COPY_ALERT_OVERRIDES{$condition} }) {
+                my $state = (split /\t/, $condition, -1)[0];
+                my $setting;
+                if ($state eq 'LOST' or $state eq 'LOST_AND_PAID') {
+                    $setting = 'circ.copy_alerts.forgive_fines_on_lost_checkin';
+                } elsif ($state eq 'LONGOVERDUE') {
+                    $setting = 'circ.copy_alerts.forgive_fines_on_long_overdue_checkin';
+                } else {
+                    next;
+                }
+                my $forgive = $U->ou_ancestor_setting_value(
+                    $self->circ_lib, $setting, $e
+                );
+                if ($U->is_true($forgive)) {
+                    $self->void_overdues(1);
+                }
+                $self->noop(1); # do not attempt transits, just check it in
+                $self->do_checkin();
+            }
+        }
+    }
+}
+
 sub mk_env {
     my $self = shift;
     my $e = $self->editor;
 
+    $self->next_copy_status([]) unless (defined $self->next_copy_status);
+    $self->overrides_per_copy_alerts({}) unless (defined $self->overrides_per_copy_alerts);
+
     # --------------------------------------------------------------------------
     # Grab the fleshed copy
     # --------------------------------------------------------------------------
@@ -676,6 +916,7 @@ sub mk_env {
                         }
                     },
                     "where" => {
+                        deleted => 'f',
                         "+bresv" => {
                             "id" => (ref $self->reservation) ?
                                 $self->reservation->id : $self->reservation
@@ -692,6 +933,19 @@ sub mk_env {
     
         if($copy) {
             $self->save_trimmed_copy($copy);
+
+            # alerts!
+            $self->copy_state(
+                $e->json_query(
+                    {from => ['asset.copy_state', $copy->id]}
+                )->[0]{'asset.copy_state'}
+            );
+
+            $self->generate_system_copy_alerts;
+            $self->add_overrides_from_system_copy_alerts;
+            $self->collect_user_copy_alerts;
+            $self->filter_user_copy_alerts;
+
         } else {
             # We can't renew if there is no copy
             return $self->bail_on_events(OpenILS::Event->new('ASSET_COPY_NOT_FOUND'))
@@ -1207,6 +1461,21 @@ sub run_copy_permit_scripts {
 
 sub check_copy_alert {
     my $self = shift;
+
+    if ($self->new_copy_alerts) {
+        my @alerts;
+        push @alerts, @{$self->user_copy_alerts} # we have preexisting alerts 
+            if ($self->user_copy_alerts && @{$self->user_copy_alerts});
+
+        push @alerts, @{$self->system_copy_alerts} # we have new dynamic alerts 
+            if ($self->system_copy_alerts && @{$self->system_copy_alerts});
+
+        if (@alerts) {
+            $self->bail_out(1) if (!$self->override);
+            return OpenILS::Event->new( 'COPY_ALERT_MESSAGE', payload => \@alerts);
+        }
+    }
+
     return undef if $self->is_renewal;
     return OpenILS::Event->new(
         'COPY_ALERT_MESSAGE', payload => $self->copy->alert_message)
@@ -2640,7 +2909,8 @@ sub do_checkin {
             $U->ou_ancestor_setting_value($self->circ->circ_lib, 'circ.claim_never_checked_out.mark_missing')) {
 
         # the item was not supposed to be checked out to the user and should now be marked as missing
-        $self->copy->status(OILS_COPY_STATUS_MISSING);
+        my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_MISSING;
+        $self->copy->status($next_status);
         $self->update_copy;
 
     } else {
@@ -2716,13 +2986,15 @@ sub reshelve_copy {
 
    my $stat = $U->copy_status($copy->status)->id;
 
+   my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
+
    if($force || (
       $stat != OILS_COPY_STATUS_ON_HOLDS_SHELF and
       $stat != OILS_COPY_STATUS_CATALOGING and
       $stat != OILS_COPY_STATUS_IN_TRANSIT and
-      $stat != OILS_COPY_STATUS_RESHELVING  )) {
+      $stat != $next_status  )) {
 
-        $copy->status( OILS_COPY_STATUS_RESHELVING );
+        $copy->status( $next_status );
             $self->update_copy;
             $self->checkin_changed(1);
     }
@@ -3333,7 +3605,8 @@ sub checkin_handle_circ_start {
     } elsif ($circ_lib != $self->circ_lib and $stat == OILS_COPY_STATUS_MISSING) {
         $logger->info("circulator: not updating copy status on checkin because copy is missing");
     } else {
-        $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
+        my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
+        $self->copy->status($U->copy_status($next_status));
         $self->update_copy;
     }
 
@@ -3521,7 +3794,8 @@ sub checkin_handle_lost_or_longoverdue {
         if ($immediately_available) {
             # item status does not need to be retained, so give it a
             # reshelving status as if it were a normal checkin
-            $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
+            my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
+            $self->copy->status($U->copy_status($next_status));
             $self->update_copy;
         } else {
             $logger->info("circulator: leaving lost/longoverdue copy".
@@ -3530,7 +3804,8 @@ sub checkin_handle_lost_or_longoverdue {
     } else {
         # lost/longoverdue item is home and processed, treat like a normal 
         # checkin from this point on
-        $self->copy->status($U->copy_status(OILS_COPY_STATUS_RESHELVING));
+        my $next_status = $self->next_copy_status->[0] || OILS_COPY_STATUS_RESHELVING;
+        $self->copy->status($U->copy_status($next_status));
         $self->update_copy;
     }
 }
@@ -3571,7 +3846,8 @@ sub check_checkin_copy_status {
    my $status = $U->copy_status($copy->status)->id;
 
    return undef
-      if(   $status == OILS_COPY_STATUS_AVAILABLE   ||
+      if(   $self->new_copy_alerts ||
+            $status == OILS_COPY_STATUS_AVAILABLE   ||
             $status == OILS_COPY_STATUS_CHECKED_OUT ||
             $status == OILS_COPY_STATUS_IN_PROCESS  ||
             $status == OILS_COPY_STATUS_ON_HOLDS_SHELF  ||
diff --git a/Open-ILS/src/sql/Pg/040.schema.asset.sql b/Open-ILS/src/sql/Pg/040.schema.asset.sql
index f9948cb..d17dcfa 100644
--- a/Open-ILS/src/sql/Pg/040.schema.asset.sql
+++ b/Open-ILS/src/sql/Pg/040.schema.asset.sql
@@ -987,5 +987,97 @@ CREATE INDEX asset_copy_tag_copy_map_copy_idx
 CREATE INDEX asset_copy_tag_copy_map_tag_idx
     ON asset.copy_tag_copy_map (tag);
 
+CREATE OR REPLACE FUNCTION asset.copy_state (cid BIGINT) RETURNS TEXT AS $$
+DECLARE
+    last_circ_stop	TEXT;
+    the_copy	    asset.copy%ROWTYPE;
+BEGIN
+
+    SELECT * INTO the_copy FROM asset.copy WHERE id = cid;
+    IF NOT FOUND THEN RETURN NULL; END IF;
+
+    IF the_copy.status = 3 THEN -- Lost
+        RETURN 'LOST';
+    ELSIF the_copy.status = 4 THEN -- Missing
+        RETURN 'MISSING';
+    ELSIF the_copy.status = 14 THEN -- Damaged
+        RETURN 'DAMAGED';
+    ELSIF the_copy.status = 17 THEN -- Lost and paid
+        RETURN 'LOST_AND_PAID';
+    END IF;
+
+    SELECT stop_fines INTO last_circ_stop
+      FROM  action.circulation
+      WHERE target_copy = cid
+      ORDER BY xact_start DESC LIMIT 1;
+
+    IF FOUND THEN
+        IF last_circ_stop IN (
+            'CLAIMSNEVERCHECKEDOUT',
+            'CLAIMSRETURNED',
+            'LONGOVERDUE'
+        ) THEN
+            RETURN last_circ_stop;
+        END IF;
+    END IF;
+
+    RETURN 'NORMAL';
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE TYPE config.copy_alert_type_state AS ENUM (
+    'NORMAL',
+    'LOST',
+    'LOST_AND_PAID',
+    'MISSING',
+    'DAMAGED',
+    'CLAIMSRETURNED',
+    'LONGOVERDUE',
+    'CLAIMSNEVERCHECKEDOUT'
+);
+
+CREATE TYPE config.copy_alert_type_event AS ENUM (
+    'CHECKIN',
+    'CHECKOUT'
+);
+
+CREATE TABLE config.copy_alert_type (
+    id      serial  primary key, -- reserve 1-100 for system
+    scope_org   int not null references actor.org_unit (id) on delete cascade,
+    active      bool    not null default true,
+    name        text    not null unique,
+    state       config.copy_alert_type_state,
+    event       config.copy_alert_type_event,
+    in_renew    bool,
+    invert_location bool    not null default false,
+    at_circ     bool,
+    at_owning   bool,
+    next_status int[]
+);
+SELECT SETVAL('config.copy_alert_type_id_seq'::TEXT, 100);
+
+CREATE TABLE actor.copy_alert_suppress (
+    id          serial primary key,
+    org         int not null references actor.org_unit (id) on delete cascade,
+    alert_type  int not null references config.copy_alert_type (id) on delete cascade
+);
+
+CREATE TABLE asset.copy_alert (
+    id      bigserial   primary key,
+    alert_type  int     not null references config.copy_alert_type (id) on delete cascade,
+    copy        bigint  not null references asset.copy (id) on delete cascade,
+    temp        bool    not null default false,
+    create_time timestamptz not null default now(),
+    create_staff    bigint  not null references actor.usr (id) on delete set null,
+    note        text,
+    ack_time    timestamptz,
+    ack_staff   bigint references actor.usr (id) on delete set null
+);
+
+CREATE VIEW asset.active_copy_alert AS
+    SELECT  *
+      FROM  asset.copy_alert
+      WHERE ack_time IS NULL;
+
 COMMIT;
 
diff --git a/Open-ILS/src/sql/Pg/live_t/copy_state.pg b/Open-ILS/src/sql/Pg/live_t/copy_state.pg
new file mode 100644
index 0000000..e09e62b
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/live_t/copy_state.pg
@@ -0,0 +1,11 @@
+BEGIN;
+
+SELECT plan(1);
+
+\set copy_id 245
+
+UPDATE asset.copy SET status = 4 WHERE id = :copy_id;
+
+SELECT is(asset.copy_state(:copy_id), 'MISSING', 'asset.copy_state detects missing state');
+
+ROLLBACK;
diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql
new file mode 100644
index 0000000..db4cd25
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.schema.copy_alerts.sql
@@ -0,0 +1,96 @@
+BEGIN;
+
+CREATE OR REPLACE FUNCTION asset.copy_state (cid BIGINT) RETURNS TEXT AS $$
+DECLARE
+    last_circ_stop	TEXT;
+    the_copy	    asset.copy%ROWTYPE;
+BEGIN
+
+    SELECT * INTO the_copy FROM asset.copy WHERE id = cid;
+    IF NOT FOUND THEN RETURN NULL; END IF;
+
+    IF the_copy.status = 3 THEN -- Lost
+        RETURN 'LOST';
+    ELSIF the_copy.status = 4 THEN -- Missing
+        RETURN 'MISSING';
+    ELSIF the_copy.status = 14 THEN -- Damaged
+        RETURN 'DAMAGED';
+    ELSIF the_copy.status = 17 THEN -- Lost and paid
+        RETURN 'LOST_AND_PAID';
+    END IF;
+
+    SELECT stop_fines INTO last_circ_stop
+      FROM  action.circulation
+      WHERE target_copy = cid
+      ORDER BY xact_start DESC LIMIT 1;
+
+    IF FOUND THEN
+        IF last_circ_stop IN (
+            'CLAIMSNEVERCHECKEDOUT',
+            'CLAIMSRETURNED',
+            'LONGOVERDUE'
+        ) THEN
+            RETURN last_circ_stop;
+        END IF;
+    END IF;
+
+    RETURN 'NORMAL';
+END;
+$$ LANGUAGE PLPGSQL;
+
+CREATE TYPE config.copy_alert_type_state AS ENUM (
+    'NORMAL',
+    'LOST',
+    'LOST_AND_PAID',
+    'MISSING',
+    'DAMAGED',
+    'CLAIMSRETURNED',
+    'LONGOVERDUE',
+    'CLAIMSNEVERCHECKEDOUT'
+);
+
+CREATE TYPE config.copy_alert_type_event AS ENUM (
+    'CHECKIN',
+    'CHECKOUT'
+);
+
+CREATE TABLE config.copy_alert_type (
+    id      	serial  primary key, -- reserve 1-100 for system
+    scope_org   int not null references actor.org_unit (id) on delete cascade,
+    active      bool    not null default true,
+    name        text    not null unique,
+    state       config.copy_alert_type_state,
+    event       config.copy_alert_type_event,
+    in_renew    bool,
+    invert_location bool    not null default false,
+    at_circ     bool,
+    at_owning   bool,
+    next_status int[]
+);
+SELECT SETVAL('config.copy_alert_type_id_seq'::TEXT, 100);
+
+CREATE TABLE actor.copy_alert_suppress (
+    id          serial primary key,
+    org         int not null references actor.org_unit (id) on delete cascade,
+    alert_type  int not null references config.copy_alert_type (id) on delete cascade
+);
+
+CREATE TABLE asset.copy_alert (
+    id      bigserial   primary key,
+    alert_type  int     not null references config.copy_alert_type (id) on delete cascade,
+    copy        bigint  not null references asset.copy (id) on delete cascade,
+    temp        bool    not null default false,
+    create_time timestamptz not null default now(),
+    create_staff    bigint  not null references actor.usr (id) on delete set null,
+    note        text,
+    ack_time    timestamptz,
+    ack_staff   bigint references actor.usr (id) on delete set null
+);
+
+CREATE VIEW asset.active_copy_alert AS
+    SELECT  *
+      FROM  asset.copy_alert
+      WHERE ack_time IS NULL;
+
+COMMIT;
+
diff --git a/Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql
new file mode 100644
index 0000000..cc09016
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/YYYY.data.stock_copy_alert_types.sql
@@ -0,0 +1,62 @@
+BEGIN;
+
+-- staff-usable alert types with no location awareness
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew)
+VALUES (1, 1, FALSE, 'Normal checkout', 'NORMAL', 'CHECKOUT', FALSE);
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew)
+VALUES (2, 1, FALSE, 'Normal checkin', 'NORMAL', 'CHECKIN', FALSE);
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew)
+VALUES (3, 1, FALSE, 'Normal renewal', 'NORMAL', 'CHECKIN', TRUE);
+
+-- copy alerts upon checkin or renewal of exceptional copy statuses are not active by
+-- default; they're meant to be turned once a site is ready to fully
+-- commit to using the webstaff client for circulation
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (4, 1, FALSE, 'Checkin of lost copy', 'LOST', 'CHECKIN');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (5, 1, FALSE, 'Checkin of missing copy', 'MISSING', 'CHECKIN');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (6, 1, FALSE, 'Checkin of lost-and-paid copy', 'LOST_AND_PAID', 'CHECKIN');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (7, 1, FALSE, 'Checkin of damaged copy', 'DAMAGED', 'CHECKIN');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (8, 1, FALSE, 'Checkin of claims-returned copy', 'CLAIMSRETURNED', 'CHECKIN');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (9, 1, FALSE, 'Checkin of long overdue copy', 'LONGOVERDUE', 'CHECKIN');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (10, 1, FALSE, 'Checkin of claims-never-checked-out copy', 'CLAIMSNEVERCHECKEDOUT', 'CHECKIN');
+
+-- copy alerts upon checkout of exceptional copy statuses are not active by
+-- default; they're meant to be turned once a site is ready to fully
+-- commit to using the webstaff client for circulation
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (11, 1, FALSE, 'Checkout of lost copy', 'LOST', 'CHECKOUT');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (12, 1, FALSE, 'Checkout of missing copy', 'MISSING', 'CHECKOUT');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (13, 1, FALSE, 'Checkout of lost-and-paid copy', 'LOST_AND_PAID', 'CHECKOUT');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (14, 1, FALSE, 'Checkout of damaged copy', 'DAMAGED', 'CHECKOUT');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (15, 1, FALSE, 'Checkout of claims-returned copy', 'CLAIMSRETURNED', 'CHECKOUT');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (16, 1, FALSE, 'Checkout of long overdue copy', 'LONGOVERDUE', 'CHECKOUT');
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event)
+VALUES (17, 1, FALSE, 'Checkout of claims-never-checked-out copy', 'CLAIMSNEVERCHECKEDOUT', 'CHECKOUT');
+
+-- staff-usable alert types based on location
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_circ)
+VALUES (18, 1, FALSE, 'Normal checkout at circ lib', 'NORMAL', 'CHECKOUT', FALSE, TRUE);
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_circ)
+VALUES (19, 1, FALSE, 'Normal checkin at circ lib', 'NORMAL', 'CHECKIN', FALSE, TRUE);
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_circ)
+VALUES (20, 1, FALSE, 'Normal renewal at circ lib', 'NORMAL', 'CHECKIN', TRUE, TRUE);
+
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_owning)
+VALUES (21, 1, FALSE, 'Normal checkout at owning lib', 'NORMAL', 'CHECKOUT', FALSE, TRUE);
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_owning)
+VALUES (22, 1, FALSE, 'Normal checkin at owning lib', 'NORMAL', 'CHECKIN', FALSE, TRUE);
+INSERT INTO config.copy_alert_type (id, scope_org, active, name, state, event, in_renew, at_owning)
+VALUES (23, 1, FALSE, 'Normal renewal at owning lib', 'NORMAL', 'CHECKIN', TRUE, TRUE);
+
+COMMIT;
diff --git a/Open-ILS/src/sql/Pg/upgrade/ZZZZ.data.yaous_for_open_circ_exists_fine_handling.sql b/Open-ILS/src/sql/Pg/upgrade/ZZZZ.data.yaous_for_open_circ_exists_fine_handling.sql
new file mode 100644
index 0000000..4235527
--- /dev/null
+++ b/Open-ILS/src/sql/Pg/upgrade/ZZZZ.data.yaous_for_open_circ_exists_fine_handling.sql
@@ -0,0 +1,35 @@
+BEGIN;
+
+--- SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO config.org_unit_setting_type
+    (name, grp, label, description, datatype)
+    VALUES
+        ('circ.copy_alerts.forgive_fines_on_lost_checkin',
+         'circ',
+         oils_i18n_gettext('circ.copy_alerts.forgive_fines_on_lost_checkin',
+            'Forgive fines when checking out a lost item and copy alert is suppressed?',
+            'coust', 'label'),
+         oils_i18n_gettext('circ.copy_alerts.forgive_fines_on_lost_checkin',
+            'Controls whether fines are automatically forgiven when checking out an '||
+            'item that has been marked as lost, and the corresponding copy alert has been '||
+            'suppressed.',
+            'coust', 'description'),
+        'bool');
+
+INSERT INTO config.org_unit_setting_type
+    (name, grp, label, description, datatype)
+    VALUES
+        ('circ.copy_alerts.forgive_fines_on_long_overdue_checkin',
+         'circ',
+         oils_i18n_gettext('circ.copy_alerts.forgive_fines_on_long_overdue_checkin',
+            'Forgive fines when checking out a long-overdue item and copy alert is suppressed?',
+            'coust', 'label'),
+         oils_i18n_gettext('circ.copy_alerts.forgive_fines_on_lost_checkin',
+            'Controls whether fines are automatically forgiven when checking out an '||
+            'item that has been marked as lost, and the corresponding copy alert has been '||
+            'suppressed.',
+            'coust', 'description'),
+        'bool');
+
+COMMIT;
diff --git a/Open-ILS/src/templates/staff/admin/local/autoGridEditor/acas.tt2 b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/acas.tt2
new file mode 100644
index 0000000..eab1609
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/acas.tt2
@@ -0,0 +1,30 @@
+[% ctx.page_title = l("Copy Alert Suppression"); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(record)">
+  <div>
+    <div class="modal-header">
+      <button type="button" class="close" 
+        ng-click="cancel()" aria-hidden="true">×</button>
+      <h4 ng-if="creating"  class="modal-title">[% l('Create copy alert suppression rule') %]</h4>
+      <h4 ng-if="!creating" class="modal-title">[% l('Update copy alert suppression rule') %]</h4>
+    </div>
+    <div class="modal-body">
+      <div class="form-group">
+        <label for="copy-alert-type-selector">[% l('Alert Type') %]</label>
+        <select id="copy-alert-type-selector" class="form-control"
+          ng-model="record.alert_type"
+          ng-options="at.id() as at.name() for at in ccat">
+        </select>
+      </div>
+      <div class="form-group">
+        <label for="select-org-unit">[% l('Org Unit') %]</label>
+        <eg-org-selector selected="record.org"></eg-org-selector>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <input type="submit" ng-disabled="form.$invalid" 
+          class="btn btn-primary" value="[% l('Save') %]"/>
+      <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+    </div>
+  </div> <!-- modal-content -->
+</form>
diff --git a/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2 b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2
new file mode 100644
index 0000000..727bfd9
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2
@@ -0,0 +1,92 @@
+[% ctx.page_title = l("Copy Alert Types"); %]
+<!-- use <form> so we get submit-on-enter for free -->
+<form class="form-validated" novalidate name="form" ng-submit="ok(record)">
+  <div>
+    <div class="modal-header">
+      <button type="button" class="close" 
+        ng-click="cancel()" aria-hidden="true">×</button>
+      <h4 ng-if="creating"  class="modal-title">[% l('Create copy alert type') %]</h4>
+      <h4 ng-if="!creating" class="modal-title">[% l('Update copy alert type') %]</h4>
+    </div>
+    <div class="modal-body">
+      <div class="form-group">
+        <label for="edit-alert-name">[% l('Name') %]</label>
+        <input type="text" class="form-control" focus-me='focusMe' required
+          id="edit-alert-name" ng-model="record.name" placeholder="[% l('Name...') %]"/>
+      </div>
+      <div class="form-group">
+        <label for="active-selector">[% l('Active') %]</label>
+        <select id="active-selector" class="form-control" ng-model="record.active">
+            <option value="t">[% l('Yes') %]</option>
+            <option value="f">[% l('No') %]</option>
+        </select>
+      </div>
+      <div class="form-group">
+        <label for="state-selector">[% l('State') %]</label>
+        <select id="state-selector" class="form-control" ng-model="record.state">
+            <option value="NORMAL">[% l('Normal') %]</option>
+            <option value="LOST">[% l('Lost') %]</option>
+            <option value="LOST_AND_PAID">[% l('Lost and paid for') %]</option>
+            <option value="LONGOVERDUE">[% l('Long Overdue') %]</option>
+            <option value="MISSING">[% l('Missing') %]</option>
+            <option value="DAMAGED">[% l('Damaged') %]</option>
+            <option value="CLAIMSRETURNED">[% l('Claims returned') %]</option>
+            <option value="CLAIMSNEVERCHECKEDOUT">[% l('Claims never checked out') %]</option>
+        </select>
+      </div>
+      <div class="form-group nullable">
+        <label for="event-selector">[% l('Event') %]</label>
+        <select id="event-selector" class="form-control" ng-model="record.event"
+          ng-init="event_list = [{l:'[% l('Checkin') %]',v:'CHECKIN'},{l:'[% l('Checkout') %]',v:'CHECKOUT'}]"
+          ng-options="e.v as e.l for e in event_list">
+            <option value="">[% l('Any Event') %]</option>
+        </select>
+      </div>
+      <div class="form-group">
+        <label for="select-org-unit">[% l('Scope Org Unit') %]</label>
+        <eg-org-selector selected="record.scope_org"></eg-org-selector>
+      </div>
+      <div class="form-group">
+        <label for="edit-alert-next-statuses">[% l('Next Status') %]</label>
+        <select id="edit-alert-next-statuses" class="form-control" focus-me='focusMe'
+                multiple="multiple" ng-model="record.next_status">
+            <option ng-repeat="s in ccs" value="{{s.id()}}">{{s.name()}}</option>
+        </select>
+      </div>
+      <div class="form-group">
+        <label for="inrenew-selector">[% l('Renewing?') %]</label>
+        <select id="inrenew-selector" class="form-control" ng-model="record.in_renew">
+            <option value="">[% l('Any') %]</option>
+            <option value="t">[% l('Yes') %]</option>
+            <option value="f">[% l('No') %]</option>
+        </select>
+      </div>
+      <div class="form-group">
+        <label for="invert-location-selector">[% l('Invert location?') %]</label>
+        <select id="invert-location-selector" class="form-control" ng-model="record.invert_location">
+            <option value="t">[% l('Yes') %]</option>
+            <option value="f">[% l('No') %]</option>
+        </select>
+      </div>
+      <div class="form-group">
+        <label for="at-circ-selector">[% l('At Circulation Library?') %]</label>
+        <select id="at-circ-selector" class="form-control" ng-model="record.at_circ">
+            <option value="">[% l('Do not care') %]</option>
+            <option value="t">[% l('Yes') %]</option>
+        </select>
+      </div>
+      <div class="form-group">
+        <label for="at-owning-selector">[% l('At Owning Library?') %]</label>
+        <select id="at-owning-selector" class="form-control" ng-model="record.at_owning">
+            <option value="">[% l('Do not care') %]</option>
+            <option value="t">[% l('Yes') %]</option>
+        </select>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <input type="submit" ng-disabled="form.$invalid" 
+          class="btn btn-primary" value="[% l('Save') %]"/>
+      <button class="btn btn-warning" ng-click="cancel()">[% l('Cancel') %]</button>
+    </div>
+  </div> <!-- modal-content -->
+</form>
diff --git a/Open-ILS/src/templates/staff/admin/local/index.tt2 b/Open-ILS/src/templates/staff/admin/local/index.tt2
index 02d604f..6200f6e 100644
--- a/Open-ILS/src/templates/staff/admin/local/index.tt2
+++ b/Open-ILS/src/templates/staff/admin/local/index.tt2
@@ -7,8 +7,14 @@
 
 [% BLOCK APP_JS %]
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/services/eframe.js"></script>
+<script src="[% ctx.media_prefix %]/js/ui/default/staff/services/grid.js"></script>
 <script src="[% ctx.media_prefix %]/js/ui/default/staff/admin/local/app.js"></script>
 <link rel="stylesheet" href="[% ctx.base_path %]/staff/css/admin.css" />
+<script>
+angular.module('egCoreMod').run(['egStrings', function(s) {
+  s.REMOVE_ITEM_CONFIRM = '[% l('Delete rows?') %]';
+}]);
+</script
 [% END %]
 
 <div ng-view></div>
diff --git a/Open-ILS/src/templates/staff/admin/local/t_grid_editor.tt2 b/Open-ILS/src/templates/staff/admin/local/t_grid_editor.tt2
new file mode 100644
index 0000000..54f3da0
--- /dev/null
+++ b/Open-ILS/src/templates/staff/admin/local/t_grid_editor.tt2
@@ -0,0 +1,12 @@
+<eg-grid
+  id-field="id"
+  idl-class="{{baseFmClass}}"
+  features="-multisort,-multiselect"
+  grid-controls="gridControls"
+  auto-fields="true"
+  persist-key="admin.local.config.grideditor.{{baseFmClass}}">
+
+  <eg-grid-menu-item label="[% l('Create') %]" handler="createHandler"></eg-grid-menu-item>
+  <eg-grid-action handler="editHandler" label="[% l('Edit') %]"></eg-grid-action>
+  <eg-grid-action handler="deleteHandler" label="[% l('Delete') %]"></eg-grid-action>
+</eg-grid>
diff --git a/Open-ILS/src/templates/staff/admin/local/t_splash.tt2 b/Open-ILS/src/templates/staff/admin/local/t_splash.tt2
index 82599b3..151f2bb 100644
--- a/Open-ILS/src/templates/staff/admin/local/t_splash.tt2
+++ b/Open-ILS/src/templates/staff/admin/local/t_splash.tt2
@@ -17,6 +17,8 @@
     ,[ l('Circ Limit Sets'), "./admin/local/config/circ_limit_set" ]
     ,[ l('Circulation Policies'), "./admin/local/config/circ_matrix_matchpoint" ]
     ,[ l('Closed Dates Editor'), "./admin/local/actor/closed_dates" ]
+    ,[ l('Copy Alert Types'), "./admin/local/config/copy_alert_types" ]
+    ,[ l('Copy Alert Suppression'), "./admin/local/actor/copy_alert_suppress" ]
     ,[ l('Copy Location Groups'), "./admin/local/asset/copy_location_group" ]
     ,[ l('Copy Location Order'), "./admin/local/asset/copy_location_order" ]
     ,[ l('Copy Locations Editor'), "./admin/local/asset/copy_locations" ]
diff --git a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2 b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
index b506f3f..c6bf4e0 100644
--- a/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
+++ b/Open-ILS/src/templates/staff/cat/catalog/t_holdings.tt2
@@ -71,6 +71,8 @@
       label="[% l('Copies') %]"></eg-grid-action>
     <eg-grid-action handler="selectedHoldingsVolCopyAdd" group="[% l('Add') %]"
       label="[% l('Volumes and Copies') %]"></eg-grid-action>
+    <eg-grid-action handler="selectedHoldingsCopyAlertsAdd" group="[% l('Add') %]" disabled="vols_not_shown"
+      label="[% l('Copy Alerts') %]"></eg-grid-action>
 
     <eg-grid-action handler="selectedHoldingsVolEdit" group="[% l('Edit') %]"
       label="[% l('Volumes') %]"></eg-grid-action>
@@ -80,6 +82,8 @@
       label="[% l('Volumes and Copies') %]"></eg-grid-action>
     <eg-grid-action handler="replaceBarcodes" group="[% l('Edit') %]"
       label="[% l('Replace Barcodes') %]"></eg-grid-action>
+    <eg-grid-action handler="selectedHoldingsCopyAlertsManage" group="[% l('Edit') %]"
+      label="[% l('Manage Copy Alerts') %]"></eg-grid-action>
 
     <eg-grid-action handler="selectedHoldingsEmptyVolCopyDelete" group="[% l('Delete') %]" disabled="vols_not_shown"
       label="[% l('Empty Volumes') %]"></eg-grid-action>
@@ -118,6 +122,10 @@
     <eg-grid-field label="[% l('Holdable') %]"               datatype="bool" path="holdable"></eg-grid-field>
     <eg-grid-field label="[% l('Age-based Hold Protection') %]" path="age_protect.name"></eg-grid-field>
     <eg-grid-field label="[% l('Reference') %]"              datatype="bool" path="ref"></eg-grid-field>
+    <eg-grid-field label="[% l('Alerts') %]" path="copy_alert_count" handlers="gridCellHandlers" visible compiled>
+      {{item['copy_alert_count']}}
+      <button ng-disabled="item['copy_alert_count'] <= 0" class="btn btn-sm btn-default" ng-click="col.handlers.copyAlertsEdit(item['id'])">[% l('Manage') %]</button>
+    </eg-grid-field>
   
   </eg-grid>
 </div>
diff --git a/Open-ILS/src/templates/staff/cat/item/t_list.tt2 b/Open-ILS/src/templates/staff/cat/item/t_list.tt2
index eb584c9..5a5d5be 100644
--- a/Open-ILS/src/templates/staff/cat/item/t_list.tt2
+++ b/Open-ILS/src/templates/staff/cat/item/t_list.tt2
@@ -46,6 +46,8 @@
     label="[% l('Items') %]"></eg-grid-action>
   <eg-grid-action handler="selectedHoldingsVolCopyAdd" group="[% l('Add') %]"
     label="[% l('Volumes and Items') %]"></eg-grid-action>
+  <eg-grid-action handler="selectedHoldingsCopyAlertsAdd" group="[% l('Add') %]"
+    label="[% l('Copy Alerts') %]"></eg-grid-action>
 
   <eg-grid-action handler="selectedHoldingsVolEdit" group="[% l('Edit') %]"
     label="[% l('Volumes') %]"></eg-grid-action>
@@ -55,6 +57,8 @@
     label="[% l('Volumes and Items') %]"></eg-grid-action>
   <eg-grid-action handler="replaceBarcodes" group="[% l('Edit') %]"
     label="[% l('Replace Barcodes') %]"></eg-grid-action>
+  <eg-grid-action handler="selectedHoldingsCopyAlertsEdit" group="[% l('Edit') %]"
+    label="[% l('Manage Copy Alerts') %]"></eg-grid-action>
 
   <eg-grid-action handler="changeItemOwningLib" group="[% l('Transfer') %]"
     label="[% l('Items to Previously Marked Library') %]"></eg-grid-action>
@@ -75,7 +79,6 @@
     </a>
   </eg-grid-field>
 
-
   <eg-grid-field label="[% l('Acquisition Cost') %]"     path="cost" hidden></eg-grid-field>
   <eg-grid-field label="[% l('Age-Based Hold Protection') %]"  path="age_protect" hidden></eg-grid-field>
   <eg-grid-field label="[% l('Author') %]"               path="call_number.record.simple_record.author"  hidden></eg-grid-field>
@@ -127,7 +130,11 @@
   <eg-grid-field label="[% l('TCN') %]"                   path="call_number.record.tcn_value" hidden></eg-grid-field>
   <eg-grid-field label="[% l('TCN Source') %]"            path="call_number.record.tcn_source" hidden></eg-grid-field>
   <eg-grid-field label="[% l('Transaction Complete') %]"  path="_circ.xact_finish" datatype="timestamp" hidden></eg-grid-field>
-</eg-grid>
+  <eg-grid-field label="[% l('Alerts') %]" path="copy_alert_count" handlers="gridCellHandlers" visible compiled>
+    {{item['copy_alert_count']}}
+    <button ng-disabled="item['copy_alert_count'] <= 0" class="btn btn-sm btn-default" ng-click="col.handlers.copyAlertsEdit(item['id'])">[% l('Manage') %]</button>
+  </eg-grid-field>
+  
 </eg-grid>
 
 <div class="flex-row pad-vert">
diff --git a/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2 b/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2
index 4b9abe3..9e37939 100644
--- a/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2
+++ b/Open-ILS/src/templates/staff/cat/item/t_summary_pane.tt2
@@ -177,8 +177,11 @@
   </div>
 
   <div class="flex-row">
-    <div class="flex-cell">[% l('Alert Message') %]</div>
-    <div class="well" style="flex:7">{{copy.alert_message()}}</div>
+    <div class="flex-cell">[% l('Copy Alerts') %]</div>
+    <div id="item-status-alert-msg">
+      <button class="btn btn-default" ng-click="addCopyAlerts(copy.id())" >[% l('Add') %]</button>
+      <button class="btn btn-default" ng-click="manageCopyAlerts(copy.id())" >[% l('Manage') %]</button>
+    </div>
   </div>
 
 </div>
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
index 5b9181e..3d60493 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_attr_edit.tt2
@@ -384,6 +384,13 @@
                       type="button">
                         [% l('Copy Notes') %]
                     </button>
+                    <button
+                      class="btn btn-default"
+                      ng-disabled="!defaults.copy_alerts"
+                      ng-click="copy_alerts_dialog(workingGridControls.selectedItems())"
+                      type="button">
+                        [% l('Copy Alerts') %]
+                    </button>
                 </div>
             </div>
 
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_copy_alerts.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_copy_alerts.tt2
new file mode 100644
index 0000000..b86fef3
--- /dev/null
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_copy_alerts.tt2
@@ -0,0 +1,94 @@
+<form ng-submit="ok(copy_alert)" role="form">
+    <div class="modal-header">
+      <button type="button" class="close" ng-click="cancel()" 
+        aria-hidden="true">×</button>
+      <h4 class="modal-title">[% l('New Copy Alert') %]</h4>
+    </div>
+    <div class="modal-body">
+      <div class="row">
+        <div class="col-md-6 form-inline">
+          <label for="copy-alert-type-selector"> [% l('Type') %]</label>
+          <select id="copy-alert-type-selector" class="form-control"
+            ng-model="copy_alert.alert_type"
+            ng-options="at.id() as at.name() for at in alert_types">
+          </select>
+        </div>
+        <div class="col-md-3">
+          <label>
+            <input type="checkbox" ng-model="copy_alert.temp"/>
+            [% l('Temporary') %]
+          </label>
+        </div>
+      </div>
+      <div class="row pad-vert">
+        <div class="col-md-12">
+          <textarea class="form-control" 
+            ng-model="copy_alert.note" placeholder="[% l('Alert...') %]">
+          </textarea>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <div class="row">
+        <div class="col-md-10 pull-right">
+          <input type="submit" class="btn btn-primary" value="[% l('OK') %]"/>
+          <button class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
+        </div>
+      </div>
+
+      <div class="row pad-vert" ng-if="copy_alert_list.length > 0"> 
+        <div class="col-md-12">
+          <div class="row">
+            <div class="col-md-12">
+              <hr/>
+            </div>
+          </div>
+          <div class="row">
+            <div class="col-md-12">
+              <h4 class="pull-left">[% l('Existing Copy Alerts') %]</h4>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="row" ng-repeat="a in copy_alert_list" ng-init="temp = (a.temp() == 't'); note = a.note(); acked = (a.ack_time() !== null); alert_type = a.alert_type().id()">
+        <div class="col-md-12">
+          <div class="row">
+            <div class="col-md-6 form-inline">
+              <label for="copy-alert-type-select-{{a.id()}}">[% l('Type') %]</label>
+              <select id="copy-alert-type-select-{{a.id()}}" class="form-control"
+                      ng-model="alert_type"
+                      ng-change="a.alert_type(alert_type) && a.ischanged(1)"
+                      ng-options="at.id() as at.name() for at in alert_types">
+              </select>
+            </div>
+            <div class="col-md-3">
+              <label>
+                <input type="checkbox" ng-model="temp" ng-change="a.temp(temp ? 't' : 'f') && a.ischanged(1)" ng-disabled="acked"/>
+                [% l('Temporary') %]
+              </label>
+            </div>
+            <div class="col-md-3">
+              <label>
+                <input type="checkbox" ng-model="acked" ng-change="(acked ? a.ack_time('now') : a.ack_time(null)) && a.ischanged(1)"/>
+                [% l('Clear?') %]
+              </label>
+            </div>
+          </div>
+          <div class="row pad-vert">
+            <div class="col-md-12">
+              <textarea class="form-control" ng-change="a.note(note) && a.ischanged(1)"
+                ng-model="note" placeholder="[% l('Alert...') %]" ng-disabled="acked">
+              </textarea>
+            </div>
+          </div>
+          <div class="row">
+            <div class="col-md-12">
+              <hr/>
+            </div>
+          </div>
+        </div>
+      </div>
+
+    </div>
+</form>
diff --git a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2 b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
index 89e4ebe..77316e6 100644
--- a/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
+++ b/Open-ILS/src/templates/staff/cat/volcopy/t_defaults.tt2
@@ -310,6 +310,15 @@
                     </label>
                 </div>
             </div>
+
+            <div class="row">
+                <div class="col-xs-6">
+                    <label>
+                        <input type="checkbox" ng-change="saveDefaults()" ng-model="defaults.copy_alerts"/>
+                        [% l('Add/Edit Copy Alerts') %]
+                    </label>
+                </div>
+            </div>
         </div>
 
         <div class="col-md-4">
diff --git a/Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2 b/Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2
index 8ff4853..6c7aab0 100644
--- a/Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2
+++ b/Open-ILS/src/templates/staff/circ/checkin/t_checkin_table.tt2
@@ -31,6 +31,15 @@
     handler="abortTransit"
     label="[% l('Cancel Transits') %]">
   </eg-grid-action>
+  <eg-grid-action 
+    handler="addCopyAlerts"
+    label="[% l('Add Copy Alerts') %]">
+  </eg-grid-action>
+  <eg-grid-action 
+    handler="manageCopyAlerts"
+    label="[% l('Manage Copy Alerts') %]">
+  </eg-grid-action>
+
   <!-- Show Group -->
   <eg-grid-action handler="showBibHolds" group="[% l('Show') %]"
     label="[% l('Record Holds') %]">
@@ -47,8 +56,6 @@
   <eg-grid-action handler="printSpineLabels" group="[% l('Print') %]"
     label="[% l('Spine Labels') %]">
   </eg-grid-action>
-  <eg-grid-field label="[% l('Alert Msg') %]"   
-    path="acp.alert_message"></eg-grid-field>
 
   <eg-grid-field label="[% l('Balance Owed') %]"     
     path='mbts.balance_owed' comparator="sort_money"></eg-grid-field>
diff --git a/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2
index ebcfb44..f52451d 100644
--- a/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2
+++ b/Open-ILS/src/templates/staff/circ/patron/t_checkout.tt2
@@ -93,8 +93,14 @@
   persist-key="circ.patron.checkout"
   dateformat="{{$root.egDateAndTimeFormat}}">
 
-  <eg-grid-field label="[% l('Alert Msg') %]"   
-    path="acp.alert_message"></eg-grid-field>
+  <eg-grid-action
+    handler="addCopyAlerts"
+    label="[% l('Add Copy Alerts') %]">
+  </eg-grid-action>
+  <eg-grid-action
+    handler="manageCopyAlerts"
+    label="[% l('Manage Copy Alerts') %]">
+  </eg-grid-action>
 
   <eg-grid-field label="[% l('Balance Owed') %]"     
     path='mbts.balance_owed'></eg-grid-field>
@@ -138,6 +144,12 @@
   <eg-grid-field path="acp.circ_modifier.name" label="[% l('Circulation Modifier') %]"></eg-grid-field>
   <eg-grid-field path="acp.circ_lib.shortname" label="[% l('Circulation Library') %]"></eg-grid-field>
   <eg-grid-field path="acn.owning_lib.shortname" label="[% l('Owning Library') %]"></eg-grid-field>
+
+  <eg-grid-field label="[% l('Alerts') %]" path="copy_alert_count" handlers="gridCellHandlers" visible compiled>
+    {{item['copy_alert_count']}}
+    <button ng-disabled="item['copy_alert_count'] <= 0" class="btn btn-sm btn-default" ng-click="col.handlers.copyAlertsEdit(item['acp'].id())">[% l('Manage') %]</button>
+  </eg-grid-field>
+
   <eg-grid-field path="circ.*" parent-idl-class="circ" hidden></eg-grid-field>
   <eg-grid-field path="acp.*" parent-idl-class="acp" hidden></eg-grid-field>
   <eg-grid-field path="acn.*" parent-idl-class="acn" hidden></eg-grid-field>
diff --git a/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2 b/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2
index 344e851..41866f4 100644
--- a/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2
+++ b/Open-ILS/src/templates/staff/circ/renew/t_renew.tt2
@@ -66,11 +66,17 @@
     handler="abortTransit"
     label="[% l('Cancel Transits') %]">
   </eg-grid-action>
+  <eg-grid-action divider="true"></eg-grid-action>
+  <eg-grid-action
+    handler="addCopyAlerts"
+    label="[% l('Add Copy Alerts') %]">
+  </eg-grid-action>
+  <eg-grid-action
+    handler="manageCopyAlerts"
+    label="[% l('Manage Copy Alerts') %]">
+  </eg-grid-action>
 
 
-  <eg-grid-field label="[% l('Alert Msg') %]"   
-    path="acp.alert_message"></eg-grid-field>
-
   <eg-grid-field label="[% l('Balance Owed') %]"     
     path='mbts.balance_owed' comparator="sort_money"></eg-grid-field>
 
diff --git a/Open-ILS/src/templates/staff/circ/share/circ_strings.tt2 b/Open-ILS/src/templates/staff/circ/share/circ_strings.tt2
index 6ad5c78..6675b99 100644
--- a/Open-ILS/src/templates/staff/circ/share/circ_strings.tt2
+++ b/Open-ILS/src/templates/staff/circ/share/circ_strings.tt2
@@ -34,7 +34,29 @@ s.COPY_IN_TRANSIT = '[% l("Copy is In-Transit") %]';
 s.TOO_MANY_CLAIMS_RETURNED = 
   '[% l("Patron exceeds claims returned count.  Force this action?") %]';
 s.MARK_NEVER_CHECKED_OUT = 
-  '[% l("Mark Never Checked Out: [_1]", "{{barcodes.toString()}}") %]'
+  '[% l("Mark Never Checked Out: [_1]", "{{barcodes.toString()}}") %]';
+s.ON_DEMAND_COPY_ALERT = {
+    'CHECKIN': {
+        'NORMAL' : '[% l("Normal checkin") %]',
+        'LOST' : '[% l("Copy was marked lost") %]',
+        'LOST_AND_PAID' : '[% l("Copy was marked lost and paid for") %]',
+        'MISSING' : '[% l("Copy was marked missing") %]',
+        'DAMAGED' : '[% l("Copy was marked damaged") %]',
+        'CLAIMSRETURNED' : '[% l("Copy was marked claims returned") %]',
+        'LONGOVERDUE' : '[% l("Copy was marked long overdue") %]',
+        'CLAIMSNEVERCHECKEDOUT' : '[% l("Copy was marked claims never checked out") %]'
+    },
+    'CHECKOUT': {
+        'NORMAL' : '[% l("Normal checkout") %]',
+        'LOST' : '[% l("Copy was marked lost") %]',
+        'LOST_AND_PAID' : '[% l("Copy was marked lost and paid for") %]',
+        'MISSING' : '[% l("Copy was marked missing") %]',
+        'DAMAGED' : '[% l("Copy was marked damaged") %]',
+        'CLAIMSRETURNED' : '[% l("Copy was marked claims returned") %]',
+        'LONGOVERDUE' : '[% l("Copy was marked long overdue") %]',
+        'CLAIMSNEVERCHECKEDOUT' : '[% l("Copy was marked claims never checked out") %]'
+    }
+};
 }]);
 </script>
 
diff --git a/Open-ILS/src/templates/staff/css/style.css.tt2 b/Open-ILS/src/templates/staff/css/style.css.tt2
index b20f720..7d32275 100644
--- a/Open-ILS/src/templates/staff/css/style.css.tt2
+++ b/Open-ILS/src/templates/staff/css/style.css.tt2
@@ -179,6 +179,10 @@ table.list tr.selected td { /* deprecated? */
 /* barcode inputs are everywhere.  Let's have a consistent style. */
 .barcode { width: 16em !important; }
 
+/* use strike-through to mark something that has been acknowledged,
+   e.g., a copy alert */
+.acknowledged { text-decoration: line-through; }
+
 /* bootstrap alerts are heavily padded.  use this to reduce */
 .alert-less-pad {padding: 5px;}
 
diff --git a/Open-ILS/src/templates/staff/share/t_add_copy_alert_dialog.tt2 b/Open-ILS/src/templates/staff/share/t_add_copy_alert_dialog.tt2
new file mode 100644
index 0000000..0b97a3f
--- /dev/null
+++ b/Open-ILS/src/templates/staff/share/t_add_copy_alert_dialog.tt2
@@ -0,0 +1,40 @@
+<form ng-submit="ok(copy_alert)" role="form">
+    <div class="modal-header">
+      <button type="button" class="close" ng-click="cancel()" 
+        aria-hidden="true">×</button>
+      <h4 class="modal-title">[% l('Add Copy Alert') %]</h4>
+    </div>
+    <div class="modal-body">
+      <div class="row">
+        <div class="col-md-6 form-inline">
+          <label for="copy-alert-type-selector"> [% l('Type') %]</label>
+          <select id="copy-alert-type-selector" class="form-control"
+            ng-model="copy_alert.alert_type"
+            ng-options="at.id() as at.name() for at in alert_types">
+          </select>
+        </div>
+        <div class="col-md-3">
+          <label>
+            <input type="checkbox" ng-model="copy_alert.temp"/>
+            [% l('Temporary') %]
+          </label>
+        </div>
+      </div>
+      <div class="row pad-vert">
+        <div class="col-md-12">
+          <textarea class="form-control" 
+            ng-model="copy_alert.note" placeholder="[% l('Alert...') %]">
+          </textarea>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <div class="row">
+        <div class="col-md-10 pull-right">
+          <input type="submit" class="btn btn-primary" value="[% l('OK') %]"/>
+          <button class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
+        </div>
+      </div>
+
+    </div>
+</form>
diff --git a/Open-ILS/src/templates/staff/share/t_autogrid.tt2 b/Open-ILS/src/templates/staff/share/t_autogrid.tt2
index c6b45d9..4a91d0c 100644
--- a/Open-ILS/src/templates/staff/share/t_autogrid.tt2
+++ b/Open-ILS/src/templates/staff/share/t_autogrid.tt2
@@ -215,7 +215,7 @@
 <div class="eg-grid" ng-class="{'eg-grid-as-conf' : showGridConf}">
 
   <!-- import our eg-grid-field defs -->
-  <div ng-transclude></div>
+  <div style="display: none;" ng-transclude></div>
 
   <div class="eg-grid-row eg-grid-header-row">
     <div class="eg-grid-cell eg-grid-cell-stock" ng-show="showIndex">
@@ -315,10 +315,14 @@
 
           <!-- if the cell comes with its own template,
                translate that content into HTML and insert it here -->
-          <span ng-if="col.template" style="padding-left:5px; padding-right:10px;"
+          <span ng-if="col.template && !col.compiled" style="padding-left:5px; padding-right:10px;"
             ng-bind-html="translateCellTemplate(col, item)">
           </span>
 
+          <span ng-if="col.template && col.compiled" style="padding-left:5px; padding-right:10px;"
+            compile="col.template">
+          </span>
+
           <!-- otherwise, simply display the item value, which may 
                pass through datatype-specific filtering. -->
           <span ng-if="!col.template" style="padding-left:5px; padding-right:10px;">
diff --git a/Open-ILS/src/templates/staff/share/t_copy_alert_editor_dialog.tt2 b/Open-ILS/src/templates/staff/share/t_copy_alert_editor_dialog.tt2
new file mode 100644
index 0000000..aad1a6b
--- /dev/null
+++ b/Open-ILS/src/templates/staff/share/t_copy_alert_editor_dialog.tt2
@@ -0,0 +1,55 @@
+<form ng-submit="ok(copy_alert)" role="form">
+    <div class="modal-header">
+      <button type="button" class="close" ng-click="cancel()" 
+        aria-hidden="true">×</button>
+      <h4 class="modal-title">[% l('Manage Copy Alerts') %]</h4>
+    </div>
+    <div class="modal-body">
+      <div class="row" ng-repeat="a in copy_alert_list" ng-init="temp = (a.temp() == 't'); note = a.note(); acked = (a.ack_time() !== null); alert_type = a.alert_type().id()">
+        <div class="col-md-12">
+          <div class="row">
+            <div class="col-md-6 form-inline">
+              <label for="copy-alert-type-select-{{a.id()}}">[% l('Type') %]</label>
+              <select id="copy-alert-type-select-{{a.id()}}" class="form-control"
+                      ng-model="alert_type"
+                      ng-change="a.alert_type(alert_type) && a.ischanged(1)"
+                      ng-options="at.id() as at.name() for at in alert_types">
+              </select>
+            </div>
+            <div class="col-md-3">
+              <label>
+                <input type="checkbox" ng-model="temp" ng-change="a.temp(temp ? 't' : 'f') && a.ischanged(1)" ng-disabled="acked"/>
+                [% l('Temporary') %]
+              </label>
+            </div>
+            <div class="col-md-3">
+              <label>
+                <input type="checkbox" ng-model="acked" ng-change="(acked ? a.ack_time('now') : a.ack_time(null)) && a.ischanged(1)"/>
+                [% l('Clear?') %]
+              </label>
+            </div>
+          </div>
+          <div class="row pad-vert">
+            <div class="col-md-12">
+              <textarea class="form-control" ng-change="a.note(note) && a.ischanged(1)"
+                ng-model="note" placeholder="[% l('Alert...') %]" ng-disabled="acked">
+              </textarea>
+            </div>
+          </div>
+          <div class="row">
+            <div class="col-md-12">
+              <hr/>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <div class="row">
+        <div class="col-md-10 pull-right">
+          <input type="submit" class="btn btn-primary" value="[% l('OK') %]"/>
+          <button class="btn btn-warning" ng-click="cancel($event)">[% l('Cancel') %]</button>
+        </div>
+      </div>
+    </div>
+</form>
diff --git a/Open-ILS/src/templates/staff/share/t_copy_alert_manager_dialog.tt2 b/Open-ILS/src/templates/staff/share/t_copy_alert_manager_dialog.tt2
new file mode 100644
index 0000000..843ea0d
--- /dev/null
+++ b/Open-ILS/src/templates/staff/share/t_copy_alert_manager_dialog.tt2
@@ -0,0 +1,49 @@
+<!--
+  Copy alert manager dialog
+-->
+<div>
+  <div class="modal-header">
+    <h4 class="modal-title alert alert-info">[% l('Copy alerts') %]</h4> 
+  </div>
+  <div class="modal-body">
+    <div>
+      <div class="row" ng-repeat="alert in alerts" style="border-bottom: 1px solid grey; margin-top: 3px;">
+        <div class="col-md-2">{{alert.evt}}</div>
+        <div class="col-md-8" ng-class="{ acknowledged: isAcknowledged(alert) }">{{alert.message}}</div>
+        <div class="col-md-2">
+          <button ng-if="canBeAcknowledged(alert)"
+                  class="btn btn-xs btn-default"
+                  ng-click="alert.acked = !alert.acked" >[% l('Clear') %]</button>
+          <button ng-if="canBeRemoved(alert) && mode == 'manage'"
+                  class="btn btn-xs btn-default"
+                  ng-click="alert.acked = !alert.acked" >[% l('Remove') %]</button>
+        </div>
+      </div>
+    </div>
+    <div ng-if="mode == 'checkin' && next_statuses.length > 0">
+        <div ng-if="next_statuses.length == 1" class="row">
+            <div class="col-md-8">
+                <b>[% l('Next copy status: ') %]</b> {{next_statuses[0].name()}}
+            </div>
+        </div>
+        <div ng-if="next_statuses.length > 1" class="row">
+          <div class="col-md-4">
+            <label for="select-next-status"><b>[% l('Next copy status') %]<b></label>
+          </div>
+          <div class="col-md-4">
+            <select id="select-next-status" class="form-control"
+                    ng-model="params.the_next_status" focus-me="true"
+                    ng-options="st.id() as st.name() for st in next_statuses">
+            </select>
+          </select>
+        </div>
+    </div>
+  </div>
+  <div class="modal-footer">
+    [% dialog_footer %]
+    <input type="submit" class="btn btn-primary" 
+      ng-click="ok()" value="[% l('OK/Continue') %]"/>
+    <button class="btn btn-warning" 
+      ng-click="cancel()">[% l('Cancel') %]</button>
+  </div>
+</div>
diff --git a/Open-ILS/web/js/ui/default/staff/admin/local/app.js b/Open-ILS/web/js/ui/default/staff/admin/local/app.js
index f2b286f..8758257 100644
--- a/Open-ILS/web/js/ui/default/staff/admin/local/app.js
+++ b/Open-ILS/web/js/ui/default/staff/admin/local/app.js
@@ -1,5 +1,5 @@
 angular.module('egLocalAdmin',
-    ['ngRoute', 'ui.bootstrap', 'egCoreMod','egUiMod'])
+    ['ngRoute', 'ui.bootstrap', 'egCoreMod','egUiMod','egGridMod'])
 
 .config(['$routeProvider','$locationProvider','$compileProvider', 
  function($routeProvider , $locationProvider , $compileProvider) {
@@ -52,6 +52,29 @@ angular.module('egLocalAdmin',
         resolve : resolver
     });
 
+    $routeProvider.when('/admin/local/config/copy_alert_types', {
+        templateUrl: './admin/local/t_grid_editor',
+        controller: 'AutoGridEditorCtl',
+        fmBase: 'ccat',
+        createEditPrefetch: {
+            ccs : { id : {'!=' : null} }
+        },
+        createDefaults : { 'in_renew' : 'f', 'next_status' : [] },
+        createEditOrgExpand: ['scope_org'],
+        createEditIntarray: ['next_status'],
+        createEditNullableBool : ['in_renew', 'at_circ', 'at_owning']
+    });
+
+    $routeProvider.when('/admin/local/actor/copy_alert_suppress', {
+        templateUrl: './admin/local/t_grid_editor',
+        controller: 'AutoGridEditorCtl',
+        fmBase: 'acas',
+        createEditPrefetch: {
+            ccat : { active: 't' }
+        },
+        createEditOrgExpand: ['org']
+    });
+
     // Conify page handler
     $routeProvider.when('/admin/local/:schema/:page', {
         template: eframe_template,
@@ -106,3 +129,138 @@ function($scope , $location , egCore , $timeout) {
     $scope.local_admin_url = $location.absUrl().replace(/\/.*/, url);
 }])
 
+.controller('AutoGridEditorCtl',
+       ['$scope','$route','$location','egCore','$timeout','egConfirmDialog','$uibModal',
+function($scope , $route , $location , egCore , $timeout , egConfirmDialog , $uibModal) {
+
+    $scope.funcs = {};
+
+    $scope.baseFmClass = $route.current.$$route.fmBase;
+    $scope.createEditPrefetch = $route.current.$$route.createEditPrefetch || {};
+    $scope.createEditOrgExpand = $route.current.$$route.createEditOrgExpand || [];
+    $scope.createEditNullableBool = $route.current.$$route.createEditNullableBool || [];
+    $scope.createEditIntarray = $route.current.$$route.createEditIntarray || [];
+    $scope.createDefaults = $route.current.$$route.createDefaults || {};
+    $scope.gridControls = {
+        setQuery : function(q) {
+            if (q) query = q;
+            return query;
+        },
+        activateItem : function (item) {
+            $scope.editHandler([item])
+        }
+    };
+    $scope.gridControls.setQuery({id : {'!=' : null}});
+
+    function openCreateEditDialog(id) {
+        return $uibModal.open({
+            templateUrl : './admin/local/autoGridEditor/' + $scope.baseFmClass,
+            scope : $scope,
+            controller :
+                ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
+                $scope.creating = id ? false : true;
+                angular.forEach($scope.$parent.createEditPrefetch, function(where, fmClass) {
+                    egCore.pcrud.search(
+                        fmClass, where, {},
+                        {atomic : true, authoritative : true}
+                    ).then(function(vals) {
+                        $scope[fmClass] = vals;
+                    });
+                });
+                if ($scope.creating) {
+                    $scope.record = $scope.createDefaults;
+                } else {
+                    egCore.pcrud.retrieve($scope.baseFmClass, id).then(function(to_edit) {
+                        $scope.record = egCore.idl.toHash(to_edit);
+                        angular.forEach($scope.createEditOrgExpand, function(ou_field) {
+                            $scope.record[ou_field] = egCore.org.get($scope.record[ou_field]);
+                        });
+                        angular.forEach($scope.createEditIntarray, function(intarray_field) {
+                            if (!($scope.record[intarray_field] == null) && $scope.record[intarray_field] != "") {
+                                $scope.record[intarray_field] = $scope.record[intarray_field]
+                                                    .replace('{', '')
+                                                    .replace('}', '')
+                                                    .split(',');
+                            } else {
+                                $scope.record[intarray_field] = [];
+                            }
+                        });
+                    });
+                }
+                $scope.ok = function(record) { $uibModalInstance.close(record) };
+                $scope.cancel = function () { $uibModalInstance.dismiss() }
+            }]
+        });
+    }
+
+    $scope.createHandler = function() {
+        openCreateEditDialog().result.then(function(record) {
+            var newRec = new egCore.idl[$scope.baseFmClass]();
+            angular.forEach(record, function(val, key) {
+                if (typeof(val) === 'object' && !angular.isArray(val)) {
+                    newRec[key](val.id());
+                } else {
+                    newRec[key](val);
+                }
+            });
+            angular.forEach($scope.createEditNullableBool, function(nb_field) {
+                if (!(record[nb_field] == null) && record[nb_field] == "")
+                    newRec[nb_field](null);
+            });
+            angular.forEach($scope.createEditIntarray, function(intarray_field) {
+                if (newRec[intarray_field]().length > 0) {
+                    newRec[intarray_field]('{' + newRec[intarray_field]().join(',') + '}');
+                } else {
+                    newRec[intarray_field](null);
+                }
+            });
+            return egCore.pcrud.create(newRec);
+        }).then(function(){
+            $scope.gridControls.refresh();
+        });
+    };
+    $scope.editHandler = function(items) {
+        openCreateEditDialog(items[0].id).result.then(function(record) {
+            var editedRec = new egCore.idl[$scope.baseFmClass]();
+            angular.forEach(record, function(val, key) {
+                if (angular.isObject(val) && !angular.isArray(val)) {
+                    editedRec[key](val.id());
+                } else {
+                    editedRec[key](val);
+                }
+            });
+            angular.forEach($scope.createEditNullableBool, function(nb_field) {
+                if (!(record[nb_field] == null) && record[nb_field] == "")
+                    editedRec[nb_field](null);
+            });
+            angular.forEach($scope.createEditIntarray, function(intarray_field) {
+                if (editedRec[intarray_field]().length > 0) {
+                    editedRec[intarray_field]('{' + editedRec[intarray_field]().join(',') + '}');
+                } else {
+                    editedRec[intarray_field](null);
+                }
+            });
+            return egCore.pcrud.update(editedRec);
+        }).then(function(){
+            $scope.gridControls.refresh();
+        });
+    };
+    $scope.deleteHandler = function(items) {
+        egConfirmDialog.open(
+            egCore.strings.REMOVE_ITEM_CONFIRM,
+            '',
+            {}
+        ).result.then(function() {
+            var ids = items.map(function(s){ return s.id });
+            egCore.pcrud.search(
+                $scope.baseFmClass, {id : ids}, {},
+                {atomic : true, authoritative : true}
+            ).then(function(to_delete) {
+                return egCore.pcrud.remove(to_delete);
+            }).then(function() {
+                $scope.gridControls.refresh();
+            });
+        });
+    };
+}])
+
diff --git a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
index 63b9c04..4b3d186 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
@@ -1477,6 +1477,13 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         });
     }
 
+    $scope.gridCellHandlers = {};
+    $scope.gridCellHandlers.copyAlertsEdit = function(id) {
+        egCirc.manage_copy_alerts([id]).then(function() {
+            // update grid items?
+        });
+    };
+
     $scope.transferItems = function (){
         var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
         var copy_ids = gatherSelectedHoldingsIds();
@@ -1586,6 +1593,17 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         });
     }
 
+    $scope.selectedHoldingsCopyAlertsAdd = function() {
+        egCirc.add_copy_alerts(gatherSelectedHoldingsIds()).then(function() {
+            // no need to refresh grid
+        });
+    }
+    $scope.selectedHoldingsCopyAlertsManage = function() {
+        egCirc.manage_copy_alerts(gatherSelectedHoldingsIds()).then(function() {
+            // no need to refresh grid
+        });
+    }
+
     $scope.attach_to_peer_bib = function() {
         var copy_list = gatherSelectedHoldingsIds();
         if (copy_list.length == 0) return;
diff --git a/Open-ILS/web/js/ui/default/staff/cat/item/app.js b/Open-ILS/web/js/ui/default/staff/cat/item/app.js
index be8e797..2a3d7a7 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/item/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/item/app.js
@@ -455,6 +455,33 @@ function($scope , $q , $routeParams , $location , $timeout , $window , egCore ,
         itemSvc.spawnHoldingsAdd(copyGrid.selectedItems(),false,true);
     }
 
+    $scope.selectedHoldingsCopyAlertsAdd = function(items) {
+        var copy_ids = [];
+        angular.forEach(items, function(item) {
+            if (item.id) copy_ids.push(item.id);
+        });
+        egCirc.add_copy_alerts(copy_ids).then(function() {
+            // update grid items?
+        });
+    }
+
+    $scope.selectedHoldingsCopyAlertsEdit = function(items) {
+        var copy_ids = [];
+        angular.forEach(items, function(item) {
+            if (item.id) copy_ids.push(item.id);
+        });
+        egCirc.manage_copy_alerts(copy_ids).then(function() {
+            // update grid items?
+        });
+    }
+
+    $scope.gridCellHandlers = {};
+    $scope.gridCellHandlers.copyAlertsEdit = function(id) {
+        egCirc.manage_copy_alerts([id]).then(function() {
+            // update grid items?
+        });
+    };
+
     $scope.showBibHolds = function () {
         angular.forEach(gatherSelectedRecordIds(), function (r) {
             var url = egCore.env.basePath + 'cat/catalog/record/' + r + '/holds';
@@ -522,8 +549,8 @@ function($scope , $q , $routeParams , $location , $timeout , $window , egCore ,
  * Detail view -- shows one copy
  */
 .controller('ViewCtrl', 
-       ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','egItem','egBilling',
-function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling) {
+       ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','egItem','egBilling','egCirc',
+function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling , egCirc) {
     var copyId = $routeParams.id;
     $scope.args.copyId = copyId;
     $scope.tab = $routeParams.tab || 'summary';
@@ -940,6 +967,17 @@ function($scope , $q , $location , $routeParams , $timeout , $window , egCore ,
         return;
     }
 
+    $scope.addCopyAlerts = function(copy_id) {
+        egCirc.add_copy_alerts([copy_id]).then(function() {
+            // update grid items?
+        });
+    }
+    $scope.manageCopyAlerts = function(copy_id) {
+        egCirc.manage_copy_alerts([copy_id]).then(function() {
+            // update grid items?
+        });
+    }
+
     $scope.context.toggleDisplay = function() {
         $location.path('/cat/item/search');
     }
diff --git a/Open-ILS/web/js/ui/default/staff/cat/services/holdings.js b/Open-ILS/web/js/ui/default/staff/cat/services/holdings.js
index f0e0185..e6cc145 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/services/holdings.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/services/holdings.js
@@ -15,7 +15,7 @@ function(egCore , $q) {
     service.prototype.flesh = {   
         flesh : 2, 
         flesh_fields : {
-            acp : ['status','location','circ_lib','parts','age_protect'],
+            acp : ['status','location','circ_lib','parts','age_protect','copy_alerts'],
             acn : ['prefix','suffix','copies']
         }
     }
@@ -117,6 +117,11 @@ function(egCore , $q) {
                     }
                 });
 
+                // create virtual field for copy alert count
+                angular.forEach(svc.copies, function (cp) {
+                    cp.copy_alert_count = cp.copy_alerts.length;
+                });
+
                 // create a label using just the unique part of the owner list
                 var index = 0;
                 var prev_owner_list;
diff --git a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
index 39e4aaf..03cde64 100644
--- a/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
+++ b/Open-ILS/web/js/ui/default/staff/cat/volcopy/app.js
@@ -136,6 +136,21 @@ function(egCore , $q) {
         );
     };
 
+    service.get_copy_alert_types = function(orgs) {
+        return egCore.pcrud.search('ccat',
+            { active : 't' },
+            {},
+            { atomic : true }
+        );
+    };
+
+    service.get_copy_alerts = function(copy_id) {
+        return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null },
+            { flesh : 1, flesh_fields : { aca : ['alert_type'] } },
+            { atomic : true }
+        );
+    };
+
     service.get_locations = function(orgs) {
         return egCore.pcrud.search('acpl',
             {owning_lib : orgs, deleted : 'f'},
@@ -374,6 +389,10 @@ function(egCore , $q) {
 
         if (!cp.parts()) cp.parts([]); // just in case...
 
+        service.get_copy_alerts(cp.id()).then(function(aca) {
+            cp.copy_alerts(aca);
+        });
+
         var lib = cp.call_number().owning_lib();
         var cn = cp.call_number().id();
 
@@ -907,6 +926,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         statcats : true,
         copy_notes : true,
         copy_tags : true,
+        copy_alerts : true,
         attributes : {
             status : true,
             loan_duration : true,
@@ -1101,6 +1121,58 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         statcat_filter: undefined
     };
 
+    $scope.copyAlertUpdate = function (alerts) {
+        if (!$scope.in_item_select &&
+            $scope.workingGridControls &&
+            $scope.workingGridControls.selectedItems) {
+            itemSvc.get_copy_alert_types().then(function(ccat) {
+                var ccat_map = {};
+                $scope.alert_types = ccat;
+                angular.forEach(ccat, function(t) {
+                    ccat_map[t.id()] = t;
+                });
+                angular.forEach(
+                    $scope.workingGridControls.selectedItems(),
+                    function (cp) {
+                        $scope.dirty = true;
+                        angular.forEach(alerts, function(alrt) {
+                            var a = egCore.idl.fromHash('aca', alrt);
+                            a.isnew(1);
+                            a.create_staff(egCore.auth.user().id());
+                            a.alert_type(ccat_map[a.alert_type()]);
+                            a.ack_time(null);
+                            a.copy(cp.id());
+                            cp.copy_alerts().push( a );
+                        });
+                        cp.ischanged(1);
+                    }
+                );
+            });
+        }
+    };
+
+    $scope.copyNoteUpdate = function (notes) {
+        if (!$scope.in_item_select &&
+            $scope.workingGridControls &&
+            $scope.workingGridControls.selectedItems) {
+            angular.forEach(
+                $scope.workingGridControls.selectedItems(),
+                function (cp) {
+                    $scope.dirty = true;
+                    angular.forEach(notes, function(note) {
+                        var n = egCore.idl.fromHash('acpn', note);
+                        n.isnew(1);
+                        n.creator(egCore.auth.user().id());
+                        n.owning_copy(cp.id());
+                        cp.notes().push( n );
+                    });
+                    cp.ischanged(1);
+                }
+            );
+
+        }
+    }
+
     $scope.statcatUpdate = function (id) {
         var newval = $scope.working.statcats[id];
 
@@ -1184,6 +1256,10 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
             angular.forEach($scope.templates[n], function (v,k) {
                 if (k == 'circ_lib') {
                     $scope.working[k] = egCore.org.get(v);
+                } else if (k == 'copy_notes' && v.length) {
+                    $scope.copyNoteUpdate(v);
+                } else if (k == 'copy_alerts' && v.length) {
+                    $scope.copyAlertUpdate(v);
                 } else if (!angular.isObject(v)) {
                     $scope.working[k] = angular.copy(v);
                 } else {
@@ -1936,6 +2012,75 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         });
     }
 
+    $scope.copy_alerts_dialog = function(copy_list) {
+        if (!angular.isArray(copy_list)) copy_list = [copy_list];
+
+        return $uibModal.open({
+            templateUrl: './cat/volcopy/t_copy_alerts',
+            animation: true,
+            controller:
+                   ['$scope','$uibModalInstance',
+            function($scope , $uibModalInstance) {
+
+                itemSvc.get_copy_alert_types().then(function(ccat) {
+                    $scope.alert_types = ccat;
+                });
+
+                $scope.focusNote = true;
+                $scope.copy_alert = {
+                    create_staff : egCore.auth.user().id(),
+                    note         : '',
+                    temp         : false
+                };
+
+                egCore.hatch.getItem('cat.copy.alerts.last_type').then(function(t) {
+                    if (t) $scope.copy_alert.alert_type = t;
+                });
+
+                if (copy_list.length == 1) {
+                    $scope.copy_alert_list = copy_list[0].copy_alerts();
+                }
+
+                $scope.ok = function(copy_alert) {
+
+                    if (typeof(copy_alert.note) != 'undefined' &&
+                        copy_alert.note != '') {
+                        angular.forEach(copy_list, function (cp) {
+                            var a = new egCore.idl.aca();
+                            a.isnew(1);
+                            a.create_staff(copy_alert.create_staff);
+                            a.note(copy_alert.note);
+                            a.temp(copy_alert.temp ? 't' : 'f');
+                            a.copy(cp.id());
+                            a.ack_time(null);
+                            a.alert_type(
+                                $scope.alert_types.filter(function(at) {
+                                    return at.id() == copy_alert.alert_type;
+                                })[0]
+                            );
+                            cp.copy_alerts().push( a );
+                        });
+
+                        if (copy_alert.alert_type) {
+                            egCore.hatch.setItem(
+                                'cat.copy.alerts.last_type',
+                                copy_alert.alert_type
+                            );
+                        }
+
+                    }
+
+                    $uibModalInstance.close();
+                }
+
+                $scope.cancel = function($event) {
+                    $uibModalInstance.dismiss();
+                    $event.preventDefault();
+                }
+            }]
+        });
+    }
+
 }])
 
 .directive("egVolTemplate", function () {
@@ -1946,8 +2091,8 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
         scope: {
             editTemplates: '=',
         },
-        controller : ['$scope','$window','itemSvc','egCore','ngToast',
-            function ( $scope , $window , itemSvc , egCore , ngToast) {
+        controller : ['$scope','$window','itemSvc','egCore','ngToast','$uibModal',
+            function ( $scope , $window , itemSvc , egCore , ngToast , $uibModal) {
 
                 $scope.i18n = egCore.i18n;
 
@@ -1957,6 +2102,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                     statcats : true,
                     copy_notes : true,
                     copy_tags : true,
+                    copy_alerts : true,
                     attributes : {
                         status : true,
                         loan_duration : true,
@@ -2029,7 +2175,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                     angular.forEach($scope.templates[n], function (v,k) {
                         if (k == 'circ_lib') {
                             $scope.working[k] = egCore.org.get(v);
-                        } else if (!angular.isObject(v)) {
+                        } else if (angular.isArray(v) || !angular.isObject(v)) {
                             $scope.working[k] = angular.copy(v);
                         } else {
                             angular.forEach(v, function (sv,sk) {
@@ -2080,7 +2226,7 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                     }
                     ngToast.create(egCore.strings.VOL_COPY_TEMPLATE_SUCCESS_SAVE);
                 }
-            
+
                 $scope.templates = {};
                 $scope.imported_templates = { data : '' };
                 $scope.template_name = '';
@@ -2134,6 +2280,8 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                 }
             
                 $scope.working = {
+                    copy_notes: [],
+                    copy_alerts: [],
                     statcats: {},
                     statcat_filter: undefined
                 };
@@ -2213,7 +2361,142 @@ function($scope , $q , $window , $routeParams , $location , $timeout , egCore ,
                         createStatcatUpdateWatcher(s.id());
                     });
                 });
+
+                $scope.copy_notes_dialog = function() {
+                    var default_pub = Boolean($scope.defaults.copy_notes_pub);
+                    var working = $scope.working;
+            
+                    return $uibModal.open({
+                        templateUrl: './cat/volcopy/t_copy_notes',
+                        animation: true,
+                        controller:
+                            ['$scope','$uibModalInstance',
+                        function($scope , $uibModalInstance) {
+                            $scope.focusNote = true;
+                            $scope.note = {
+                                title   : '',
+                                value   : '',
+                                pub     : default_pub,
+                            };
+
+                            $scope.require_initials = false;
+                            egCore.org.settings([
+                                'ui.staff.require_initials.copy_notes'
+                            ]).then(function(set) {
+                                $scope.require_initials = Boolean(set['ui.staff.require_initials.copy_notes']);
+                            });
+
+                            $scope.note_list = [];
+                            angular.forEach(working.copy_notes, function(note) {
+                                var acpn = egCore.idl.fromHash('acpn', note);
+                                $scope.note_list.push(acpn);
+                            });
+
+                            $scope.ok = function(note) {
+
+                                if (!working.copy_notes) {
+                                    working.copy_notes = [];
+                                }
+
+                                // clear slate
+                                working.copy_notes.length = 0;
+                                angular.forEach($scope.note_list, function(existing_note) {
+                                    if (!existing_note.isdeleted()) {
+                                        working.copy_notes.push({
+                                            pub : existing_note.pub() ? 't' : 'f',
+                                            title : existing_note.title(),
+                                            value : existing_note.value()
+                                        });
+                                    }
+                                });
+
+                                // add new note, if any
+                                if (note.initials) note.value += ' [' + note.initials + ']';
+                                note.pub = note.pub ? 't' : 'f';
+                                if (note.title.length && note.value.length) {
+                                    working.copy_notes.push(note);
+                                }
+
+                                $uibModalInstance.close();
+                            }
+
+                            $scope.cancel = function($event) {
+                                $uibModalInstance.dismiss();
+                                $event.preventDefault();
+                            }
+                        }]
+                    });
+                }
+            
+                $scope.copy_alerts_dialog = function() {
+                    var working = $scope.working;
+
+                    return $uibModal.open({
+                        templateUrl: './cat/volcopy/t_copy_alerts',
+                        animation: true,
+                        controller:
+                            ['$scope','$uibModalInstance',
+                        function($scope , $uibModalInstance) {
+
+                            itemSvc.get_copy_alert_types().then(function(ccat) {
+                                var ccat_map = {};
+                                $scope.alert_types = ccat;
+                                angular.forEach(ccat, function(t) {
+                                    ccat_map[t.id()] = t;
+                                });
+                                $scope.copy_alert_list = [];
+                                angular.forEach(working.copy_alerts, function (alrt) {
+                                    var aca = egCore.idl.fromHash('aca', alrt);
+                                    aca.alert_type(ccat_map[alrt.alert_type]);
+                                    aca.ack_time(null);
+                                    $scope.copy_alert_list.push(aca);
+                                });
+                            });
+
+                            $scope.focusNote = true;
+                            $scope.copy_alert = {
+                                note         : '',
+                                temp         : false
+                            };
+
+                            $scope.ok = function(copy_alert) {
             
+                                if (!working.copy_alerts) {
+                                    working.copy_alerts = [];
+                                }
+                                // clear slate
+                                working.copy_alerts.length = 0;
+
+                                angular.forEach($scope.copy_alert_list, function(alrt) {
+                                    if (alrt.ack_time() == null) {
+                                        working.copy_alerts.push({
+                                            note : alrt.note(),
+                                            temp : alrt.temp(),
+                                            alert_type : alrt.alert_type().id()
+                                        });
+                                    }
+                                });
+
+                                if (typeof(copy_alert.note) != 'undefined' &&
+                                    copy_alert.note != '') {
+                                    working.copy_alerts.push({
+                                        note : copy_alert.note,
+                                        temp : copy_alert.temp ? 't' : 'f',
+                                        alert_type : copy_alert.alert_type
+                                    });
+                                }
+
+                                $uibModalInstance.close();
+                            }
+
+                            $scope.cancel = function($event) {
+                                $uibModalInstance.dismiss();
+                                $event.preventDefault();
+                            }
+                        }]
+                    });
+                }
+
                 $scope.status_list = [];
                 itemSvc.get_magic_statuses().then(function(list){
                     $scope.magic_status_list = list;
diff --git a/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js b/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
index 0a71c3f..24e4a3b 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/checkin/app.js
@@ -387,5 +387,26 @@ function($scope , $q , $window , $location , $timeout , egCore , checkinSvc , eg
         });
         itemSvc.print_spine_labels(copy_ids);
     }
+
+    $scope.addCopyAlerts = function(items) {
+        var copy_ids = [];
+        angular.forEach(items, function(item) {
+            if (item.acp) copy_ids.push(item.acp.id());
+        });
+        egCirc.add_copy_alerts(copy_ids).then(function() {
+            // update grid items?
+        });
+    }
+
+    $scope.manageCopyAlerts = function(items) {
+        var copy_ids = [];
+        angular.forEach(items, function(item) {
+            if (item.acp) copy_ids.push(item.acp.id());
+        });
+        egCirc.manage_copy_alerts(copy_ids).then(function() {
+            // update grid items?
+        });
+    }
+
 }])
 
diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js b/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js
index 10a97e4..99b001c 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/patron/checkout.js
@@ -252,9 +252,47 @@ function($scope , $q , $routeParams , egCore , egUser , patronSvc ,
             // Non-cat circs don't return the full list of circs.
             // Refresh the list of non-cat circs from the server.
             patronSvc.getUserNonCats(patronSvc.current.id());
+            row_item.copy_alert_count = 0;
+        } else {
+            row_item.copy_alert_count = 0;
+            egCore.pcrud.search(
+                'aca',
+                { copy : co_resp.data.acp.id(), ack_time : null },
+                null,
+                { atomic : true }
+            ).then(function(list) {
+                row_item.copy_alert_count = list.length;
+            });
         }
     }
 
+    $scope.addCopyAlerts = function(items) {
+        var copy_ids = [];
+        angular.forEach(items, function(item) {
+            if (item.acp) copy_ids.push(item.acp.id());
+        });
+        egCirc.add_copy_alerts(copy_ids).then(function() {
+            // update grid items?
+        });
+    }
+
+    $scope.manageCopyAlerts = function(items) {
+        var copy_ids = [];
+        angular.forEach(items, function(item) {
+            if (item.acp) copy_ids.push(item.acp.id());
+        });
+        egCirc.manage_copy_alerts(copy_ids).then(function() {
+            // update grid items?
+        });
+    }
+
+    $scope.gridCellHandlers = {};
+    $scope.gridCellHandlers.copyAlertsEdit = function(id) {
+        egCirc.manage_copy_alerts([id]).then(function() {
+            // update grid items?
+        });
+    };
+
     $scope.print_receipt = function() {
         var print_data = {circulations : []};
         var cusr = patronSvc.current;
diff --git a/Open-ILS/web/js/ui/default/staff/circ/renew/app.js b/Open-ILS/web/js/ui/default/staff/circ/renew/app.js
index 2c907bd..2c6ba63 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/renew/app.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/renew/app.js
@@ -201,6 +201,26 @@ function($scope , $window , $location , egCore , egGridDataProvider , egCirc) {
         });
     }
 
+    $scope.addCopyAlerts = function(items) {
+        var copy_ids = [];
+        angular.forEach(items, function(item) {
+            if (item.acp) copy_ids.push(item.acp.id());
+        });
+        egCirc.add_copy_alerts(copy_ids).then(function() {
+            // update grid items?
+        });
+    }
+
+    $scope.manageCopyAlerts = function(items) {
+        var copy_ids = [];
+        angular.forEach(items, function(item) {
+            if (item.acp) copy_ids.push(item.acp.id());
+        });
+        egCirc.manage_copy_alerts(copy_ids).then(function() {
+            // update grid items?
+        });
+    }
+
     $scope.print_receipt = function() {
         var print_data = {circulations : []}
 
diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
index d43ad41..91ad8fb 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/services/circ.js
@@ -5,9 +5,10 @@
 angular.module('egCoreMod')
 
 .factory('egCirc',
-       ['$uibModal','$q','egCore','egAlertDialog','egConfirmDialog',
+
+       ['$uibModal','$q','egCore','egAlertDialog','egConfirmDialog','egAddCopyAlertDialog','egCopyAlertManagerDialog','egCopyAlertEditorDialog',
         'egWorkLog',
-function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
+function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,  egAddCopyAlertDialog , egCopyAlertManagerDialog,  egCopyAlertEditorDialog ,
          egWorkLog) {
 
     var service = {
@@ -123,6 +124,8 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // options : non-parameter controls.  e.g. "override", "check_barcode"
     service.checkout = function(params, options) {
         if (!options) options = {};
+        params.new_copy_alerts = 1;
+
         console.debug('egCirc.checkout() : ' 
             + js2JSON(params) + ' : ' + js2JSON(options));
 
@@ -177,6 +180,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // Rejected if the renewal cannot be completed.
     service.renew = function(params, options) {
         if (!options) options = {};
+        params.new_copy_alerts = 1;
 
         console.debug('egCirc.renew() : ' 
             + js2JSON(params) + ' : ' + js2JSON(options));
@@ -224,6 +228,7 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // Rejected if the checkin cannot be completed.
     service.checkin = function(params, options) {
         if (!options) options = {};
+        params.new_copy_alerts = 1;
 
         console.debug('egCirc.checkin() : ' 
             + js2JSON(params) + ' : ' + js2JSON(options));
@@ -947,12 +952,24 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         if (angular.isArray(evt)) evt = evt[0];
 
         if (!evt.payload.old_circ) {
-            return egCore.pcrud.search('circ',
-                {target_copy : evt.payload.copy.id(), checkin_time : null},
-                {limit : 1} // should only ever be 1
-            ).then(function(old_circ) {
-                evt.payload.old_circ = old_circ;
-               return service.circ_exists_dialog_impl(evt, params, options);
+            return egCore.net.request(
+                'open-ils.search',
+                'open-ils.search.asset.copy.fleshed2.find_by_barcode',
+                params.copy_barcode
+            ).then(function(resp){
+                console.log(resp);
+                if (egCore.evt.parse(resp)) {
+                    console.error(egCore.evt.parse(resp));
+                } else {
+                    return egCore.net.request(
+                         'open-ils.circ',
+                         'open-ils.circ.copy_checkout_history.retrieve',
+                         egCore.auth.token(), resp.id(), 1
+                    ).then( function (circs) {
+                        evt.payload.old_circ = circs[0];
+                        return service.circ_exists_dialog_impl( evt, params, options );
+                    });
+                }
             });
         } else {
             return service.circ_exists_dialog_impl( evt, params, options );
@@ -983,14 +1000,12 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
             function(args) {
                 if (sameUser) {
                     params.void_overdues = args.forgive_fines;
-                    options.override = true;
                     return service.renew(params, options);
                 }
 
                 return service.checkin({
                     barcode : params.copy_barcode,
                     noop : true,
-                    override : true,
                     void_overdues : args.forgive_fines
                 }).then(function(checkin_resp) {
                     if (checkin_resp.evt[0].textcode == 'SUCCESS') {
@@ -1344,7 +1359,21 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
         });
     }
 
+    service.add_copy_alerts = function(item_ids) {
+        return egAddCopyAlertDialog.open({
+            copy_ids : item_ids,
+            ok : function() { },
+            cancel : function() {}
+        }).result.then(function() { });
+    }
 
+    service.manage_copy_alerts = function(item_ids) {
+        return egCopyAlertEditorDialog.open({
+            copy_id : item_ids[0],
+            ok : function() { },
+            cancel : function() {}
+        }).result.then(function() { });
+    }
 
     // alert when copy location alert_message is set.
     // This does not affect processing, it only produces a click-through
@@ -1617,17 +1646,33 @@ function($uibModal , $q , egCore , egAlertDialog , egConfirmDialog,
     // action == what action to take if the user confirms the alert
     service.copy_alert_dialog = function(evt, params, options, action) {
         if (angular.isArray(evt)) evt = evt[0];
-        return egConfirmDialog.open(
-            egCore.strings.COPY_ALERT_MSG_DIALOG_TITLE, 
-            evt.payload,  // payload == alert message text
-            {   copy_barcode : params.copy_barcode,
-                ok : function() {},
+        if (!angular.isArray(evt.payload)) {
+            return egConfirmDialog.open(
+                egCore.strings.COPY_ALERT_MSG_DIALOG_TITLE, 
+                evt.payload,  // payload == alert message text
+                {   copy_barcode : params.copy_barcode,
+                    ok : function() {},
+                    cancel : function() {}
+                }
+            ).result.then(function() {
+                options.override = true;
+                return service[action](params, options);
+            });
+        } else { // we got a list of copy alert objects ...
+            return egCopyAlertManagerDialog.open({
+                alerts : evt.payload,
+                mode : action,
+                ok : function(the_next_status) {
+                        if (the_next_status !== null) {
+                            params.next_copy_status = [ the_next_status ];
+                        }
+                     },
                 cancel : function() {}
-            }
-        ).result.then(function() {
-            options.override = true;
-            return service[action](params, options);
-        });
+            }).result.then(function() {
+                options.override = true;
+                return service[action](params, options);
+            });
+        }
     }
 
     // action == what action to take if the user confirms the alert
diff --git a/Open-ILS/web/js/ui/default/staff/circ/services/item.js b/Open-ILS/web/js/ui/default/staff/circ/services/item.js
index fa9f440..02a58d9 100644
--- a/Open-ILS/web/js/ui/default/staff/circ/services/item.js
+++ b/Open-ILS/web/js/ui/default/staff/circ/services/item.js
@@ -16,7 +16,7 @@ function(egCore , egCirc , $uibModal , $q , $timeout , $window , egConfirmDialog
         flesh : 3, 
         flesh_fields : {
             acp : ['call_number','location','status','location','floating','circ_modifier',
-                'age_protect','circ_lib'],
+                'age_protect','circ_lib','copy_alerts'],
             acn : ['record','prefix','suffix','label_class'],
             bre : ['simple_record','creator','editor']
         },
@@ -152,6 +152,10 @@ function(egCore , egCirc , $uibModal , $q , $timeout , $window , egConfirmDialog
                     flatCopy._duration = copyData.circ.duration();
                 }
                 flatCopy.index = service.index++;
+                flatCopy.copy_alert_count = copyData.copy.copy_alerts().filter(function(aca) {
+                    return !aca.ack_time();
+                }).length;
+
                 service.copies.unshift(flatCopy);
             }
 
diff --git a/Open-ILS/web/js/ui/default/staff/services/grid.js b/Open-ILS/web/js/ui/default/staff/services/grid.js
index b2d760a..ed21ae0 100644
--- a/Open-ILS/web/js/ui/default/staff/services/grid.js
+++ b/Open-ILS/web/js/ui/default/staff/services/grid.js
@@ -1263,7 +1263,12 @@ angular.module('egGridMod',
             // optional: for non-IDL columns, specifying a datatype
             // lets the caller control which display filter is used.
             // datatype should match the standard IDL datatypes.
-            datatype : '@'
+            datatype : '@',
+
+            // optional hash of functions that can be imported into
+            // the directive's scope; meant for cases where the "compiled"
+            // attribute is set
+            handlers : '='
         },
         link : function(scope, element, attrs, egGridCtrl) {
 
@@ -1271,6 +1276,7 @@ angular.module('egGridMod',
             angular.forEach(
                 [
                     'visible', 
+                    'compiled', 
                     'hidden', 
                     'sortable', 
                     'nonsortable',
@@ -1538,6 +1544,8 @@ angular.module('egGridMod',
                 linkpath : colSpec.linkpath,
                 template : colSpec.template,
                 visible  : colSpec.visible,
+                compiled : colSpec.compiled,
+                handlers : colSpec.handlers,
                 hidden   : colSpec.hidden,
                 datatype : colSpec.datatype,
                 sortable : colSpec.sortable,
@@ -2076,6 +2084,31 @@ angular.module('egGridMod',
     };
 })
 
+/* https://stackoverflow.com/questions/17343696/adding-an-ng-click-event-inside-a-filter/17344875#17344875 */
+.directive('compile', ['$compile', function ($compile) {
+    return function(scope, element, attrs) {
+      // pass through column defs from grid cell's scope
+      scope.col = scope.$parent.col;
+      scope.$watch(
+        function(scope) {
+          // watch the 'compile' expression for changes
+          return scope.$eval(attrs.compile);
+        },
+        function(value) {
+          // when the 'compile' expression changes
+          // assign it into the current DOM
+          element.html(value);
+
+          // compile the new DOM and link it to the current
+          // scope.
+          // NOTE: we only compile .childNodes so that
+          // we don't get into infinite loop compiling ourselves
+          $compile(element.contents())(scope);
+        }
+    );
+  };
+}])
+
 
 
 /**
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 47632ed..1a3f822 100644
--- a/Open-ILS/web/js/ui/default/staff/services/ui.js
+++ b/Open-ILS/web/js/ui/default/staff/services/ui.js
@@ -634,6 +634,256 @@ function($window , egStrings) {
     return service;
 }])
 
+/**
+ * egAddCopyAlertDialog - manage copy alerts
+ */
+.factory('egAddCopyAlertDialog', 
+       ['$uibModal','$interpolate','egCore',
+function($uibModal , $interpolate , egCore) {
+    var service = {};
+
+    service.open = function(args) {
+        return $uibModal.open({
+            templateUrl: './share/t_add_copy_alert_dialog',
+            controller: ['$scope','$q','$uibModalInstance',
+                function( $scope , $q , $uibModalInstance) {
+
+                    $scope.copy_ids = args.copy_ids;
+                    egCore.pcrud.search('ccat',
+                        { active : 't' },
+                        {},
+                        { atomic : true }
+                    ).then(function (ccat) {
+                        $scope.alert_types = ccat;
+                    }); 
+
+                    $scope.copy_alert = {
+                        create_staff : egCore.auth.user().id(),
+                        note         : '',
+                        temp         : false
+                    };
+
+                    $scope.ok = function(copy_alert) {
+                        if (typeof(copy_alert.note) != 'undefined' &&
+                            copy_alert.note != '') {
+                            copy_alerts = [];
+                            angular.forEach($scope.copy_ids, function (cp_id) {
+                                var a = new egCore.idl.aca();
+                                a.isnew(1);
+                                a.create_staff(copy_alert.create_staff);
+                                a.note(copy_alert.note);
+                                a.temp(copy_alert.temp ? 't' : 'f');
+                                a.copy(cp_id);
+                                a.ack_time(null);
+                                a.alert_type(
+                                    $scope.alert_types.filter(function(at) {
+                                        return at.id() == copy_alert.alert_type;
+                                    })[0]
+                                );
+                                copy_alerts.push( a );
+                            });
+                            if (copy_alerts.length > 0) {
+                                egCore.pcrud.apply(copy_alerts);
+                            }
+                        }
+                        if (args.ok) args.ok();
+                        $uibModalInstance.close()
+                    }
+                    $scope.cancel = function() {
+                        if (args.cancel) args.cancel();
+                        $uibModalInstance.dismiss();
+                    }
+                }
+            ]
+        })
+    }
+
+    return service;
+}])
+
+/**
+ * egCopyAlertManagerDialog - manage copy alerts
+ */
+.factory('egCopyAlertManagerDialog', 
+       ['$uibModal','$interpolate','egCore',
+function($uibModal , $interpolate , egCore) {
+    var service = {};
+
+    service.get_user_copy_alerts = function(copy_id) {
+        return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null },
+            { flesh : 1, flesh_fields : { aca : ['alert_type'] } },
+            { atomic : true }
+        );
+    }
+
+    service.open = function(args) {
+        return $uibModal.open({
+            templateUrl: './share/t_copy_alert_manager_dialog',
+            controller: ['$scope','$q','$uibModalInstance',
+                function( $scope , $q , $uibModalInstance) {
+
+                    function init(args) {
+                        var defer = $q.defer();
+                        if (args.copy_id) {
+                            service.get_user_copy_alerts(args.copy_id).then(function(aca) {
+                                defer.resolve(aca);
+                            });
+                        } else {
+                            defer.resolve(args.alerts);
+                        }
+                        return defer.promise;
+                    }
+
+                    // returns a promise resolved with the list of circ statuses
+                    $scope.get_copy_statuses = function() {
+                        if (egCore.env.ccs)
+                            return $q.when(egCore.env.ccs.list);
+
+                        return egCore.pcrud.retrieveAll('ccs', null, {atomic : true})
+                        .then(function(list) {
+                            egCore.env.absorbList(list, 'ccs');
+                            return list;
+                        });
+                    };
+
+                    $scope.mode = args.mode || 'checkin';
+
+                    var next_statuses = [];
+                    var seen_statuses = {};
+                    $scope.next_statuses = [];
+                    $scope.params = {
+                        'the_next_status' : null
+                    }
+                    init(args).then(function(copy_alerts) {
+                        $scope.alerts = copy_alerts;
+                        angular.forEach($scope.alerts, function(copy_alert) {
+                            var state = copy_alert.alert_type().state();
+                            copy_alert.evt = copy_alert.alert_type().event();
+
+                            copy_alert.message = copy_alert.note() ||
+                                egCore.strings.ON_DEMAND_COPY_ALERT[copy_alert.evt][state];
+
+                            if (copy_alert.temp() == 't') {
+                                angular.forEach(copy_alert.alert_type().next_status(), function (st) {
+                                    if (!seen_statuses[st]) {
+                                        seen_statuses[st] = true;
+                                        next_statuses.push(st);
+                                    }
+                                });
+                            }
+                        });
+                        if ($scope.mode == 'checkin' && next_statuses.length > 0) {
+                            $scope.get_copy_statuses().then(function() {
+                                angular.forEach(next_statuses, function(st) {
+                                    if (egCore.env.ccs.map[st])
+                                    	$scope.next_statuses.push(egCore.env.ccs.map[st]);
+                                });
+                                $scope.params.the_next_status = $scope.next_statuses[0].id();
+                            });
+                        }
+                    });
+
+                    $scope.isAcknowledged = function(copy_alert) {
+                        return (copy_alert.acked);
+                    };
+                    $scope.canBeAcknowledged = function(copy_alert) {
+                        return (!copy_alert.ack_time() && copy_alert.temp() == 't');
+                    };
+                    $scope.canBeRemoved = function(copy_alert) {
+                        return (!copy_alert.ack_time() && copy_alert.temp() == 'f');
+                    };
+
+                    $scope.ok = function() {
+                        var acks = [];
+                        angular.forEach($scope.alerts, function (copy_alert) {
+                            if (copy_alert.acked) {
+                                copy_alert.ack_time('now');
+                                copy_alert.ack_staff(egCore.auth.user().id());
+                                copy_alert.ischanged(true);
+                                acks.push(copy_alert);
+                            }
+                        });
+                        if (acks.length > 0) {
+                            egCore.pcrud.apply(acks);
+                        }
+                        if (args.ok) args.ok($scope.params.the_next_status);
+                        $uibModalInstance.close()
+                    }
+                    $scope.cancel = function() {
+                        if (args.cancel) args.cancel();
+                        $uibModalInstance.dismiss();
+                    }
+                }
+            ]
+        })
+    }
+
+    return service;
+}])
+
+/**
+ * egCopyAlertEditorDialog - manage copy alerts
+ */
+.factory('egCopyAlertEditorDialog', 
+       ['$uibModal','$interpolate','egCore',
+function($uibModal , $interpolate , egCore) {
+    var service = {};
+
+    service.get_user_copy_alerts = function(copy_id) {
+        return egCore.pcrud.search('aca', { copy : copy_id, ack_time : null },
+            { flesh : 1, flesh_fields : { aca : ['alert_type'] } },
+            { atomic : true }
+        );
+    }
+
+    service.get_copy_alert_types = function() {
+        return egCore.pcrud.search('ccat',
+            { active : 't' },
+            {},
+            { atomic : true }
+        );
+    };
+
+    service.open = function(args) {
+        return $uibModal.open({
+            templateUrl: './share/t_copy_alert_editor_dialog',
+            controller: ['$scope','$q','$uibModalInstance',
+                function( $scope , $q , $uibModalInstance) {
+
+                    function init(args) {
+                        var defer = $q.defer();
+                        if (args.copy_id) {
+                            service.get_user_copy_alerts(args.copy_id).then(function(aca) {
+                                defer.resolve(aca);
+                            });
+                        } else {
+                            defer.resolve(args.alerts);
+                        }
+                        return defer.promise;
+                    }
+
+                    init(args).then(function(copy_alerts) {
+                        $scope.copy_alert_list = copy_alerts;
+                    });
+                    service.get_copy_alert_types().then(function(ccat) {
+                        $scope.alert_types = ccat;
+                    });
+
+                    $scope.ok = function() {
+                        egCore.pcrud.apply($scope.copy_alert_list);
+                        $uibModalInstance.close()
+                    }
+                    $scope.cancel = function() {
+                        if (args.cancel) args.cancel();
+                        $uibModalInstance.dismiss();
+                    }
+                }
+            ]
+        })
+    }
+
+    return service;
+}])
 .directive('aDisabled', function() {
     return {
         restrict : 'A',
@@ -676,9 +926,9 @@ function($window , egStrings) {
         template:
             '<div class="input-group">'+
                 '<input placeholder="{{placeholder}}" type="text" ng-disabled="egDisabled" class="form-control" ng-model="selected" ng-change="makeOpen()" focus-me="focusMe">'+
-                '<div class="input-group-btn" dropdown ng-class="{open:isopen}">'+
-                    '<button type="button" ng-click="showAll()" ng-disabled="egDisabled" class="btn btn-default dropdown-toggle"><span class="caret"></span></button>'+
-                    '<ul class="dropdown-menu dropdown-menu-right">'+
+                '<div class="input-group-btn" uib-dropdown ng-class="{open:isopen}">'+
+                    '<button type="button" ng-click="showAll()" ng-disabled="egDisabled" class="btn btn-default" uib-dropdown-toggle><span class="caret"></span></button>'+
+                    '<ul dropdown-menu class="dropdown-menu-right">'+
                         '<li ng-repeat="item in list|filter:selected:compare"><a href ng-click="changeValue(item)">{{item}}</a></li>'+
                         '<li ng-if="complete_list" class="divider"><span></span></li>'+
                         '<li ng-if="complete_list" ng-repeat="item in list"><a href ng-click="changeValue(item)">{{item}}</a></li>'+

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

Summary of changes:
 Open-ILS/examples/fm_IDL.xml                       |  105 +++++++
 .../perlmods/lib/OpenILS/Application/AppUtils.pm   |    8 +
 .../lib/OpenILS/Application/Cat/AssetCommon.pm     |   42 +++-
 .../lib/OpenILS/Application/Circ/Circulate.pm      |  298 +++++++++++++++++++-
 Open-ILS/src/sql/Pg/002.schema.config.sql          |    2 +-
 Open-ILS/src/sql/Pg/040.schema.asset.sql           |   92 ++++++
 Open-ILS/src/sql/Pg/800.fkeys.sql                  |   16 +
 Open-ILS/src/sql/Pg/950.data.seed-values.sql       |   93 ++++++
 Open-ILS/src/sql/Pg/live_t/copy_state.pg           |   11 +
 .../src/sql/Pg/upgrade/1095.schema.copy_alerts.sql |  114 ++++++++
 .../upgrade/1096.data.stock_copy_alert_types.sql   |   64 +++++
 ...ta.yaous_for_open_circ_exists_fine_handling.sql |   35 +++
 .../upgrade/1098.data.move_legacy_copy_alerts.sql  |   23 ++
 .../staff/admin/local/autoGridEditor/acas.tt2      |   30 ++
 .../staff/admin/local/autoGridEditor/ccat.tt2      |   93 ++++++
 Open-ILS/src/templates/staff/admin/local/index.tt2 |    6 +
 .../templates/staff/admin/local/t_grid_editor.tt2  |   12 +
 .../src/templates/staff/admin/local/t_splash.tt2   |    2 +
 .../src/templates/staff/cat/catalog/t_holdings.tt2 |    8 +
 Open-ILS/src/templates/staff/cat/item/t_list.tt2   |   11 +-
 .../templates/staff/cat/item/t_summary_pane.tt2    |   14 +-
 .../templates/staff/cat/volcopy/t_attr_edit.tt2    |   41 +--
 .../templates/staff/cat/volcopy/t_copy_alerts.tt2  |   94 ++++++
 .../src/templates/staff/cat/volcopy/t_defaults.tt2 |    6 +-
 .../staff/circ/checkin/t_checkin_table.tt2         |   11 +-
 .../src/templates/staff/circ/patron/t_checkout.tt2 |   16 +-
 .../src/templates/staff/circ/renew/t_renew.tt2     |   12 +-
 .../templates/staff/circ/share/circ_strings.tt2    |   24 ++-
 Open-ILS/src/templates/staff/css/style.css.tt2     |    4 +
 .../staff/share/t_add_copy_alert_dialog.tt2        |   40 +++
 Open-ILS/src/templates/staff/share/t_autogrid.tt2  |    8 +-
 .../staff/share/t_copy_alert_editor_dialog.tt2     |   55 ++++
 .../staff/share/t_copy_alert_manager_dialog.tt2    |   49 ++++
 .../web/js/ui/default/staff/admin/local/app.js     |  160 +++++++++++-
 .../web/js/ui/default/staff/cat/catalog/app.js     |   18 ++
 Open-ILS/web/js/ui/default/staff/cat/item/app.js   |   54 ++++-
 .../js/ui/default/staff/cat/services/holdings.js   |    7 +-
 .../web/js/ui/default/staff/cat/volcopy/app.js     |  294 +++++++++++++++++++-
 .../web/js/ui/default/staff/circ/checkin/app.js    |   21 ++
 .../js/ui/default/staff/circ/patron/checkout.js    |   38 +++
 Open-ILS/web/js/ui/default/staff/circ/renew/app.js |   20 ++
 .../web/js/ui/default/staff/circ/services/circ.js  |  119 +++++++--
 .../web/js/ui/default/staff/circ/services/item.js  |    6 +-
 Open-ILS/web/js/ui/default/staff/services/grid.js  |   35 +++-
 Open-ILS/web/js/ui/default/staff/services/pcrud.js |    1 +
 Open-ILS/web/js/ui/default/staff/services/ui.js    |  268 +++++++++++++++++-
 .../Copy_Alerts_And_Suppresion_Matrix.adoc         |   23 ++
 47 files changed, 2403 insertions(+), 100 deletions(-)
 create mode 100644 Open-ILS/src/sql/Pg/live_t/copy_state.pg
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1095.schema.copy_alerts.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1096.data.stock_copy_alert_types.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1097.data.yaous_for_open_circ_exists_fine_handling.sql
 create mode 100644 Open-ILS/src/sql/Pg/upgrade/1098.data.move_legacy_copy_alerts.sql
 create mode 100644 Open-ILS/src/templates/staff/admin/local/autoGridEditor/acas.tt2
 create mode 100644 Open-ILS/src/templates/staff/admin/local/autoGridEditor/ccat.tt2
 create mode 100644 Open-ILS/src/templates/staff/admin/local/t_grid_editor.tt2
 create mode 100644 Open-ILS/src/templates/staff/cat/volcopy/t_copy_alerts.tt2
 create mode 100644 Open-ILS/src/templates/staff/share/t_add_copy_alert_dialog.tt2
 create mode 100644 Open-ILS/src/templates/staff/share/t_copy_alert_editor_dialog.tt2
 create mode 100644 Open-ILS/src/templates/staff/share/t_copy_alert_manager_dialog.tt2
 create mode 100644 docs/RELEASE_NOTES_NEXT/Circulation/Copy_Alerts_And_Suppresion_Matrix.adoc


hooks/post-receive
-- 
Evergreen ILS




More information about the open-ils-commits mailing list