merge seials-integration [sic] branch into trunk
authorsenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Fri, 13 Aug 2010 18:15:46 +0000 (18:15 +0000)
committersenator <senator@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Fri, 13 Aug 2010 18:15:46 +0000 (18:15 +0000)
Dan Wells has provided tremendous efforts in developing a user interface
and middle-layer logic for controlling serials.  His work builds on the
serial schema hashed out earlier in the year, also with his heavy involvement,
and it provides ways to manage subscriptions, distribution, prediction,
receiving and more.  Although this code is but a beginning, much of it
is functional today.

This commit brings Dan's work into trunk.  Dan has provided some release
notes here: http://www.open-ils.org/dokuwiki/doku.php?id=acq:serials:release_notes:initial_trunk_merge

git-svn-id: svn://svn.open-ils.org/ILS/trunk@17215 dcc99617-32d9-48b4-a31d-7c20da2025e4

38 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/examples/opensrf.xml.example
Open-ILS/examples/opensrf_core.xml.example
Open-ILS/src/extras/ils_events.xml
Open-ILS/src/perlmods/OpenILS/Application/Search/Serial.pm
Open-ILS/src/perlmods/OpenILS/Application/Serial.pm [new file with mode: 0644]
Open-ILS/src/perlmods/OpenILS/Utils/MFHD.pm
Open-ILS/src/perlmods/OpenILS/Utils/MFHD/Holding.pm
Open-ILS/src/perlmods/OpenILS/Utils/MFHD/test/testlib.pm
Open-ILS/src/perlmods/OpenILS/Utils/MFHDParser.pm
Open-ILS/src/sql/Pg/002.schema.config.sql
Open-ILS/src/sql/Pg/upgrade/0371.schema.serial_supplement-fix.sql [new file with mode: 0644]
Open-ILS/web/js/dojo/openils/opac/nls/opac.js
Open-ILS/web/opac/locale/en-US/lang.dtd
Open-ILS/web/opac/skin/default/js/rdetail.js
Open-ILS/xul/staff_client/chrome/content/cat/opac.js
Open-ILS/xul/staff_client/chrome/content/cat/opac.xul
Open-ILS/xul/staff_client/chrome/content/main/constants.js
Open-ILS/xul/staff_client/server/locale/en-US/serial.properties [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/editor_base.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/manage_items.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/manage_items.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/manage_subs.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/manage_subs.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/notes.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/scap_editor.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/scap_editor.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/sdist_editor.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/sdist_editor.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/select_aou.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/select_unit.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/serctrl_main.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/siss_editor.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/siss_editor.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/sitem_editor.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/sitem_editor.xul [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/ssub_editor.js [new file with mode: 0644]
Open-ILS/xul/staff_client/server/serial/ssub_editor.xul [new file with mode: 0644]

index 8fefa7a..180d168 100644 (file)
@@ -3088,15 +3088,15 @@ SELECT  usr,
 
        <class id="svr" controller="open-ils.cstore" oils_obj:fieldmapper="serial::virtual_record" oils_persist:virtual="true" reporter:label="Serial Virtual Record">
                <fields>
-                       <field name="id" oils_persist:virtual="true" />
+                       <field name="sre_id" oils_persist:virtual="true" />
                        <field name="location" oils_persist:virtual="true" />
                        <field name="owning_lib" oils_persist:virtual="true" />
-                       <field name="holdings" oils_persist:virtual="true" />
-                       <field name="current_holdings" oils_persist:virtual="true" />
-                       <field name="supplements" oils_persist:virtual="true" />
-                       <field name="current_supplements" oils_persist:virtual="true" />
-                       <field name="indexes" oils_persist:virtual="true" />
-                       <field name="current_indexes" oils_persist:virtual="true" />
+                       <field name="basic_holdings" oils_persist:virtual="true" />
+                       <field name="basic_holdings_add" oils_persist:virtual="true" />
+                       <field name="supplement_holdings" oils_persist:virtual="true" />
+                       <field name="supplement_holdings_add" oils_persist:virtual="true" />
+                       <field name="index_holdings" oils_persist:virtual="true" />
+                       <field name="index_holdings_add" oils_persist:virtual="true" />
                        <field name="online" oils_persist:virtual="true" />
                        <field name="missing" oils_persist:virtual="true" />
                        <field name="incomplete" oils_persist:virtual="true" />
@@ -3481,7 +3481,7 @@ SELECT  usr,
        </class>
 
        <class id="sssum" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="serial::supplement_summary" oils_persist:tablename="serial.supplement_summary" reporter:label="Supplemental Issue Summary">
-               <fields oils_persist:primary="id" oils_persist:sequence="serial.sup_summary_id_seq">
+               <fields oils_persist:primary="id" oils_persist:sequence="serial.supplement_summary_id_seq">
                        <field name="id" reporter:datatype="id" />
                        <field name="distribution" reporter:datatype="link"/>
                        <field name="generated_coverage" reporter:datatype="text"/>
@@ -4419,6 +4419,12 @@ SELECT  usr,
                        <link field="circ_lib" reltype="has_a" key="id" map="" class="aou"/>
                </links>
                <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <create permission="ADMIN_ASSET_COPY_TEMPLATE" global_required="true"/>
+                               <retrieve />
+                               <update permission="ADMIN_ASSET_COPY_TEMPLATE" global_required="true"/>
+                               <delete permission="ADMIN_ASSET_COPY_TEMPLATE" global_required="true"/>
+                       </actions>
                </permacrud>
        </class>
 
index af9c045..7ca8310 100644 (file)
@@ -1056,6 +1056,26 @@ vim:et:ts=4:sw=4:
                 </app_settings>
             </open-ils.vandelay>
 
+            <open-ils.serial>
+                <keepalive>3</keepalive>
+                <stateless>1</stateless>
+                <language>perl</language>
+                <implementation>OpenILS::Application::Serial</implementation>
+                <max_requests>17</max_requests>
+                <unix_config>
+                    <unix_sock>open-ils.serial_unix.sock</unix_sock>
+                    <unix_pid>open-ils.serial_unix.pid</unix_pid>
+                    <max_requests>1000</max_requests>
+                    <unix_log>open-ils.serial_unix.log</unix_log>
+                    <min_children>5</min_children>
+                    <max_children>15</max_children>
+                    <min_spare_children>3</min_spare_children>
+                    <max_spare_children>5</max_spare_children>
+                </unix_config>
+                <app_settings>
+                </app_settings>
+            </open-ils.serial>
+
         </apps>
     </default>
 
@@ -1096,6 +1116,7 @@ vim:et:ts=4:sw=4:
                 <appname>open-ils.trigger</appname>  
                 <appname>open-ils.fielder</appname>  
                 <appname>open-ils.vandelay</appname>  
+                <appname>open-ils.serial</appname>  
             </activeapps>
         </localhost>
     </hosts>
index 3097ce0..0d803fc 100644 (file)
@@ -33,6 +33,7 @@ Example OpenSRF bootstrap configuration file for Evergreen
           <service>open-ils.search</service>
           <service>open-ils.supercat</service>
           <service>open-ils.vandelay</service>
+          <service>open-ils.serial</service>
         </services>
       </router>
 
index 05dd59c..7f807b9 100644 (file)
 
        <!-- ================================================================ -->
 
+    <event code='11000' textcode='SERIAL_SUBSCRIPTION_NOT_EMPTY'>
+        <desc xml:lang="en-US">The subscription still has dependent objects</desc>
+    </event>
+
+    <event code='11001' textcode='SERIAL_CAPTION_AND_PATTERN_HAS_ISSUANCES'>
+        <desc xml:lang="en-US">The caption/pattern still has dependent issuances</desc>
+    </event>
+
+       <!-- ================================================================ -->
+
 </ils_events>
 
 
index ede2706..cd1d48a 100644 (file)
@@ -73,65 +73,116 @@ Given a bib record ID, returns a hash of holdings statements
 
 =cut
 
-sub bib_to_mfhd_hash {
+# DEFUNCT ?
+#sub bib_to_mfhd_hash {
+#      my ($self, $client, $bib) = @_;
+#      
+#      my $mfhd_hash;
+#
+#      # XXX perhaps this? --miker
+##     my $e = OpenILS::Utils::CStoreEditor->new();
+##     my $mfhd = $e->search_serial_record_entry({ record => $bib });
+##     return $u->generate_svr( $mfhd->[0] ) if (ref $mfhd);
+##     return undef;
+#
+#      my @mfhd = $U->cstorereq( "open-ils.cstore.json_query.atomic", {
+#              select  => { sre => 'marc' },
+#              from    => 'sre',
+#              where   => { record => $bib, deleted => 'f' },
+#              distinct => 1
+#      });
+#      
+#      if (!@mfhd or scalar(@mfhd) == 0) {
+#              return undef;
+#      }
+#
+#      my $u = OpenILS::Utils::MFHDParser->new();
+#      $mfhd_hash = $u->generate_svr( $mfhd[0][0]->{id}, $mfhd[0][0]->{marc}, $mfhd[0][0]->{owning_lib} );
+#
+#      return $mfhd_hash;
+#}
+#
+#__PACKAGE__->register_method(
+#      method  => "bib_to_mfhd_hash",
+#      api_name        => "open-ils.search.serial.record.bib_to_mfhd.retrieve",
+#      argc            => 1, 
+#      note            => "Given a bibliographic record ID, return MFHD holdings"
+#);
+
+sub bib_to_svr {
        my ($self, $client, $bib) = @_;
        
-       my $mfhd_hash;
-
-       # XXX perhaps this? --miker
-#      my $e = OpenILS::Utils::CStoreEditor->new();
-#      my $mfhd = $e->search_serial_record_entry({ record => $bib });
-#      return $u->generate_svr( $mfhd->[0] ) if (ref $mfhd);
-#      return undef;
-
-       my @mfhd = $U->cstorereq( "open-ils.cstore.json_query.atomic", {
-               select  => { sre => 'marc' },
-               from    => 'sre',
-               where   => { record => $bib, deleted => 'f' },
-               distinct => 1
-       });
-       
-       if (!@mfhd or scalar(@mfhd) == 0) {
-               return undef;
-       }
-
-       my $u = OpenILS::Utils::MFHDParser->new();
-       $mfhd_hash = $u->generate_svr( $mfhd[0][0]->{id}, $mfhd[0][0]->{marc}, $mfhd[0][0]->{owning_lib} );
-
-       return $mfhd_hash;
-}
-
-__PACKAGE__->register_method(
-       method  => "bib_to_mfhd_hash",
-       api_name        => "open-ils.search.serial.record.bib_to_mfhd.retrieve",
-       argc            => 1, 
-       note            => "Given a bibliographic record ID, return MFHD holdings"
-);
-
-sub bib_to_mfhd {
-       my ($self, $client, $bib) = @_;
-       
-       my $mfhd;
+       my $svrs;
 
        my $e = OpenILS::Utils::CStoreEditor->new();
-       my $serials = $e->search_serial_record_entry({ record => $bib, deleted => 'f' });
-       if (!ref $serials) {
+    # TODO: 'deleted' ssub support
+    my $sdists = $e->search_serial_distribution([{ "+ssub" => {"record_entry" => $bib} }, { "flesh" => 1, "flesh_fields" => {'sdist' => [ "record_entry", "holding_lib", "basic_summary", "supplement_summary", "index_summary" ]}, "join" => {"ssub" => {}} }]);
+       my $sres = $e->search_serial_record_entry([{ record => $bib, deleted => 'f', "+sdist" => {"id" => undef} }, { "join" => {"sdist" => { 'type' => 'left' }} }]);
+       if (!ref $sres and !ref $sdists) {
                return undef;
        }
 
-       my $u = OpenILS::Utils::MFHDParser->new();
-       foreach (@$serials) {
-               push(@$mfhd, $u->generate_svr($_->id, $_->marc, $_->owning_lib));
+       my $mfhd_parser = OpenILS::Utils::MFHDParser->new();
+       foreach (@$sdists) {
+        my $svr;
+        if (ref $_->record_entry) {
+            $svr = $mfhd_parser->generate_svr($_->record_entry->id, $_->record_entry->marc, $_->record_entry->owning_lib);
+        } else {
+            $svr = Fieldmapper::serial::virtual_record->new;
+            $svr->sre_id(-1);
+            $svr->location($_->holding_lib->name);
+            $svr->owning_lib($_->holding_lib);
+            $svr->basic_holdings([]);
+            $svr->supplement_holdings([]);
+            $svr->index_holdings([]);
+            $svr->basic_holdings_add([]);
+            $svr->supplement_holdings_add([]);
+            $svr->index_holdings_add([]);
+            $svr->online([]);
+            $svr->missing([]);
+            $svr->incomplete([]);
+        }
+        if (ref $_->basic_summary) { #TODO: 'show-generated' boolean on summaries
+            if ($_->basic_summary->generated_coverage) {
+                push(@{$svr->basic_holdings}, $_->basic_summary->generated_coverage);
+            }
+            if ($_->basic_summary->textual_holdings) {
+                push(@{$svr->basic_holdings_add}, $_->basic_summary->textual_holdings);
+            }
+        }
+        if (ref $_->supplement_summary) {
+            if ($_->supplement_summary->generated_coverage) {
+                push(@{$svr->supplement_holdings}, $_->supplement_summary->generated_coverage);
+            }
+            if ($_->supplement_summary->textual_holdings) {
+                push(@{$svr->supplement_holdings_add}, $_->supplement_summary->textual_holdings);
+            }
+        }
+        if (ref $_->index_summary) {
+            if ($_->index_summary->generated_coverage) {
+                push(@{$svr->index_holdings}, $_->index_summary->generated_coverage);
+            }
+            if ($_->index_summary->textual_holdings) {
+                push(@{$svr->index_holdings_add}, $_->index_summary->textual_holdings);
+            }
+        }
+        push(@$svrs, $svr);
        }
+       foreach (@$sres) {
+               push(@$svrs, $mfhd_parser->generate_svr($_->id, $_->marc, $_->owning_lib));
+       }
+
+    # do a basic location sort for simple predictability
+    @$svrs = sort { $a->location cmp $b->location } @$svrs;
 
-       return $mfhd;
+       return $svrs;
 }
 
 __PACKAGE__->register_method(
-       method  => "bib_to_mfhd",
+       method  => "bib_to_svr",
        api_name        => "open-ils.search.serial.record.bib.retrieve",
        argc            => 1, 
-       note            => "Given a bibliographic record ID, return MFHD holdings"
+       note            => "Given a bibliographic record ID, return holdings in svr form"
 );
 
 1;
diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Serial.pm b/Open-ILS/src/perlmods/OpenILS/Application/Serial.pm
new file mode 100644 (file)
index 0000000..9bb0a18
--- /dev/null
@@ -0,0 +1,1607 @@
+#!/usr/bin/perl
+
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+=head1 NAME
+
+OpenILS::Application::Serial - Performs serials-related tasks such as receiving issues and generating predictions
+
+=head1 SYNOPSIS
+
+TBD
+
+=head1 DESCRIPTION
+
+TBD
+
+=head1 AUTHOR
+
+Dan Wells, dbw2@calvin.edu
+
+=cut
+
+package OpenILS::Application::Serial;
+
+use strict;
+use warnings;
+
+use OpenILS::Application;
+use base qw/OpenILS::Application/;
+use OpenILS::Application::AppUtils;
+use OpenSRF::AppSession;
+use OpenSRF::Utils qw/:datetime/;;
+use OpenSRF::Utils::Logger qw($logger);
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+use OpenILS::Utils::MFHD;
+use MARC::File::XML (BinaryEncoding => 'utf8');
+my $U = 'OpenILS::Application::AppUtils';
+my @MFHD_NAMES = ('basic','supplement','index');
+my %MFHD_NAMES_BY_TAG = (  '853' => $MFHD_NAMES[0],
+                        '863' => $MFHD_NAMES[0],
+                        '854' => $MFHD_NAMES[1],
+                        '864' => $MFHD_NAMES[1],
+                        '855' => $MFHD_NAMES[2],
+                        '865' => $MFHD_NAMES[2] );
+my %MFHD_TAGS_BY_NAME = (  $MFHD_NAMES[0] => '853',
+                        $MFHD_NAMES[1] => '854',
+                        $MFHD_NAMES[2] => '855');
+
+
+# helper method for conforming dates to ISO8601
+sub _cleanse_dates {
+    my $item = shift;
+    my $fields = shift;
+
+    foreach my $field (@$fields) {
+        $item->$field(OpenSRF::Utils::clense_ISO8601($item->$field)) if $item->$field;
+    }
+    return 0;
+}
+
+
+##########################################################################
+# item methods
+#
+__PACKAGE__->register_method(
+    method    => 'fleshed_item_alter',
+    api_name  => 'open-ils.serial.item.fleshed.batch.update',
+    api_level => 1,
+    argc      => 2,
+    signature => {
+        desc     => 'Receives an array of one or more items and updates the database as needed',
+        'params' => [ {
+                 name => 'authtoken',
+                 desc => 'Authtoken for current user session',
+                 type => 'string'
+            },
+            {
+                 name => 'items',
+                 desc => 'Array of fleshed items',
+                 type => 'array'
+            }
+
+        ],
+        'return' => {
+            desc => 'Returns 1 if successful, event if failed',
+            type => 'mixed'
+        }
+    }
+);
+
+sub fleshed_item_alter {
+    my( $self, $conn, $auth, $items ) = @_;
+    return 1 unless ref $items;
+    my( $reqr, $evt ) = $U->checkses($auth);
+    return $evt if $evt;
+    my $editor = new_editor(requestor => $reqr, xact => 1);
+    my $override = $self->api_name =~ /override/;
+
+# TODO: permission check
+#        return $editor->event unless
+#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
+
+    for my $item (@$items) {
+
+        my $itemid = $item->id;
+        $item->editor($editor->requestor->id);
+        $item->edit_date('now');
+
+        if( $item->isdeleted ) {
+            $evt = _delete_sitem( $editor, $override, $item);
+        } elsif( $item->isnew ) {
+            # TODO: reconsider this
+            # if the item has a new issuance, create the issuance first
+            if (ref $item->issuance eq 'Fieldmapper::serial::issuance' and $item->issuance->isnew) {
+                fleshed_issuance_alter($self, $conn, $auth, [$item->issuance]);
+            }
+            _cleanse_dates($item, ['date_expected','date_received']);
+            $evt = _create_sitem( $editor, $item );
+        } else {
+            _cleanse_dates($item, ['date_expected','date_received']);
+            $evt = _update_sitem( $editor, $override, $item );
+        }
+    }
+
+    if( $evt ) {
+        $logger->info("fleshed item-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
+        $editor->rollback;
+        return $evt;
+    }
+    $logger->debug("item-alter: done updating item batch");
+    $editor->commit;
+    $logger->info("fleshed item-alter successfully updated ".scalar(@$items)." items");
+    return 1;
+}
+
+sub _delete_sitem {
+    my ($editor, $override, $item) = @_;
+    $logger->info("item-alter: delete item ".OpenSRF::Utils::JSON->perl2JSON($item));
+    return $editor->event unless $editor->delete_serial_item($item);
+    return 0;
+}
+
+sub _create_sitem {
+    my ($editor, $item) = @_;
+
+    $item->creator($editor->requestor->id);
+    $item->create_date('now');
+
+    $logger->info("item-alter: new item ".OpenSRF::Utils::JSON->perl2JSON($item));
+    return $editor->event unless $editor->create_serial_item($item);
+    return 0;
+}
+
+sub _update_sitem {
+    my ($editor, $override, $item) = @_;
+
+    $logger->info("item-alter: retrieving item ".$item->id);
+    my $orig_item = $editor->retrieve_serial_item($item->id);
+
+    $logger->info("item-alter: original item ".OpenSRF::Utils::JSON->perl2JSON($orig_item));
+    $logger->info("item-alter: updated item ".OpenSRF::Utils::JSON->perl2JSON($item));
+    return $editor->event unless $editor->update_serial_item($item);
+    return 0;
+}
+
+__PACKAGE__->register_method(
+    method  => "fleshed_serial_item_retrieve_batch",
+    authoritative => 1,
+    api_name    => "open-ils.serial.item.fleshed.batch.retrieve"
+);
+
+sub fleshed_serial_item_retrieve_batch {
+    my( $self, $client, $ids ) = @_;
+# FIXME: permissions?
+    $logger->info("Fetching fleshed serial items @$ids");
+    return $U->cstorereq(
+        "open-ils.cstore.direct.serial.item.search.atomic",
+        { id => $ids },
+        { flesh => 2,
+          flesh_fields => {sitem => [ qw/issuance creator editor stream unit notes/ ], sstr => ["distribution"], sunit => ["call_number"], siss => [qw/creator editor subscription/]}
+        });
+}
+
+
+##########################################################################
+# issuance methods
+#
+__PACKAGE__->register_method(
+    method    => 'fleshed_issuance_alter',
+    api_name  => 'open-ils.serial.issuance.fleshed.batch.update',
+    api_level => 1,
+    argc      => 2,
+    signature => {
+        desc     => 'Receives an array of one or more issuances and updates the database as needed',
+        'params' => [ {
+                 name => 'authtoken',
+                 desc => 'Authtoken for current user session',
+                 type => 'string'
+            },
+            {
+                 name => 'issuances',
+                 desc => 'Array of fleshed issuances',
+                 type => 'array'
+            }
+
+        ],
+        'return' => {
+            desc => 'Returns 1 if successful, event if failed',
+            type => 'mixed'
+        }
+    }
+);
+
+sub fleshed_issuance_alter {
+    my( $self, $conn, $auth, $issuances ) = @_;
+    return 1 unless ref $issuances;
+    my( $reqr, $evt ) = $U->checkses($auth);
+    return $evt if $evt;
+    my $editor = new_editor(requestor => $reqr, xact => 1);
+    my $override = $self->api_name =~ /override/;
+
+# TODO: permission support
+#        return $editor->event unless
+#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
+
+    for my $issuance (@$issuances) {
+        my $issuanceid = $issuance->id;
+        $issuance->editor($editor->requestor->id);
+        $issuance->edit_date('now');
+
+        if( $issuance->isdeleted ) {
+            $evt = _delete_siss( $editor, $override, $issuance);
+        } elsif( $issuance->isnew ) {
+            _cleanse_dates($issuance, ['date_published']);
+            $evt = _create_siss( $editor, $issuance );
+        } else {
+            _cleanse_dates($issuance, ['date_published']);
+            $evt = _update_siss( $editor, $override, $issuance );
+        }
+    }
+
+    if( $evt ) {
+        $logger->info("fleshed issuance-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
+        $editor->rollback;
+        return $evt;
+    }
+    $logger->debug("issuance-alter: done updating issuance batch");
+    $editor->commit;
+    $logger->info("fleshed issuance-alter successfully updated ".scalar(@$issuances)." issuances");
+    return 1;
+}
+
+sub _delete_siss {
+    my ($editor, $override, $issuance) = @_;
+    $logger->info("issuance-alter: delete issuance ".OpenSRF::Utils::JSON->perl2JSON($issuance));
+    return $editor->event unless $editor->delete_serial_issuance($issuance);
+    return 0;
+}
+
+sub _create_siss {
+    my ($editor, $issuance) = @_;
+
+    $issuance->creator($editor->requestor->id);
+    $issuance->create_date('now');
+
+    $logger->info("issuance-alter: new issuance ".OpenSRF::Utils::JSON->perl2JSON($issuance));
+    return $editor->event unless $editor->create_serial_issuance($issuance);
+    return 0;
+}
+
+sub _update_siss {
+    my ($editor, $override, $issuance) = @_;
+
+    $logger->info("issuance-alter: retrieving issuance ".$issuance->id);
+    my $orig_issuance = $editor->retrieve_serial_issuance($issuance->id);
+
+    $logger->info("issuance-alter: original issuance ".OpenSRF::Utils::JSON->perl2JSON($orig_issuance));
+    $logger->info("issuance-alter: updated issuance ".OpenSRF::Utils::JSON->perl2JSON($issuance));
+    return $editor->event unless $editor->update_serial_issuance($issuance);
+    return 0;
+}
+
+__PACKAGE__->register_method(
+    method  => "fleshed_serial_issuance_retrieve_batch",
+    authoritative => 1,
+    api_name    => "open-ils.serial.issuance.fleshed.batch.retrieve"
+);
+
+sub fleshed_serial_issuance_retrieve_batch {
+    my( $self, $client, $ids ) = @_;
+# FIXME: permissions?
+    $logger->info("Fetching fleshed serial issuances @$ids");
+    return $U->cstorereq(
+        "open-ils.cstore.direct.serial.issuance.search.atomic",
+        { id => $ids },
+        { flesh => 1,
+          flesh_fields => {siss => [ qw/creator editor subscription/ ]}
+        });
+}
+
+
+##########################################################################
+# unit methods
+#
+__PACKAGE__->register_method(
+    method    => 'fleshed_sunit_alter',
+    api_name  => 'open-ils.serial.sunit.fleshed.batch.update',
+    api_level => 1,
+    argc      => 2,
+    signature => {
+        desc     => 'Receives an array of one or more Units and updates the database as needed',
+        'params' => [ {
+                 name => 'authtoken',
+                 desc => 'Authtoken for current user session',
+                 type => 'string'
+            },
+            {
+                 name => 'sunits',
+                 desc => 'Array of fleshed Units',
+                 type => 'array'
+            }
+
+        ],
+        'return' => {
+            desc => 'Returns 1 if successful, event if failed',
+            type => 'mixed'
+        }
+    }
+);
+
+sub fleshed_sunit_alter {
+    my( $self, $conn, $auth, $sunits ) = @_;
+    return 1 unless ref $sunits;
+    my( $reqr, $evt ) = $U->checkses($auth);
+    return $evt if $evt;
+    my $editor = new_editor(requestor => $reqr, xact => 1);
+    my $override = $self->api_name =~ /override/;
+
+# TODO: permission support
+#        return $editor->event unless
+#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
+
+    for my $sunit (@$sunits) {
+        if( $sunit->isdeleted ) {
+            $evt = _delete_sunit( $editor, $override, $sunit );
+        } else {
+            $sunit->default_location( $sunit->default_location->id ) if ref $sunit->default_location;
+
+            if( $sunit->isnew ) {
+                $evt = _create_sunit( $editor, $sunit );
+            } else {
+                $evt = _update_sunit( $editor, $override, $sunit );
+            }
+        }
+    }
+
+    if( $evt ) {
+        $logger->info("fleshed sunit-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
+        $editor->rollback;
+        return $evt;
+    }
+    $logger->debug("sunit-alter: done updating sunit batch");
+    $editor->commit;
+    $logger->info("fleshed sunit-alter successfully updated ".scalar(@$sunits)." Units");
+    return 1;
+}
+
+sub _delete_sunit {
+    my ($editor, $override, $sunit) = @_;
+    $logger->info("sunit-alter: delete sunit ".OpenSRF::Utils::JSON->perl2JSON($sunit));
+    return $editor->event unless $editor->delete_serial_unit($sunit);
+    return 0;
+}
+
+sub _create_sunit {
+    my ($editor, $sunit) = @_;
+
+    $logger->info("sunit-alter: new Unit ".OpenSRF::Utils::JSON->perl2JSON($sunit));
+    return $editor->event unless $editor->create_serial_unit($sunit);
+    return 0;
+}
+
+sub _update_sunit {
+    my ($editor, $override, $sunit) = @_;
+
+    $logger->info("sunit-alter: retrieving sunit ".$sunit->id);
+    my $orig_sunit = $editor->retrieve_serial_unit($sunit->id);
+
+    $logger->info("sunit-alter: original sunit ".OpenSRF::Utils::JSON->perl2JSON($orig_sunit));
+    $logger->info("sunit-alter: updated sunit ".OpenSRF::Utils::JSON->perl2JSON($sunit));
+    return $editor->event unless $editor->update_serial_unit($sunit);
+    return 0;
+}
+
+__PACKAGE__->register_method(
+       method  => "retrieve_unit_list",
+    authoritative => 1,
+       api_name        => "open-ils.serial.unit_list.retrieve"
+);
+
+sub retrieve_unit_list {
+
+       my( $self, $client, @sdist_ids ) = @_;
+
+       if(ref($sdist_ids[0])) { @sdist_ids = @{$sdist_ids[0]}; }
+
+       my $e = new_editor();
+
+    my $query = {
+        'select' => 
+            { 'sunit' => [ 'id', 'summary_contents', 'sort_key' ],
+              'sitem' => ['stream'],
+              'sstr' => ['distribution'],
+              'sdist' => [{'column' => 'label', 'alias' => 'sdist_label'}]
+            },
+        'from' =>
+            { 'sdist' =>
+                { 'sstr' =>
+                    { 'join' =>
+                        { 'sitem' =>
+                            { 'join' => { 'sunit' => {} } }
+                        }
+                    }
+                }
+            },
+        'distinct' => 'true',
+        'where' => { '+sdist' => {'id' => \@sdist_ids} },
+        'order_by' => [{'class' => 'sunit', 'field' => 'sort_key'}]
+    };
+
+    my $unit_list_entries = $e->json_query($query);
+    
+    my @entries;
+    foreach my $entry (@$unit_list_entries) {
+        my $value = {'sunit' => $entry->{id}, 'sstr' => $entry->{stream}, 'sdist' => $entry->{distribution}};
+        my $label = $entry->{summary_contents};
+        if (length($label) > 100) {
+            $label = substr($label, 0, 100) . '...'; # limited space in dropdown / menu
+        }
+        $label = "[$entry->{sdist_label}/$entry->{stream} #$entry->{id}] " . $label;
+        push (@entries, [$label, OpenSRF::Utils::JSON->perl2JSON($value)]);
+    }
+
+    return \@entries;
+}
+
+
+
+##########################################################################
+# predict and receive methods
+#
+__PACKAGE__->register_method(
+    method    => 'make_predictions',
+    api_name  => 'open-ils.serial.make_predictions',
+    api_level => 1,
+    argc      => 1,
+    signature => {
+        desc     => 'Receives an ssub id and populates the issuance and item tables',
+        'params' => [ {
+                 name => 'ssub_id',
+                 desc => 'Serial Subscription ID',
+                 type => 'int'
+            }
+        ]
+    }
+);
+
+sub make_predictions {
+    my ($self, $conn, $authtoken, $args) = @_;
+
+    my $editor = OpenILS::Utils::CStoreEditor->new();
+    my $ssub_id = $args->{ssub_id};
+    my $mfhd = MFHD->new(MARC::Record->new());
+
+    my $ssub = $editor->retrieve_serial_subscription([$ssub_id]);
+    my $scaps = $editor->search_serial_caption_and_pattern({ subscription => $ssub_id, active => 't'});
+    my $sdists = $editor->search_serial_distribution( [{ subscription => $ssub->id }, {  flesh => 1,
+              flesh_fields => {sdist => [ qw/ streams / ]}, limit => 1 }] ); #TODO: 'deleted' support?
+
+    my @predictions;
+    my $link_id = 1;
+    foreach my $scap (@$scaps) {
+        my $caption_field = _revive_caption($scap);
+        $caption_field->update('8' => $link_id);
+        $mfhd->append_fields($caption_field);
+        my $options = {
+                'caption' => $caption_field,
+                'scap_id' => $scap->id,
+                'num_to_predict' => $args->{num_to_predict}
+                };
+        if ($args->{base_issuance}) { # predict from a given issuance
+            $options->{predict_from} = _revive_holding($args->{base_issuance}->holding_code, $caption_field, 1); # fresh MFHD Record, so we simply default to 1 for seqno
+        } else { # default to predicting from last published
+            my $last_published = $editor->search_serial_issuance([
+                    {'caption_and_pattern' => $scap->id,
+                    'subscription' => $ssub_id},
+                {limit => 1, order_by => { siss => "date_published DESC" }}]
+                );
+            if ($last_published->[0]) {
+                my $last_siss = $last_published->[0];
+                $options->{predict_from} = _revive_holding($last_siss->holding_code, $caption_field, 1);
+            } else {
+                #TODO: throw event (can't predict from nothing!)
+            }
+        }
+        push( @predictions, _generate_issuance_values($mfhd, $options) );
+        $link_id++;
+    }
+
+    my @issuances;
+    foreach my $prediction (@predictions) {
+        my $issuance = new Fieldmapper::serial::issuance;
+        $issuance->isnew(1);
+        $issuance->label($prediction->{label});
+        $issuance->date_published($prediction->{date_published}->strftime('%F'));
+        $issuance->holding_code(OpenSRF::Utils::JSON->perl2JSON($prediction->{holding_code}));
+        $issuance->holding_type($prediction->{holding_type});
+        $issuance->caption_and_pattern($prediction->{caption_and_pattern});
+        $issuance->subscription($ssub->id);
+        push (@issuances, $issuance);
+    }
+
+    fleshed_issuance_alter($self, $conn, $authtoken, \@issuances); # FIXME: catch events
+
+    my @items;
+    for (my $i = 0; $i < @issuances; $i++) {
+        my $date_expected = $predictions[$i]->{date_published}->add(seconds => interval_to_seconds($ssub->expected_date_offset))->strftime('%F');
+        my $issuance = $issuances[$i];
+        #$issuance->label(interval_to_seconds($ssub->expected_date_offset));
+        foreach my $sdist (@$sdists) {
+            my $streams = $sdist->streams;
+            foreach my $stream (@$streams) {
+                my $item = new Fieldmapper::serial::item;
+                $item->isnew(1);
+                $item->stream($stream->id);
+                $item->date_expected($date_expected);
+                $item->issuance($issuance->id);
+                push (@items, $item);
+            }
+        }
+    }
+    fleshed_item_alter($self, $conn, $authtoken, \@items); # FIXME: catch events
+    return \@items;
+}
+
+#
+# _generate_issuance_values() is an initial attempt at a function which can be used
+# to populate an issuance table with a list of predicted issues.  It accepts
+# a hash ref of options initially defined as:
+# caption : the caption field to predict on
+# num_to_predict : the number of issues you wish to predict
+# last_rec_date : the date of the last received issue, to be used as an offset
+#                 for predicting future issues
+#
+# The basic method is to first convert to a single holding if compressed, then
+# increment the holding and save the resulting values to @issuances.
+# 
+# returns @issuance_values, an array of hashrefs containing (formatted
+# label, formatted chronology date, formatted estimated arrival date, and an
+# array ref of holding subfields as (key, value, key, value ...)) (not a hash
+# to protect order and possible duplicate keys), and a holding type.
+#
+sub _generate_issuance_values {
+    my ($mfhd, $options) = @_;
+    my $caption = $options->{caption};
+    my $scap_id = $options->{scap_id};
+    my $num_to_predict = $options->{num_to_predict};
+    my $predict_from = $options->{predict_from};   # issuance to predict from
+    #my $last_rec_date = $options->{last_rec_date};   # expected or actual
+
+    # TODO: add support for predicting serials with no chronology by passing in
+    # a last_pub_date option?
+
+
+# Only needed for 'real' MFHD records, not our temp records
+#    my $link_id = $caption->link_id;
+#    if(!$predict_from) {
+#        my $htag = $caption->tag;
+#        $htag =~ s/^85/86/;
+#        my @holdings = $mfhd->holdings($htag, $link_id);
+#        my $last_holding = $holdings[-1];
+#
+#        #if ($last_holding->is_compressed) {
+#        #    $last_holding->compressed_to_last; # convert to last in range
+#        #}
+#        $predict_from = $last_holding;
+#    }
+#
+
+    $predict_from->notes('public',  []);
+# add a note marker for system use (?)
+    $predict_from->notes('private', ['AUTOGEN']);
+
+    my $strp = new DateTime::Format::Strptime(pattern => '%F');
+    my $pub_date;
+    my @issuance_values;
+    my @predictions = $mfhd->generate_predictions({'base_holding' => $predict_from, 'num_to_predict' => $num_to_predict});
+    foreach my $prediction (@predictions) {
+        $pub_date = $strp->parse_datetime($prediction->chron_to_date);
+        push(
+                @issuance_values,
+                {
+                    #$link_id,
+                    label => $prediction->format,
+                    date_published => $pub_date,
+                    #date_expected => $date_expected->strftime('%F'),
+                    holding_code => [$prediction->indicator(1),$prediction->indicator(2),$prediction->subfields_list],
+                    holding_type => $MFHD_NAMES_BY_TAG{$caption->tag},
+                    caption_and_pattern => $scap_id
+                }
+            );
+    }
+
+    return @issuance_values;
+}
+
+sub _revive_caption {
+    my $scap = shift;
+
+    my $pattern_code = $scap->pattern_code;
+
+    # build MARC::Field
+    my $pattern_parts = OpenSRF::Utils::JSON->JSON2perl($pattern_code);
+    unshift(@$pattern_parts, $MFHD_TAGS_BY_NAME{$scap->type});
+    my $pattern_field = new MARC::Field(@$pattern_parts);
+
+    # build MFHD::Caption
+    return new MFHD::Caption($pattern_field);
+}
+
+sub _revive_holding {
+    my $holding_code = shift;
+    my $caption_field = shift;
+    my $seqno = shift;
+
+    # build MARC::Field
+    my $holding_parts = OpenSRF::Utils::JSON->JSON2perl($holding_code);
+    my $captag = $caption_field->tag;
+    $captag =~ s/^85/86/;
+    unshift(@$holding_parts, $captag);
+    my $holding_field = new MARC::Field(@$holding_parts);
+
+    # build MFHD::Holding
+    return new MFHD::Holding($seqno, $holding_field, $caption_field);
+}
+
+__PACKAGE__->register_method(
+    method    => 'unitize_items',
+    api_name  => 'open-ils.serial.receive_items',
+    api_level => 1,
+    argc      => 1,
+    signature => {
+        desc     => 'Marks an item as received, updates the shelving unit (creating a new shelving unit if needed), and updates the summaries',
+        'params' => [ {
+                 name => 'items',
+                 desc => 'array of serial items',
+                 type => 'array'
+            }
+        ],
+        'return' => {
+            desc => 'Returns number of received items',
+            type => 'int'
+        }
+    }
+);
+
+sub unitize_items {
+    my ($self, $conn, $auth, $items) = @_;
+
+    my( $reqr, $evt ) = $U->checkses($auth);
+    return $evt if $evt;
+    my $editor = new_editor(requestor => $reqr, xact => 1);
+    $self->api_name =~ /serial\.(\w*)_items/;
+    my $mode = $1;
+    
+    my %found_unit_ids;
+    my %found_stream_ids;
+    my %found_types;
+
+    my %stream_ids_by_unit_id;
+
+    my %unit_map;
+    my %sdist_by_unit_id;
+    my %sdist_by_stream_id;
+
+    my $new_unit_id; # id for '-2' units to share
+    foreach my $item (@$items) {
+        # for debugging only, TODO: delete
+        if (!ref $item) { # hopefully we got an id instead
+            $item = $editor->retrieve_serial_item($item);
+        }
+        # get ids
+        my $unit_id = ref($item->unit) ? $item->unit->id : $item->unit;
+        my $stream_id = ref($item->stream) ? $item->stream->id : $item->stream;
+        my $issuance_id = ref($item->issuance) ? $item->issuance->id : $item->issuance;
+        #TODO: evt on any missing ids
+
+        if ($mode eq 'receive') {
+            $item->date_received('now');
+            $item->status('Received');
+        } else {
+            $item->status('Bindery');
+        }
+
+        # check for types to trigger summary updates
+        my $scap;
+        if (!ref $item->issuance) {
+            my $scaps = $editor->search_serial_caption_and_pattern([{"+siss" => {"id" => $issuance_id}}, { "join" => {"siss" => {}} }]);
+            $scap = $scaps->[0];
+        } elsif (!ref $item->issuance->caption_and_pattern) {
+            $scap = $editor->retrieve_serial_caption_and_pattern($item->issuance->caption_and_pattern);
+        } else {
+            $scap = $editor->issuance->caption_and_pattern;
+        }
+        if (!exists($found_types{$stream_id})) {
+            $found_types{$stream_id} = {};
+        }
+        $found_types{$stream_id}->{$scap->type} = 1;
+
+        # create unit if needed
+        if ($unit_id == -1 or (!$new_unit_id and $unit_id == -2)) { # create unit per item
+            my $unit;
+            my $sdists = $editor->search_serial_distribution([{"+sstr" => {"id" => $stream_id}}, { "join" => {"sstr" => {}} }]);
+            $unit = _build_unit($editor, $sdists->[0], $mode);
+            my $evt =  _create_sunit($editor, $unit);
+            return $evt if $evt;
+            if ($unit_id == -2) {
+                $new_unit_id = $unit->id;
+                $unit_id = $new_unit_id;
+            } else {
+                $unit_id = $unit->id;
+            }
+            $item->unit($unit_id);
+            
+            # get unit with 'DEFAULT's and save unit and sdist for later use
+            $unit = $editor->retrieve_serial_unit($unit->id);
+            $unit_map{$unit_id} = $unit;
+            $sdist_by_unit_id{$unit_id} = $sdists->[0];
+            $sdist_by_stream_id{$stream_id} = $sdists->[0];
+        } elsif ($unit_id == -2) { # create one unit for all '-2' items
+            $unit_id = $new_unit_id;
+            $item->unit($unit_id);
+        }
+
+        $found_unit_ids{$unit_id} = 1;
+        $found_stream_ids{$stream_id} = 1;
+
+        # save the stream_id for this unit_id
+        # TODO: prevent items from different streams in same unit? (perhaps in interface)
+        $stream_ids_by_unit_id{$unit_id} = $stream_id;
+
+        my $evt = _update_sitem($editor, undef, $item);
+        return $evt if $evt;
+    }
+
+    # deal with unit level labels
+    foreach my $unit_id (keys %found_unit_ids) {
+
+        # get all the needed issuances for unit
+        my $issuances = $editor->search_serial_issuance([ {"+sitem" => {"unit" => $unit_id, "status" => "Received"}}, {"join" => {"sitem" => {}}, "order_by" => {"siss" => "date_published"}} ]);
+        #TODO: evt on search failure
+
+        my ($mfhd, $formatted_parts) = _summarize_contents($editor, $issuances);
+
+        # special case for single formatted_part (may have summarized version)
+        if (@$formatted_parts == 1) {
+            #TODO: MFHD.pm should have a 'format_summary' method for this
+        }
+
+        # retrieve and update unit contents
+        my $sunit;
+        my $sdist;
+
+        # if we just created the unit, we will already have it and the distribution stored
+        if (exists $unit_map{$unit_id}) {
+            $sunit = $unit_map{$unit_id};
+            $sdist = $sdist_by_unit_id{$unit_id};
+        } else {
+            $sunit = $editor->retrieve_serial_unit($unit_id);
+            $sdist = $editor->search_serial_distribution([{"+sstr" => {"id" => $stream_ids_by_unit_id{$unit_id}}}, { "join" => {"sstr" => {}} }]);
+            $sdist = $sdist->[0];
+        }
+
+        $sunit->detailed_contents($sdist->unit_label_prefix . ' '
+                    . join(', ', @$formatted_parts) . ' '
+                    . $sdist->unit_label_suffix);
+
+        $sunit->summary_contents($sunit->detailed_contents); #TODO: change this when real summary contents are available
+
+        # create sort_key by left padding numbers to 6 digits
+        my $sort_key = $sunit->detailed_contents;
+        $sort_key =~ s/(\d+)/sprintf '%06d', $1/eg; # this may need improvement
+        $sunit->sort_key($sort_key);
+        
+        if ($mode eq 'bind') {
+            $sunit->status(2); # set to 'Bindery' status
+        }
+
+        my $evt = _update_sunit($editor, undef, $sunit);
+        return $evt if $evt;
+    }
+
+    # TODO: cleanup 'dead' units (units which are now emptied of their items)
+
+    if ($mode eq 'receive') { # the summary holdings do not change when binding
+        # deal with stream level summaries
+        # summaries will be built from the "primary" stream only, that is, the stream with the lowest ID per distribution
+        # (TODO: consider direct designation)
+        my %primary_streams_by_sdist;
+        my %streams_by_sdist;
+
+        # see if we have primary streams, and if so, associate them with their distributions
+        foreach my $stream_id (keys %found_stream_ids) {
+            my $sdist;
+            if (exists $sdist_by_stream_id{$stream_id}) {
+                $sdist = $sdist_by_stream_id{$stream_id};
+            } else {
+                $sdist = $editor->search_serial_distribution([{"+sstr" => {"id" => $stream_id}}, { "join" => {"sstr" => {}} }]);
+                $sdist = $sdist->[0];
+            }
+            my $streams;
+            if (!exists($streams_by_sdist{$sdist->id})) {
+                $streams = $editor->search_serial_stream([{"distribution" => $sdist->id}, {"order_by" => {"sstr" => "id"}}]);
+                $streams_by_sdist{$sdist->id} = $streams;
+            } else {
+                $streams = $streams_by_sdist{$sdist->id};
+            }
+            $primary_streams_by_sdist{$sdist->id} = $streams->[0] if ($stream_id == $streams->[0]->id);
+        }
+
+        # retrieve and update summaries for each affected primary stream's distribution
+        foreach my $sdist_id (keys %primary_streams_by_sdist) {
+            my $stream = $primary_streams_by_sdist{$sdist_id};
+            my $stream_id = $stream->id;
+            # get all the needed issuances for stream
+            # FIXME: search in Bindery/Bound/Not Published? as well as Received
+            foreach my $type (keys %{$found_types{$stream_id}}) {
+                my $issuances = $editor->search_serial_issuance([ {"+sitem" => {"stream" => $stream_id, "status" => "Received"}, "+scap" => {"type" => $type}}, {"join" => {"sitem" => {}, "scap" => {}}, "order_by" => {"siss" => "date_published"}} ]);
+                #TODO: evt on search failure
+
+                my ($mfhd, $formatted_parts) = _summarize_contents($editor, $issuances);
+
+                # retrieve and update the generated_coverage of the summary
+                my $search_method = "search_serial_${type}_summary";
+                my $summary = $editor->$search_method([{"distribution" => $sdist_id}]);
+                $summary = $summary->[0];
+                $summary->generated_coverage(join(', ', @$formatted_parts));
+                my $update_method = "update_serial_${type}_summary";
+                return $editor->event unless $editor->$update_method($summary);
+            }
+        }
+    }
+
+    $editor->commit;
+    return {'num_items_received' => scalar @$items, 'new_unit_id' => $new_unit_id};
+}
+
+sub _build_unit {
+    my $editor = shift;
+    my $sdist = shift;
+    my $mode = shift;
+
+    my $attr = $mode . '_unit_template';
+    my $template = $editor->retrieve_asset_copy_template($sdist->$attr);
+
+    my @parts = qw( status location loan_duration fine_level age_protect circulate deposit ref holdable deposit_amount price circ_modifier circ_as_type alert_message opac_visible floating mint_condition );
+
+    my $unit = new Fieldmapper::serial::unit;
+    foreach my $part (@parts) {
+        my $value = $template->$part;
+        next if !defined($value);
+        $unit->$part($value);
+    }
+
+    # ignore circ_lib in template, set to distribution holding_lib
+    $unit->circ_lib($sdist->holding_lib);
+    $unit->creator($editor->requestor->id);
+    $unit->editor($editor->requestor->id);
+    $attr = $mode . '_call_number';
+    $unit->call_number($sdist->$attr);
+    $unit->barcode('AUTO');
+    $unit->sort_key('');
+    $unit->summary_contents('');
+    $unit->detailed_contents('');
+
+    return $unit;
+}
+
+
+sub _summarize_contents {
+    my $editor = shift;
+    my $issuances = shift;
+
+    # create MFHD record
+    my $mfhd = MFHD->new(MARC::Record->new());
+    my %scaps;
+    my %scap_fields;
+    my @scap_fields_ordered;
+    my $seqno = 1;
+    my $link_id = 1;
+    foreach my $issuance (@$issuances) {
+        my $scap_id = $issuance->caption_and_pattern;
+        next if (!$scap_id); # skip issuances with no caption/pattern
+
+        my $scap;
+        my $scap_field;
+        # if this is the first appearance of this scap, retrieve it and add it to the temporary record
+        if (!exists $scaps{$issuance->caption_and_pattern}) {
+            $scaps{$scap_id} = $editor->retrieve_serial_caption_and_pattern($scap_id);
+            $scap = $scaps{$scap_id};
+            $scap_field = _revive_caption($scap);
+            $scap_fields{$scap_id} = $scap_field;
+            push(@scap_fields_ordered, $scap_field);
+            $scap_field->update('8' => $link_id);
+            $mfhd->append_fields($scap_field);
+            $link_id++;
+        } else {
+            $scap = $scaps{$scap_id};
+            $scap_field = $scap_fields{$scap_id};
+        }
+
+        $mfhd->append_fields(_revive_holding($issuance->holding_code, $scap_field, $seqno));
+        $seqno++;
+    }
+
+    my @formatted_parts;
+    foreach my $scap_field (@scap_fields_ordered) { #TODO: use generic MFHD "summarize" method, once available
+       my @updated_holdings = $mfhd->get_compressed_holdings($scap_field);
+       foreach my $holding (@updated_holdings) {
+           push(@formatted_parts, $holding->format);
+       }
+    }
+
+    return ($mfhd, \@formatted_parts);
+}
+
+##########################################################################
+# note methods
+#
+__PACKAGE__->register_method(
+    method      => 'fetch_notes',
+    api_name        => 'open-ils.serial.item_note.retrieve.all',
+    signature   => q/
+        Returns an array of copy note objects.  
+        @param args A named hash of parameters including:
+            authtoken   : Required if viewing non-public notes
+            item_id      : The id of the item whose notes we want to retrieve
+            pub         : True if all the caller wants are public notes
+        @return An array of note objects
+    /
+);
+
+__PACKAGE__->register_method(
+    method      => 'fetch_notes',
+    api_name        => 'open-ils.serial.subscription_note.retrieve.all',
+    signature   => q/
+        Returns an array of copy note objects.  
+        @param args A named hash of parameters including:
+            authtoken       : Required if viewing non-public notes
+            subscription_id : The id of the item whose notes we want to retrieve
+            pub             : True if all the caller wants are public notes
+        @return An array of note objects
+    /
+);
+
+__PACKAGE__->register_method(
+    method      => 'fetch_notes',
+    api_name        => 'open-ils.serial.distribution_note.retrieve.all',
+    signature   => q/
+        Returns an array of copy note objects.  
+        @param args A named hash of parameters including:
+            authtoken       : Required if viewing non-public notes
+            distribution_id : The id of the item whose notes we want to retrieve
+            pub             : True if all the caller wants are public notes
+        @return An array of note objects
+    /
+);
+
+# TODO: revisit this method to consider replacing cstore direct calls
+sub fetch_notes {
+    my( $self, $connection, $args ) = @_;
+    
+    $self->api_name =~ /serial\.(\w*)_note/;
+    my $type = $1;
+
+    my $id = $$args{object_id};
+    my $authtoken = $$args{authtoken};
+    my( $r, $evt);
+
+    if( $$args{pub} ) {
+        return $U->cstorereq(
+            'open-ils.cstore.direct.serial.'.$type.'_note.search.atomic',
+            { $type => $id, pub => 't' } );
+    } else {
+        # FIXME: restore perm check
+        # ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
+        # return $evt if $evt;
+        return $U->cstorereq(
+            'open-ils.cstore.direct.serial.'.$type.'_note.search.atomic', {$type => $id} );
+    }
+
+    return undef;
+}
+
+__PACKAGE__->register_method(
+    method      => 'create_note',
+    api_name        => 'open-ils.serial.item_note.create',
+    signature   => q/
+        Creates a new item note
+        @param authtoken The login session key
+        @param note The note object to create
+        @return The id of the new note object
+    /
+);
+
+__PACKAGE__->register_method(
+    method      => 'create_note',
+    api_name        => 'open-ils.serial.subscription_note.create',
+    signature   => q/
+        Creates a new subscription note
+        @param authtoken The login session key
+        @param note The note object to create
+        @return The id of the new note object
+    /
+);
+
+__PACKAGE__->register_method(
+    method      => 'create_note',
+    api_name        => 'open-ils.serial.distribution_note.create',
+    signature   => q/
+        Creates a new distribution note
+        @param authtoken The login session key
+        @param note The note object to create
+        @return The id of the new note object
+    /
+);
+
+sub create_note {
+    my( $self, $connection, $authtoken, $note ) = @_;
+
+    $self->api_name =~ /serial\.(\w*)_note/;
+    my $type = $1;
+
+    my $e = new_editor(xact=>1, authtoken=>$authtoken);
+    return $e->event unless $e->checkauth;
+
+    # FIXME: restore permission support
+#    my $item = $e->retrieve_serial_item(
+#        [
+#            $note->item
+#        ]
+#    );
+#
+#    return $e->event unless
+#        $e->allowed('CREATE_COPY_NOTE', $item->call_number->owning_lib);
+
+    $note->create_date('now');
+    $note->creator($e->requestor->id);
+    $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
+    $note->clear_id;
+
+    my $method = "create_serial_${type}_note";
+    $e->$method($note) or return $e->event;
+    $e->commit;
+    return $note->id;
+}
+
+__PACKAGE__->register_method(
+    method      => 'delete_note',
+    api_name        =>  'open-ils.serial.item_note.delete',
+    signature   => q/
+        Deletes an existing item note
+        @param authtoken The login session key
+        @param noteid The id of the note to delete
+        @return 1 on success - Event otherwise.
+        /
+);
+
+__PACKAGE__->register_method(
+    method      => 'delete_note',
+    api_name        =>  'open-ils.serial.subscription_note.delete',
+    signature   => q/
+        Deletes an existing subscription note
+        @param authtoken The login session key
+        @param noteid The id of the note to delete
+        @return 1 on success - Event otherwise.
+        /
+);
+
+__PACKAGE__->register_method(
+    method      => 'delete_note',
+    api_name        =>  'open-ils.serial.distribution_note.delete',
+    signature   => q/
+        Deletes an existing distribution note
+        @param authtoken The login session key
+        @param noteid The id of the note to delete
+        @return 1 on success - Event otherwise.
+        /
+);
+
+sub delete_note {
+    my( $self, $conn, $authtoken, $noteid ) = @_;
+
+    $self->api_name =~ /serial\.(\w*)_note/;
+    my $type = $1;
+
+    my $e = new_editor(xact=>1, authtoken=>$authtoken);
+    return $e->die_event unless $e->checkauth;
+
+    my $method = "retrieve_serial_${type}_note";
+    my $note = $e->$method([
+        $noteid,
+    ]) or return $e->die_event;
+
+# FIXME: restore permissions check
+#    if( $note->creator ne $e->requestor->id ) {
+#        return $e->die_event unless
+#            $e->allowed('DELETE_COPY_NOTE', $note->item->call_number->owning_lib);
+#    }
+
+    $method = "delete_serial_${type}_note";
+    $e->$method($note) or return $e->die_event;
+    $e->commit;
+    return 1;
+}
+
+
+##########################################################################
+# subscription methods
+#
+__PACKAGE__->register_method(
+    method    => 'fleshed_ssub_alter',
+    api_name  => 'open-ils.serial.subscription.fleshed.batch.update',
+    api_level => 1,
+    argc      => 2,
+    signature => {
+        desc     => 'Receives an array of one or more subscriptions and updates the database as needed',
+        'params' => [ {
+                 name => 'authtoken',
+                 desc => 'Authtoken for current user session',
+                 type => 'string'
+            },
+            {
+                 name => 'subscriptions',
+                 desc => 'Array of fleshed subscriptions',
+                 type => 'array'
+            }
+
+        ],
+        'return' => {
+            desc => 'Returns 1 if successful, event if failed',
+            type => 'mixed'
+        }
+    }
+);
+
+sub fleshed_ssub_alter {
+    my( $self, $conn, $auth, $ssubs ) = @_;
+    return 1 unless ref $ssubs;
+    my( $reqr, $evt ) = $U->checkses($auth);
+    return $evt if $evt;
+    my $editor = new_editor(requestor => $reqr, xact => 1);
+    my $override = $self->api_name =~ /override/;
+
+# TODO: permission check
+#        return $editor->event unless
+#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
+
+    for my $ssub (@$ssubs) {
+
+        my $ssubid = $ssub->id;
+
+        if( $ssub->isdeleted ) {
+            $evt = _delete_ssub( $editor, $override, $ssub);
+        } elsif( $ssub->isnew ) {
+            _cleanse_dates($ssub, ['start_date','end_date']);
+            $evt = _create_ssub( $editor, $ssub );
+        } else {
+            _cleanse_dates($ssub, ['start_date','end_date']);
+            $evt = _update_ssub( $editor, $override, $ssub );
+        }
+    }
+
+    if( $evt ) {
+        $logger->info("fleshed subscription-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
+        $editor->rollback;
+        return $evt;
+    }
+    $logger->debug("subscription-alter: done updating subscription batch");
+    $editor->commit;
+    $logger->info("fleshed subscription-alter successfully updated ".scalar(@$ssubs)." subscriptions");
+    return 1;
+}
+
+sub _delete_ssub {
+    my ($editor, $override, $ssub) = @_;
+    $logger->info("subscription-alter: delete subscription ".OpenSRF::Utils::JSON->perl2JSON($ssub));
+    my $sdists = $editor->search_serial_distribution(
+            { subscription => $ssub->id }, { limit => 1 } ); #TODO: 'deleted' support?
+    my $cps = $editor->search_serial_caption_and_pattern(
+            { subscription => $ssub->id }, { limit => 1 } ); #TODO: 'deleted' support?
+    my $sisses = $editor->search_serial_issuance(
+            { subscription => $ssub->id }, { limit => 1 } ); #TODO: 'deleted' support?
+    return OpenILS::Event->new(
+            'SERIAL_SUBSCRIPTION_NOT_EMPTY', payload => $ssub->id ) if (@$sdists or @$cps or @$sisses);
+
+    return $editor->event unless $editor->delete_serial_subscription($ssub);
+    return 0;
+}
+
+sub _create_ssub {
+    my ($editor, $ssub) = @_;
+
+    $logger->info("subscription-alter: new subscription ".OpenSRF::Utils::JSON->perl2JSON($ssub));
+    return $editor->event unless $editor->create_serial_subscription($ssub);
+    return 0;
+}
+
+sub _update_ssub {
+    my ($editor, $override, $ssub) = @_;
+
+    $logger->info("subscription-alter: retrieving subscription ".$ssub->id);
+    my $orig_ssub = $editor->retrieve_serial_subscription($ssub->id);
+
+    $logger->info("subscription-alter: original subscription ".OpenSRF::Utils::JSON->perl2JSON($orig_ssub));
+    $logger->info("subscription-alter: updated subscription ".OpenSRF::Utils::JSON->perl2JSON($ssub));
+    return $editor->event unless $editor->update_serial_subscription($ssub);
+    return 0;
+}
+
+__PACKAGE__->register_method(
+    method  => "fleshed_serial_subscription_retrieve_batch",
+    authoritative => 1,
+    api_name    => "open-ils.serial.subscription.fleshed.batch.retrieve"
+);
+
+sub fleshed_serial_subscription_retrieve_batch {
+    my( $self, $client, $ids ) = @_;
+# FIXME: permissions?
+    $logger->info("Fetching fleshed subscriptions @$ids");
+    return $U->cstorereq(
+        "open-ils.cstore.direct.serial.subscription.search.atomic",
+        { id => $ids },
+        { flesh => 1,
+          flesh_fields => {ssub => [ qw/owning_lib notes/ ]}
+        });
+}
+
+__PACKAGE__->register_method(
+       method  => "retrieve_sub_tree",
+    authoritative => 1,
+       api_name        => "open-ils.serial.subscription_tree.retrieve"
+);
+
+__PACKAGE__->register_method(
+       method  => "retrieve_sub_tree",
+       api_name        => "open-ils.serial.subscription_tree.global.retrieve"
+);
+
+sub retrieve_sub_tree {
+
+       my( $self, $client, $user_session, $docid, @org_ids ) = @_;
+
+       if(ref($org_ids[0])) { @org_ids = @{$org_ids[0]}; }
+
+       $docid = "$docid";
+
+       # TODO: permission support
+       if(!@org_ids and $user_session) {
+               my $user_obj = 
+                       OpenILS::Application::AppUtils->check_user_session( $user_session ); #throws EX on error
+                       @org_ids = ($user_obj->home_ou);
+       }
+
+       if( $self->api_name =~ /global/ ) {
+               return _build_subs_list( { record_entry => $docid } ); # TODO: filter for !deleted, or active?
+
+       } else {
+
+               my @all_subs;
+               for my $orgid (@org_ids) {
+                       my $subs = _build_subs_list( 
+                                       { record_entry => $docid, owning_lib => $orgid } );# TODO: filter for !deleted, or active?
+                       push( @all_subs, @$subs );
+               }
+               
+               return \@all_subs;
+       }
+
+       return undef;
+}
+
+sub _build_subs_list {
+       my $search_hash = shift;
+
+       #$search_hash->{deleted} = 'f';
+       my $e = new_editor();
+
+       my $subs = $e->search_serial_subscription([$search_hash, { 'order_by' => {'ssub' => 'id'} }]);
+
+       my @built_subs;
+
+       for my $sub (@$subs) {
+
+        # TODO: filter on !deleted?
+               my $dists = $e->search_serial_distribution(
+            [{ subscription => $sub->id }, { 'order_by' => {'sdist' => 'label'} }]
+            );
+
+               #$dists = [ sort { $a->label cmp $b->label } @$dists  ];
+
+               $sub->distributions($dists);
+        
+        # TODO: filter on !deleted?
+               my $issuances = $e->search_serial_issuance(
+                       [{ subscription => $sub->id }, { 'order_by' => {'siss' => 'label'} }]
+            );
+
+               #$issuances = [ sort { $a->label cmp $b->label } @$issuances  ];
+               $sub->issuances($issuances);
+
+        # TODO: filter on !deleted?
+               my $scaps = $e->search_serial_caption_and_pattern(
+                       [{ subscription => $sub->id }, { 'order_by' => {'scap' => 'id'} }]
+            );
+
+               #$scaps = [ sort { $a->id cmp $b->id } @$scaps  ];
+               $sub->scaps($scaps);
+               push( @built_subs, $sub );
+       }
+
+       return \@built_subs;
+
+}
+
+__PACKAGE__->register_method(
+    method  => "subscription_orgs_for_title",
+    authoritative => 1,
+    api_name    => "open-ils.serial.subscription.retrieve_orgs_by_title"
+);
+
+sub subscription_orgs_for_title {
+    my( $self, $client, $record_id ) = @_;
+
+    my $subs = $U->simple_scalar_request(
+        "open-ils.cstore",
+        "open-ils.cstore.direct.serial.subscription.search.atomic",
+        { record_entry => $record_id }); # TODO: filter on !deleted?
+
+    my $orgs = { map {$_->owning_lib => 1 } @$subs };
+    return [ keys %$orgs ];
+}
+
+
+##########################################################################
+# distribution methods
+#
+__PACKAGE__->register_method(
+    method    => 'fleshed_sdist_alter',
+    api_name  => 'open-ils.serial.distribution.fleshed.batch.update',
+    api_level => 1,
+    argc      => 2,
+    signature => {
+        desc     => 'Receives an array of one or more distributions and updates the database as needed',
+        'params' => [ {
+                 name => 'authtoken',
+                 desc => 'Authtoken for current user session',
+                 type => 'string'
+            },
+            {
+                 name => 'distributions',
+                 desc => 'Array of fleshed distributions',
+                 type => 'array'
+            }
+
+        ],
+        'return' => {
+            desc => 'Returns 1 if successful, event if failed',
+            type => 'mixed'
+        }
+    }
+);
+
+sub fleshed_sdist_alter {
+    my( $self, $conn, $auth, $sdists ) = @_;
+    return 1 unless ref $sdists;
+    my( $reqr, $evt ) = $U->checkses($auth);
+    return $evt if $evt;
+    my $editor = new_editor(requestor => $reqr, xact => 1);
+    my $override = $self->api_name =~ /override/;
+
+# TODO: permission check
+#        return $editor->event unless
+#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
+
+    for my $sdist (@$sdists) {
+        my $sdistid = $sdist->id;
+
+        if( $sdist->isdeleted ) {
+            $evt = _delete_sdist( $editor, $override, $sdist);
+        } elsif( $sdist->isnew ) {
+            $evt = _create_sdist( $editor, $sdist );
+        } else {
+            $evt = _update_sdist( $editor, $override, $sdist );
+        }
+    }
+
+    if( $evt ) {
+        $logger->info("fleshed distribution-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
+        $editor->rollback;
+        return $evt;
+    }
+    $logger->debug("distribution-alter: done updating distribution batch");
+    $editor->commit;
+    $logger->info("fleshed distribution-alter successfully updated ".scalar(@$sdists)." distributions");
+    return 1;
+}
+
+sub _delete_sdist {
+    my ($editor, $override, $sdist) = @_;
+    $logger->info("distribution-alter: delete distribution ".OpenSRF::Utils::JSON->perl2JSON($sdist));
+    return $editor->event unless $editor->delete_serial_distribution($sdist);
+    return 0;
+}
+
+sub _create_sdist {
+    my ($editor, $sdist) = @_;
+
+    $logger->info("distribution-alter: new distribution ".OpenSRF::Utils::JSON->perl2JSON($sdist));
+    return $editor->event unless $editor->create_serial_distribution($sdist);
+
+    # create summaries too
+    my $summary = new Fieldmapper::serial::basic_summary;
+    $summary->distribution($sdist->id);
+    $summary->generated_coverage('');
+    return $editor->event unless $editor->create_serial_basic_summary($summary);
+    $summary = new Fieldmapper::serial::supplement_summary;
+    $summary->distribution($sdist->id);
+    $summary->generated_coverage('');
+    return $editor->event unless $editor->create_serial_supplement_summary($summary);
+    $summary = new Fieldmapper::serial::index_summary;
+    $summary->distribution($sdist->id);
+    $summary->generated_coverage('');
+    return $editor->event unless $editor->create_serial_index_summary($summary);
+
+    # create a starter stream (TODO: reconsider this)
+    my $stream = new Fieldmapper::serial::stream;
+    $stream->distribution($sdist->id);
+    return $editor->event unless $editor->create_serial_stream($stream);
+
+    return 0;
+}
+
+sub _update_sdist {
+    my ($editor, $override, $sdist) = @_;
+
+    $logger->info("distribution-alter: retrieving distribution ".$sdist->id);
+    my $orig_sdist = $editor->retrieve_serial_distribution($sdist->id);
+
+    $logger->info("distribution-alter: original distribution ".OpenSRF::Utils::JSON->perl2JSON($orig_sdist));
+    $logger->info("distribution-alter: updated distribution ".OpenSRF::Utils::JSON->perl2JSON($sdist));
+    return $editor->event unless $editor->update_serial_distribution($sdist);
+    return 0;
+}
+
+__PACKAGE__->register_method(
+    method  => "fleshed_serial_distribution_retrieve_batch",
+    authoritative => 1,
+    api_name    => "open-ils.serial.distribution.fleshed.batch.retrieve"
+);
+
+sub fleshed_serial_distribution_retrieve_batch {
+    my( $self, $client, $ids ) = @_;
+# FIXME: permissions?
+    $logger->info("Fetching fleshed distributions @$ids");
+    return $U->cstorereq(
+        "open-ils.cstore.direct.serial.distribution.search.atomic",
+        { id => $ids },
+        { flesh => 1,
+          flesh_fields => {sdist => [ qw/ holding_lib receive_call_number receive_unit_template bind_call_number bind_unit_template streams / ]}
+        });
+}
+
+##########################################################################
+# caption and pattern methods
+#
+__PACKAGE__->register_method(
+    method    => 'scap_alter',
+    api_name  => 'open-ils.serial.caption_and_pattern.batch.update',
+    api_level => 1,
+    argc      => 2,
+    signature => {
+        desc     => 'Receives an array of one or more caption and patterns and updates the database as needed',
+        'params' => [ {
+                 name => 'authtoken',
+                 desc => 'Authtoken for current user session',
+                 type => 'string'
+            },
+            {
+                 name => 'scaps',
+                 desc => 'Array of caption and patterns',
+                 type => 'array'
+            }
+
+        ],
+        'return' => {
+            desc => 'Returns 1 if successful, event if failed',
+            type => 'mixed'
+        }
+    }
+);
+
+sub scap_alter {
+    my( $self, $conn, $auth, $scaps ) = @_;
+    return 1 unless ref $scaps;
+    my( $reqr, $evt ) = $U->checkses($auth);
+    return $evt if $evt;
+    my $editor = new_editor(requestor => $reqr, xact => 1);
+    my $override = $self->api_name =~ /override/;
+
+# TODO: permission check
+#        return $editor->event unless
+#            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
+
+    for my $scap (@$scaps) {
+        my $scapid = $scap->id;
+
+        if( $scap->isdeleted ) {
+            $evt = _delete_scap( $editor, $override, $scap);
+        } elsif( $scap->isnew ) {
+            $evt = _create_scap( $editor, $scap );
+        } else {
+            $evt = _update_scap( $editor, $override, $scap );
+        }
+    }
+
+    if( $evt ) {
+        $logger->info("caption_and_pattern-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
+        $editor->rollback;
+        return $evt;
+    }
+    $logger->debug("caption_and_pattern-alter: done updating caption_and_pattern batch");
+    $editor->commit;
+    $logger->info("caption_and_pattern-alter successfully updated ".scalar(@$scaps)." caption_and_patterns");
+    return 1;
+}
+
+sub _delete_scap {
+    my ($editor, $override, $scap) = @_;
+    $logger->info("caption_and_pattern-alter: delete caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($scap));
+    my $sisses = $editor->search_serial_issuance(
+            { caption_and_pattern => $scap->id }, { limit => 1 } ); #TODO: 'deleted' support?
+    return OpenILS::Event->new(
+            'SERIAL_CAPTION_AND_PATTERN_HAS_ISSUANCES', payload => $scap->id ) if (@$sisses);
+
+    return $editor->event unless $editor->delete_serial_caption_and_pattern($scap);
+    return 0;
+}
+
+sub _create_scap {
+    my ($editor, $scap) = @_;
+
+    $logger->info("caption_and_pattern-alter: new caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($scap));
+    return $editor->event unless $editor->create_serial_caption_and_pattern($scap);
+    return 0;
+}
+
+sub _update_scap {
+    my ($editor, $override, $scap) = @_;
+
+    $logger->info("caption_and_pattern-alter: retrieving caption_and_pattern ".$scap->id);
+    my $orig_scap = $editor->retrieve_serial_caption_and_pattern($scap->id);
+
+    $logger->info("caption_and_pattern-alter: original caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($orig_scap));
+    $logger->info("caption_and_pattern-alter: updated caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($scap));
+    return $editor->event unless $editor->update_serial_caption_and_pattern($scap);
+    return 0;
+}
+
+__PACKAGE__->register_method(
+    method  => "serial_caption_and_pattern_retrieve_batch",
+    authoritative => 1,
+    api_name    => "open-ils.serial.caption_and_pattern.batch.retrieve"
+);
+
+sub serial_caption_and_pattern_retrieve_batch {
+    my( $self, $client, $ids ) = @_;
+    $logger->info("Fetching caption_and_patterns @$ids");
+    return $U->cstorereq(
+        "open-ils.cstore.direct.serial.caption_and_pattern.search.atomic",
+        { id => $ids }
+    );
+}
+
+1;
index 682d2eb..7579255 100644 (file)
@@ -6,6 +6,9 @@ use Carp;
 use DateTime::Format::Strptime;
 use Data::Dumper;
 
+# for inherited methods to work properly, we need to force a
+# MARC::Record version greater than 2.0.0
+use MARC::Record 2.0.1;
 use base 'MARC::Record';
 
 use OpenILS::Utils::MFHD::Caption;
@@ -84,30 +87,148 @@ sub caption_link_ids {
     return sort keys %{$self->{_mfhd_CAPTIONS}->{$field}};
 }
 
+# optional argument to get back a 'hashref' or an 'array' (default)
 sub captions {
     my $self  = shift;
-    my $field = shift;
+    my $tag = shift;
+    my $return_type = shift;
 
     # TODO: add support for caption types as argument? (base, index, supplement)
-    my @captions;
-    my @sorted_ids = $self->caption_link_ids($field);
+    my @sorted_ids = $self->caption_link_ids($tag);
+
+    if (defined($return_type) and $return_type eq 'hashref') {
+        my %captions;
+        foreach my $link_id (@sorted_ids) {
+            $captions{$link_id} = $self->{_mfhd_CAPTIONS}{$tag}{$link_id};
+        }
+        return \%captions;
+    } else {
+        my @captions;
+        foreach my $link_id (@sorted_ids) {
+            push(@captions, $self->{_mfhd_CAPTIONS}{$tag}{$link_id});
+        }
+        return @captions;
+    }
+}
+
+sub append_fields {
+    my $self = shift;
+
+    my $field_count = $self->SUPER::append_fields(@_);
+    if ($field_count) {
+        foreach my $field (@_) {
+            $self->_avoid_link_collision($field);
+            my $field_type = ref $field;
+            if ($field_type eq 'MFHD::Holding') {
+                $self->{_mfhd_HOLDINGS}{$field->tag}{$field->caption->link_id}{$field->seqno} = $field;
+            } elsif ($field_type eq 'MFHD::Caption') {
+                $self->{_mfhd_CAPTIONS}{$field->tag}{$field->link_id} = $field;
+            }
+        }
+        return $field_count;
+    } else {
+        return;
+    }   
+}
+
+sub delete_field {
+    my $self = shift;
+    my $field = shift;
+
+    my $field_count = $self->SUPER::delete_field($field);
+    if ($field_count) {
+        my $field_type = ref($field);
+        if ($field_type eq 'MFHD::Holding') {
+            delete($self->{_mfhd_HOLDINGS}{$field->tag}{$field->caption->link_id}{$field->seqno});
+        } elsif ($field_type eq 'MFHD::Caption') {
+            delete($self->{_mfhd_CAPTIONS}{$field->tag}{$field->link_id});
+        }
+        return $field_count;
+    } else {
+        return;
+    }
+}
+
+sub insert_fields_before {
+    my $self = shift;
+    my $before = shift;
+
+    my $field_count = $self->SUPER::insert_fields_before($before, @_);
+    if ($field_count) {
+        foreach my $field (@_) {
+            $self->_avoid_link_collision($field);
+            my $field_type = ref $field;
+            if ($field_type eq 'MFHD::Holding') {
+                $self->{_mfhd_HOLDINGS}{$field->tag}{$field->caption->link_id}{$field->seqno} = $field;
+            } elsif ($field_type eq 'MFHD::Caption') {
+                $self->{_mfhd_CAPTIONS}{$field->tag}{$field->link_id} = $field;
+            }
+        }
+        return $field_count;
+    } else {
+        return;
+    }
+}
 
-    foreach my $link_id (@sorted_ids) {
-        push(@captions, $self->{_mfhd_CAPTIONS}{$field}{$link_id});
+sub insert_fields_after {
+    my $self = shift;
+    my $after = shift;
+
+    my $field_count = $self->SUPER::insert_fields_after($after, @_);
+    if ($field_count) {
+        foreach my $field (@_) {
+            $self->_avoid_link_collision($field);
+            my $field_type = ref $field;
+            if ($field_type eq 'MFHD::Holding') {
+                $self->{_mfhd_HOLDINGS}{$field->tag}{$field->caption->link_id}{$field->seqno} = $field;
+            } elsif ($field_type eq 'MFHD::Caption') {
+                $self->{_mfhd_CAPTIONS}{$field->tag}{$field->link_id} = $field;
+            }
+        }
+        return $field_count;
+    } else {
+        return;
     }
+}
 
-    return @captions;
+sub _avoid_link_collision {
+    my $self = shift;
+    my $field = shift;
+
+    my $fieldref = ref($field);
+    if ($fieldref eq 'MFHD::Holding') {
+        my $seqno = $field->seqno;
+        my $changed_seqno = 0;
+        if (exists($self->{_mfhd_HOLDINGS}{$field->tag}{$field->caption->link_id}{$seqno})) {
+            $changed_seqno = 1;
+            do {
+                $seqno++;
+            } while (exists($self->{_mfhd_HOLDINGS}{$field->tag}{$field->caption->link_id}{$seqno}));
+        }
+        $field->seqno($seqno) if $changed_seqno;
+    } elsif ($fieldref eq 'MFHD::Caption') {
+        my $link_id = $field->link_id;
+        my $changed_link_id = 0;
+        if (exists($self->{_mfhd_CAPTIONS}{$field->tag}{$link_id})) {
+            $link_id++;
+            $changed_link_id = 1;
+            do {
+                $link_id++;
+            } while (exists($self->{_mfhd_CAPTIONS}{$field->tag}{$link_id}));
+        }
+        $field->link_id($link_id) if $changed_link_id;
+    }
 }
 
 sub active_captions {
     my $self  = shift;
-    my $field = shift;
+    my $tag = shift;
 
-    # TODO: add support for caption types as argument? (base, index, supplement)
+    # TODO: add support for caption types as argument? (basic, index, supplement)
     my @captions;
     my @active_captions;
 
-    @captions = $self->captions($field);
+    @captions = $self->captions($tag);
 
     # TODO: for now, we will assume the last 85X field is active
     # and the rest are historical.  The standard is hazy about
@@ -130,74 +251,178 @@ sub holdings {
 }
 
 #
-# generate_predictions() is an initial attempt at a function which can be used
-# to populate an issuance table with a list of predicted issues.  It accepts
-# a hash ref of options initially defined as:
-# field : the caption field to predict on (853, 854, or 855)
+# generate_predictions()
+# Accepts a hash ref of options initially defined as:
+# base_holding : reference to the holding field to predict from
 # num_to_predict : the number of issues you wish to predict
-# last_rec_date : the date of the last received issue, to be used as an offset
-#                 for predicting future issues
+# OR
+# end_holding : holding field ref, keep predicting until you meet or exceed it
 #
 # The basic method is to first convert to a single holding if compressed, then
 # increment the holding and save the resulting values to @predictions.
 # 
-# returns @preditions, an array of array refs containing (link id, formatted
-# label, formatted chronology date, formatted estimated arrival date, and an
-# array ref of holding subfields as (key, value, key, value ...)) (not a hash
-# to protect order and possible duplicate keys).
-#
+# returns @predictions, an array of holding field refs (including end_holding
+# if applicable but NOT base_holding)
+# 
 sub generate_predictions {
     my ($self, $options) = @_;
-    my $field          = $options->{field};
-    my $num_to_predict = $options->{num_to_predict};
-    my $last_rec_date =
-      $options->{last_rec_date};   # expected or actual, according to preference
 
-    # TODO: add support for predicting serials with no chronology by passing in
-    # a last_pub_date option?
+    my $base_holding   = $options->{base_holding};
+    my $num_to_predict = $options->{num_to_predict};
+    my $end_holding    = $options->{end_holding};
+    my $max_to_predict = $options->{max_to_predict} || 10000; # fail-safe
 
-    my $strp = new DateTime::Format::Strptime(pattern => '%F');
+    if (!defined($base_holding)) {
+        carp("Base holding not defined in generate_predictions, returning empty set");
+        return ();
+    }
+    if ($base_holding->is_compressed) {
+        carp("Ambiguous compressed base holding in generate_predictions, returning empty set");
+        return ();
+    }
+    my $curr_holding = $base_holding->clone; # prevent side-effects
+    
+    my @predictions;
+        
+    if ($num_to_predict) {
+        for (my $i = 0; $i < $num_to_predict; $i++) {
+            push(@predictions, $curr_holding->increment->clone);
+        }
+    } elsif (defined($end_holding)) {
+        $end_holding = $end_holding->clone; # prevent side-effects
+        my $next_holding = $curr_holding->increment->clone;
+        my $num_predicted = 0;
+        while ($next_holding le $end_holding) {
+            push(@predictions, $next_holding);
+            $num_predicted++;
+            if ($num_predicted >= $max_to_predict) {
+                carp("Maximum prediction count exceeded");
+                last;
+            }
+            $next_holding = $curr_holding->increment->clone;
+        }
+    }
 
-    my $receival_date = $strp->parse_datetime($last_rec_date);
+    return @predictions;
+}
 
-    my @active_captions = $self->active_captions($field);
+#
+# create an array of compressed holdings from all holdings for a given caption,
+# compressing as needed
+#
+# Optionally you can skip sorting, but the resulting compression will be compromised
+# if the current holdings are out of order
+#
+# TODO: gap marking, gap preservation
+#
+# TODO: some of this could be moved to the Caption object to allow for 
+# decompression in the absense of an overarching MFHD object
+#
+sub get_compressed_holdings {
+    my $self = shift;
+    my $caption = shift;
+    my $opts = shift;
+    my $skip_sort = $opts->{'skip_sort'};
+
+    # make sure none are compressed
+    my @decomp_holdings;
+    if ($skip_sort) {
+        @decomp_holdings = $self->get_decompressed_holdings($caption, {'skip_sort' => 1});
+    } else {
+        # sort for best algorithm
+        @decomp_holdings = $self->get_decompressed_holdings($caption, {'dedupe' => 1});
+    }
 
-    my @predictions;
-    foreach my $caption (@active_captions) {
-        my $htag    = $caption->tag;
-        my $link_id = $caption->link_id;
-        $htag =~ s/^85/86/;
-        my @holdings = $self->holdings($htag, $link_id);
-        my $last_holding = $holdings[-1];
-
-        if ($last_holding->is_compressed) {
-            $last_holding->compressed_to_last; # convert to last in range
+    my $runner = $decomp_holdings[0]->clone->increment;   
+    my $curr_holding = shift(@decomp_holdings);
+    $curr_holding = $curr_holding->clone;
+    my $seqno = 1;
+    $curr_holding->seqno($seqno);
+    my @comp_holdings;
+#    my $last_holding;
+    foreach my $holding (@decomp_holdings) {
+        if ($runner eq $holding) {
+            $curr_holding->extend;
+            $runner->increment;
+#        } elsif ($holding eq $last_holding) {
+#            carp("Found duplicate holding in compression set, skipping");
+        } elsif ($runner gt $holding) { # should not happen unless holding is not in series
+            carp("Found unexpected holding, skipping");
+        } else {
+            push(@comp_holdings, $curr_holding);
+            while ($runner le $holding) {
+                $runner->increment;
+            }
+            $curr_holding = $holding->clone;
+            $seqno++;
+            $curr_holding->seqno($seqno);
         }
+#        $last_holding = $holding;
+    }
+    push(@comp_holdings, $curr_holding);
 
-        my $pub_date  = $strp->parse_datetime($last_holding->chron_to_date);
-        my $date_diff = $receival_date - $pub_date;
+    return @comp_holdings;
+}
+
+#
+# create an array of single holdings from all holdings for a given caption,
+# decompressing as needed
+#
+# resulting array is returned as they come in the record, unsorted
+#
+# optional argument will reorder and renumber the holdings before returning
+# 
+# TODO: some of this could be moved to the Caption (and/or Holding) object to
+# allow for decompression in the absense of an overarching MFHD object
+#
+sub get_decompressed_holdings {
+    my $self = shift;
+    my $caption = shift;
+    my $opts = shift;
+    my $skip_sort = $opts->{'skip_sort'};
+    my $dedupe = $opts->{'dedupe'};
 
-        $last_holding->notes('public',  []);
-        # add a note marker for system use
-        $last_holding->notes('private', ['AUTOGEN']);
+    if ($dedupe and $skip_sort) {
+        carp("Attempted deduplication without sorting, failure likely");
+    }
 
-        for (my $i = 0; $i < $num_to_predict; $i++) {
-            $last_holding->increment;
-            $pub_date = $strp->parse_datetime($last_holding->chron_to_date);
-            my $arrival_date = $pub_date + $date_diff;
-            push(
-                @predictions,
-                [
-                    $link_id,
-                    $last_holding->format,
-                    $pub_date->strftime('%F'),
-                    $arrival_date->strftime('%F'),
-                    [$last_holding->subfields_list]
-                ]
-            );
+    my $htag    = $caption->tag;
+    my $link_id = $caption->link_id;
+    $htag =~ s/^85/86/;
+    my @holdings = $self->holdings($htag, $link_id);
+    my @decomp_holdings;
+
+    foreach my $holding (@holdings) {
+        if (!$holding->is_compressed) {
+            push(@decomp_holdings, $holding->clone);
+        } else {
+            my $base_holding = $holding->clone->compressed_to_first;
+            my @new_holdings = $self->generate_predictions(
+                {'base_holding' => $base_holding,
+                 'end_holding' => $holding->clone->compressed_to_last});
+            push(@decomp_holdings, $base_holding, @new_holdings);
         }
     }
-    return @predictions;
+
+    unless ($skip_sort) {
+        my @temp_holdings = sort {$a cmp $b} @decomp_holdings;
+        @decomp_holdings = @temp_holdings;
+    }
+
+    my @return_holdings = (shift(@decomp_holdings));
+    $return_holdings[0]->seqno(1);
+    my $seqno = 2;
+    foreach my $holding (@decomp_holdings) { # renumber sequence
+        if ($holding eq $return_holdings[-1] and $dedupe) {
+            carp("Found duplicate holding in decompression set, discarding");
+            next;
+        }
+        $holding->seqno($seqno);
+        $seqno++;
+        push(@return_holdings, $holding);
+    }
+
+    return @return_holdings;
 }
 
 #
index 4ce15f2..efd6027 100644 (file)
@@ -39,6 +39,10 @@ sub new {
         my ($key, $val) = @$subfield;
 
         if ($key =~ /[a-m]/) {
+            if (exists($self->{_mfhdh_FIELDS}->{$key})) {
+                carp("Duplicate, non-repeatable subfield '$key' found, ignoring");
+                next;
+            }
             if ($self->{_mfhdh_COMPRESSED}) {
                 $self->{_mfhdh_FIELDS}->{$key}{HOLDINGS} = [split(/\-/, $val)];
             } else {
@@ -466,29 +470,21 @@ sub validate {
 # Replace a single holding with it's next prediction
 # and return itself
 #
-# If the holding is compressed, the range is expanded
-#
 sub increment {
     my $self = shift;
 
     if ($self->is_open_ended) {
         carp "Holding is open-ended, cannot increment";
         return $self;
+    } elsif ($self->is_compressed) {
+        carp "Incrementing a compressed holding is deprecated, use extend instead";
+        return $self->extend;
     }
 
     my $next = $self->next();
 
-    if ($self->is_compressed) {    # expand range
-        foreach my $key (keys %{$next}) {
-            my @values = @{$self->field_values($key)};
-            $values[1] = $next->{$key};
-            $self->fields->{$key}{HOLDINGS} = \@values;
-            $next->{$key} = join('-', @values);
-        }
-    } else {
-        foreach my $key (keys %{$next}) {
-            $self->fields->{$key}{HOLDINGS}[0] = $next->{$key};
-        }
+    foreach my $key (keys %{$next}) {
+        $self->fields->{$key}{HOLDINGS}[0] = $next->{$key};
     }
 
     $self->seqno($self->seqno + 1);
@@ -497,6 +493,60 @@ sub increment {
 }
 
 #
+# Extends a holding (compressing if needed) to include the next
+# prediction and returns itself
+#
+sub extend {
+    my $self = shift;
+
+    if ($self->is_open_ended) {
+        carp "Holding is open-ended, cannot extend";
+        return $self;
+    }
+
+    my $next = $self->next();
+
+    if (!$self->is_compressed) {
+        $self->is_compressed(1);  # add compressed state
+    }
+
+    foreach my $key (keys %{$next}) {
+        my @values = @{$self->field_values($key)};
+        $values[1] = $next->{$key};
+        $self->fields->{$key}{HOLDINGS} = \@values;
+        $next->{$key} = join('-', @values);
+    }
+
+    $self->update(%{$next});    # update underlying subfields
+    return $self;
+}
+
+#
+# Turns a compressed holding into the singular form of the first member
+# in the range
+#
+sub compressed_to_first {
+    my $self = shift;
+
+    if (!$self->is_compressed) {
+        carp "Holding not compressed, cannot convert to first member";
+        return $self;
+    }
+
+    my %changes;
+    foreach my $key (keys %{$self->fields}) {
+        my @values = @{$self->field_values($key)};
+        $self->fields->{$key}{HOLDINGS} = [$values[0]];
+        $changes{$key} = $values[0];
+    }
+
+    $self->update(%changes);    # update underlying subfields
+    $self->is_compressed(0);    # remove compressed state
+
+    return $self;
+}
+
+#
 # Turns a compressed holding into the singular form of the last member
 # in the range
 #
@@ -627,4 +677,117 @@ sub _uncombine {
     my @parts = split('/', $combo);
     return $parts[$pos];
 }
+
+#
+# Overload string comparison operators
+#
+# We are not overloading '<=>' because '==' is used liberally in MARC::Record
+# to compare field identity (i.e. is this the same exact Field object?), not value
+#
+# Other string operators are auto-generated from 'cmp'
+#
+# Please note that this comparison is based on what the holding represents,
+# not whether it is strictly identical (e.g. the seqno and link may vary)
+#
+use overload ('cmp' => \&_compare,
+              'fallback' => 1);
+sub _compare {
+    my ($holding_1, $holding_2) = @_;
+
+    # TODO: this needs some more consideration
+    # fall back to 'built-in' comparison
+    if (!UNIVERSAL::isa($holding_2, ref $holding_1)) {
+        if (defined $holding_2) {
+            carp("Use of non-holding in holding comparison operation");
+            return ( "$holding_1" cmp "$holding_2" );
+        } else {
+            carp("Use of undefined value in holding comparison operation");
+            return 1; # similar to built-in, something is "greater than" nothing
+        }
+    }
+
+    # special cases for compressed holdings
+    my ($holding_1_first, $holding_1_last, $holding_2_first, $holding_2_last, $found_compressed);
+    # 0 for no compressed, 1 for first compressed, 2 for second compressed, 3 for both compressed
+    $found_compressed = 0; 
+    if ($holding_1->is_compressed) {
+        $holding_1_last = $holding_1->clone->compressed_to_last;
+        $found_compressed += 1;
+    } else {
+        $holding_1_first = $holding_1;
+        $holding_1_last = $holding_1;
+    }
+    if ($holding_2->is_compressed) {
+        $holding_2_first = $holding_2->clone->compressed_to_first;
+        $found_compressed += 2;
+    } else {
+        $holding_2_first = $holding_2;
+        $holding_2_last = $holding_2;
+    }
+
+    if ($found_compressed) {
+        my $cmp = ($holding_1_last cmp $holding_2_first); # 1 ends before 2 starts
+        if ($cmp == -1) {
+            return -1; # 1 is fully lt
+        } elsif ($cmp == 0) {
+            carp("Overlapping holdings in comparison, lt and gt based on start value only");
+            return -1;
+        } else { # check the opposite, 2 ends before 1 starts
+            # clone is expensive, wait until we need it (here)
+            if (!defined($holding_2_last)) {
+                $holding_2_last = $holding_2->clone->compressed_to_last;
+            }
+            if (!defined($holding_1_first)) {
+                $holding_1_first = $holding_1->clone->compressed_to_first;
+            }
+            $cmp = ($holding_2_last cmp $holding_1_first);
+            if ($cmp == -1) {
+                return 1; # 1 is fully gt
+            } elsif ($cmp == 0) {
+                carp("Overlapping holdings in comparison, lt and gt based on start value only");
+                return 1;
+            } else {
+                $cmp = ($holding_1_first cmp $holding_2_first);
+                if (!$cmp) { # they are not equal
+                    carp("Overlapping holdings in comparison, lt and gt based on start value only");
+                    return $cmp;
+                } elsif ($found_compressed == 1) {
+                    carp("Compressed holding found with start equal to non-compressed holding");
+                    return 1; # compressed (first holding) is 'greater than' non-compressed
+                } elsif ($found_compressed == 2) {
+                    carp("Compressed holding found with start equal to non-compressed holding");
+                    return -1; # compressed (second holding) is 'greater than' non-compressed
+                } else { # both holdings compressed, check for full equality
+                    $cmp = ($holding_1_last cmp $holding_2_last);
+                    if (!$cmp) { # they are not equal
+                        carp("Compressed holdings in comparison have equal starts, lt and gt based on end value only");
+                        return $cmp;
+                    } else {
+                        return 0; # both are compressed, both ends are equal
+                    }
+                }
+            }
+        }
+    }
+
+    # start doing the actual comparison
+    my $result;
+    foreach my $key ('a'..'f') {
+        if (defined($holding_1->field_values($key))) {
+            if (!defined($holding_2->field_values($key))) {
+                return 1; # more details equals 'greater' (?)
+            } else {
+                $result = $holding_1->field_values($key)->[0] <=> $holding_2->field_values($key)->[0];
+            }
+        } elsif (defined($holding_2->field_values($key))) {
+            return -1; # more details equals 'greater' (?)
+        }
+
+        return $result if $result;
+    }
+
+    # got through, return 0 for equal
+    return 0;
+}
+
 1;
index a000231..9953a56 100644 (file)
@@ -55,7 +55,6 @@ sub load_MARC_rec {
 
         $field = MARC::Field->new(
             $fieldno, $inds[0], $inds[1],
-            a => 'scratch',
             @subfields
         );
 
index 2e49cc0..7a191dd 100644 (file)
@@ -59,16 +59,16 @@ sub mfhd_to_hash {
     my $marc;
     my $mfhd;
 
-    my $location            = '';
-    my $holdings            = [];
-    my $supplements         = [];
-    my $indexes             = [];
-    my $current_holdings    = [];
-    my $current_supplements = [];
-    my $current_indexes     = [];
-    my $online              = [];    # Laurentian extension to MFHD standard
-    my $missing             = [];    # Laurentian extension to MFHD standard
-    my $incomplete          = [];    # Laurentian extension to MFHD standard
+    my $location                = '';
+    my $basic_holdings          = [];
+    my $supplement_holdings     = [];
+    my $index_holdings          = [];
+    my $basic_holdings_add      = [];
+    my $supplement_holdings_add = [];
+    my $index_holdings_add      = [];
+    my $online                  = [];    # Laurentian extension to MFHD standard
+    my $missing                 = [];    # Laurentian extension to MFHD standard
+    my $incomplete              = [];    # Laurentian extension to MFHD standard
 
     try {
         $marc = MARC::Record->new_from_xml($mfhd_xml);
@@ -108,48 +108,89 @@ sub mfhd_to_hash {
 
     $location =~ s/ -- $//;
 
+    # TODO: for now, we will assume that textual holdings are in addition to the 
+    # computable holdings (that is, they have link IDs greater than the 85X fields)
+    # or that they fully replace the computable holdings (checking for link ID '0').
+    # Eventually this may be handled better by format_holdings() in MFHD.pm
+    my %skip_computable;
     try {
         foreach my $field ($marc->field('866')) {
             my $textual_holdings = $self->format_textual_holdings($field);
             if ($textual_holdings) {
-                push @$holdings, $textual_holdings;
+                push @$basic_holdings_add, $textual_holdings;
+                if ($field->subfield('8') eq '0') {
+                   $skip_computable{'basic'} = 1; # link ID 0 trumps computable fields
+                }
             }
         }
         foreach my $field ($marc->field('867')) {
             my $textual_holdings = $self->format_textual_holdings($field);
             if ($textual_holdings) {
-                push @$supplements, $textual_holdings;
+                push @$supplement_holdings_add, $textual_holdings;
+                if ($field->subfield('8') eq '0') {
+                   $skip_computable{'supplement'} = 1; # link ID 0 trumps computable fields
+                }
             }
         }
         foreach my $field ($marc->field('868')) {
             my $textual_holdings = $self->format_textual_holdings($field);
             if ($textual_holdings) {
-                push @$indexes, $textual_holdings;
+                push @$index_holdings_add, $textual_holdings;
+                if ($field->subfield('8') eq '0') {
+                   $skip_computable{'index'} = 1; # link ID 0 trumps computable fields
+                }
             }
         }
 
-        foreach my $cap_id ($mfhd->caption_link_ids('853')) {
-            my @curr_holdings = $mfhd->holdings('863', $cap_id);
-            next unless scalar @curr_holdings;
-            foreach (@curr_holdings) {
-                push @$current_holdings, $_->format();
+        if (!exists($skip_computable{'basic'})) {
+            foreach my $cap_id ($mfhd->caption_link_ids('853')) {
+                my @holdings = $mfhd->holdings('863', $cap_id);
+                next unless scalar @holdings;
+                foreach (@holdings) {
+                    push @$basic_holdings, $_->format();
+                }
             }
+            if (!@$basic_holdings) { # no computed holdings found
+                $basic_holdings = $basic_holdings_add;
+                $basic_holdings_add = [];
+            }
+        } else { # textual are non additional, but primary
+            $basic_holdings = $basic_holdings_add;
+            $basic_holdings_add = [];
         }
 
-        foreach my $cap_id ($mfhd->caption_link_ids('854')) {
-            my @curr_supplements = $mfhd->holdings('864', $cap_id);
-            next unless scalar @curr_supplements;
-            foreach (@curr_supplements) {
-                push @$current_supplements, $_->format();
+        if (!exists($skip_computable{'supplement'})) {
+            foreach my $cap_id ($mfhd->caption_link_ids('854')) {
+                my @supplements = $mfhd->holdings('864', $cap_id);
+                next unless scalar @supplements;
+                foreach (@supplements) {
+                    push @$supplement_holdings, $_->format();
+                }
+            }
+            if (!@$supplement_holdings) { # no computed holdings found
+                $supplement_holdings = $supplement_holdings_add;
+                $supplement_holdings_add = [];
             }
+        } else { # textual are non additional, but primary
+            $supplement_holdings = $supplement_holdings_add;
+            $supplement_holdings_add = [];
         }
 
-        foreach my $cap_id ($mfhd->caption_link_ids('855')) {
-            my @curr_indexes = $mfhd->holdings('865', $cap_id);
-            next unless scalar @curr_indexes;
-            foreach (@curr_indexes) {
-                push @$current_indexes, $_->format();
+        if (!exists($skip_computable{'index'})) {
+            foreach my $cap_id ($mfhd->caption_link_ids('855')) {
+                my @indexes = $mfhd->holdings('865', $cap_id);
+                next unless scalar @indexes;
+                foreach (@indexes) {
+                    push @$index_holdings, $_->format();
+                }
+            }
+            if (!@$index_holdings) { # no computed holdings found
+                $index_holdings = $index_holdings_add;
+                $index_holdings_add = [];
             }
+        } else { # textual are non additional, but primary
+            $index_holdings = $index_holdings_add;
+            $index_holdings_add = [];
         }
 
         # Laurentian extensions
@@ -179,15 +220,16 @@ sub mfhd_to_hash {
     };
 
     return {
-        location            => $location,
-        holdings            => $holdings,
-        current_holdings    => $current_holdings,
-        supplements         => $supplements,
-        current_supplements => $current_supplements,
-        indexes             => $indexes,
-        current_indexes     => $current_indexes,
-        missing             => $missing,
-        incomplete          => $incomplete,
+        location                => $location,
+        basic_holdings          => $basic_holdings,
+        basic_holdings_add      => $basic_holdings_add,
+        supplement_holdings     => $supplement_holdings,
+        supplement_holdings_add => $supplement_holdings_add,
+        index_holdings          => $index_holdings,
+        index_holdings_add      => $index_holdings_add,
+        missing                 => $missing,
+        incomplete              => $incomplete,
+        online                  => $online
     };
 }
 
@@ -203,15 +245,15 @@ Initialize the serial virtual record (svr) instance
 
 sub init_holdings_virtual_record {
     my $record = Fieldmapper::serial::virtual_record->new;
-    $record->id();
+    $record->sre_id();
     $record->location();
     $record->owning_lib();
-    $record->holdings([]);
-    $record->current_holdings([]);
-    $record->supplements([]);
-    $record->current_supplements([]);
-    $record->indexes([]);
-    $record->current_indexes([]);
+    $record->basic_holdings([]);
+    $record->basic_holdings_add([]);
+    $record->supplement_holdings([]);
+    $record->supplement_holdings_add([]);
+    $record->index_holdings([]);
+    $record->index_holdings_add([]);
     $record->online([]);
     $record->missing([]);
     $record->incomplete([]);
@@ -238,7 +280,7 @@ sub generate_svr {
     my $record   = init_holdings_virtual_record();
     my $holdings = $self->mfhd_to_hash($mfhd);
 
-    $record->id($id);
+    $record->sre_id($id);
     $record->owning_lib($owning_lib);
 
     if (!$holdings) {
@@ -246,12 +288,12 @@ sub generate_svr {
     }
 
     $record->location($holdings->{location});
-    $record->holdings($holdings->{holdings});
-    $record->current_holdings($holdings->{current_holdings});
-    $record->supplements($holdings->{supplements});
-    $record->current_supplements($holdings->{current_supplements});
-    $record->indexes($holdings->{indexes});
-    $record->current_indexes($holdings->{current_indexes});
+    $record->basic_holdings($holdings->{basic_holdings});
+    $record->basic_holdings_add($holdings->{basic_holdings_add});
+    $record->supplement_holdings($holdings->{supplement_holdings});
+    $record->supplement_holdings_add($holdings->{supplement_holdings_add});
+    $record->index_holdings($holdings->{index_holdings});
+    $record->index_holdings_add($holdings->{index_holdings_add});
     $record->online($holdings->{online});
     $record->missing($holdings->{missing});
     $record->incomplete($holdings->{incomplete});
index 0945054..251d118 100644 (file)
@@ -68,7 +68,7 @@ CREATE TABLE config.upgrade_log (
     install_date    TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
 );
 
-INSERT INTO config.upgrade_log (version) VALUES ('0370'); -- miker
+INSERT INTO config.upgrade_log (version) VALUES ('0371'); -- senator
 
 CREATE TABLE config.bib_source (
        id              SERIAL  PRIMARY KEY,
diff --git a/Open-ILS/src/sql/Pg/upgrade/0371.schema.serial_supplement-fix.sql b/Open-ILS/src/sql/Pg/upgrade/0371.schema.serial_supplement-fix.sql
new file mode 100644 (file)
index 0000000..45994b0
--- /dev/null
@@ -0,0 +1,10 @@
+-- If these two fail, they're already renamed (DB built after 0352), so
+-- we're good.
+ALTER SEQUENCE serial.sup_summary_id_seq RENAME TO supplement_summary_id_seq;
+ALTER SEQUENCE serial.bib_summary_id_seq RENAME TO basic_summary_id_seq;
+
+BEGIN;  -- but we still need to consume an upgrade number :-/
+
+INSERT INTO config.upgrade_log (version) VALUES ('0371');   -- senator
+
+COMMIT;
index 000d879..7d515bf 100644 (file)
        "EDIT_MFHD_RECORD": "Edit Record",
        "EDIT_MFHD_MENU": "Edit Holdings",
        "EDIT_PROPERTIES": "Edit Propeties",
-       "HOLDINGS": "Previous volumes",
-       "INDEXES": "Previous indexes",
+       "BASIC_HOLDINGS": "Volumes",
+       "BASIC_HOLDINGS_ADD": "Additional Volume Information",
+       "INDEX_HOLDINGS": "Indexes",
+       "INDEX_HOLDINGS_ADD": "Additional Index Information",
        "CURRENT_HOLDINGS": "Current volume",
        "CURRENT_INDEXES": "Current indexes",
        "CURRENT_SUPPLEMENTS": "Current supplements",
@@ -28,5 +30,6 @@
        "MISSING_VOLUMES": "Missing volumes",
        "ONLINE_VOLUMES": "Online volumes",
        "SAVE_MFHD_LABEL": "Save MFHD",
-       "SUPPLEMENTS": "Previous supplements"
+       "SUPPLEMENT_HOLDINGS": "Supplements",
+       "SUPPLEMENT_HOLDINGS_ADD": "Additional Supplement Information"
 }
index 62f84e9..5bfd267 100644 (file)
 <!ENTITY staff.record_list.title "Title">
 <!ENTITY staff.record_list.win_title "Cataloging">
 <!ENTITY staff.retrieving.record "Retrieving...">
+<!ENTITY staff.serial.mfhd_menu.label "MFHD Holdings">
+<!ENTITY staff.serial.mfhd_menu.add.label "Add MFHD Record">
+<!ENTITY staff.serial.mfhd_menu.edit.label "Edit MFHD Record">
+<!ENTITY staff.serial.mfhd_menu.delete.label "Delete MFHD Record">
+<!ENTITY staff.serial.scap_editor.modify "Modify Caption and Pattern(s)">
+<!ENTITY staff.serial.scap_editor.modify.accesskey "M">
+<!ENTITY staff.serial.scap_editor.create "Create Caption and Pattern(s)">
+<!ENTITY staff.serial.scap_editor.create.accesskey "C">
+<!ENTITY staff.serial.scap_editor.notes "Caption and Pattern Notes">
+<!ENTITY staff.serial.scap_editor.notes.accesskey "N">
+<!ENTITY staff.serial.sdist_editor.modify "Modify Distribution(s)">
+<!ENTITY staff.serial.sdist_editor.modify.accesskey "M">
+<!ENTITY staff.serial.sdist_editor.create "Create Distribution(s)">
+<!ENTITY staff.serial.sdist_editor.create.accesskey "C">
+<!ENTITY staff.serial.sdist_editor.notes "Distribution Notes">
+<!ENTITY staff.serial.sdist_editor.notes.accesskey "N">
+<!ENTITY staff.serial.serctrl_view.label "Serial Control View">
+<!ENTITY staff.serial.siss_editor.modify "Modify Issuance(s)">
+<!ENTITY staff.serial.siss_editor.modify.accesskey "M">
+<!ENTITY staff.serial.siss_editor.create "Create Issuance(s)">
+<!ENTITY staff.serial.siss_editor.create.accesskey "C">
+<!ENTITY staff.serial.siss_editor.notes "Issuance Notes">
+<!ENTITY staff.serial.siss_editor.notes.accesskey "N">
+<!ENTITY staff.serial.sitem_editor.modify "Modify Item(s)">
+<!ENTITY staff.serial.sitem_editor.modify.accesskey "M">
+<!ENTITY staff.serial.sitem_editor.create "Create Item(s)">
+<!ENTITY staff.serial.sitem_editor.create.accesskey "C">
+<!ENTITY staff.serial.sitem_editor.notes "Item Notes">
+<!ENTITY staff.serial.sitem_editor.notes.accesskey "N">
+<!ENTITY staff.serial.ssub_editor.modify "Modify Subscription(s)">
+<!ENTITY staff.serial.ssub_editor.modify.accesskey "M">
+<!ENTITY staff.serial.ssub_editor.create "Create Subscription(s)">
+<!ENTITY staff.serial.ssub_editor.create.accesskey "C">
+<!ENTITY staff.serial.ssub_editor.notes "Subscription Notes">
+<!ENTITY staff.serial.ssub_editor.notes.accesskey "N">
 <!ENTITY staff.survey.wizard.page1 "Initial Settings">
 <!ENTITY staff.survey.wizard.page2 "Add Questions for Survey:">
 <!ENTITY staff.survey.wizard.title "Add a Survey Wizard">
index 93bdeef..adb2343 100644 (file)
@@ -43,6 +43,8 @@ var rdetailNext = null;
 var rdetailStart = null;
 var rdetailEnd = null;
 
+var mfhdDetails = [];
+
 /* serials are currently the only use of Dojo strings in the OPAC */
 if (rdetailDisplaySerialHoldings) {
        dojo.require("dijit.Menu");
@@ -268,6 +270,10 @@ function _holdingsDraw(h) {
 
        dojo.forEach(holdings, _holdingsDrawMFHD);
 
+       // Populate XUL menus
+       if (isXUL()) {
+               runEvt('rdetail','MFHDDrawn');
+       }
 }
 
 function _holdingsDrawMFHD(holdings, entryNum) {
@@ -281,19 +287,19 @@ function _holdingsDrawMFHD(holdings, entryNum) {
                }
         }
 
-       var hh = holdings.holdings();
-       var hch = holdings.current_holdings();
-       var hs = holdings.supplements();
-       var hcs = holdings.current_supplements();
-       var hi = holdings.indexes();
-       var hci = holdings.current_indexes();
+       var hb = holdings.basic_holdings();
+       var hba = holdings.basic_holdings_add();
+       var hs = holdings.supplement_holdings();
+       var hsa = holdings.supplement_holdings_add();
+       var hi = holdings.index_holdings();
+       var hia = holdings.index_holdings_add();
        var ho = holdings.online();
        var hm = holdings.missing();
        var hinc = holdings.incomplete();
        var hloc = holdings.location() || 'MFHD';
 
-       if (    hh.length == 0 && hch.length == 0 && hs.length == 0 &&
-               hcs.length == 0 && hi.length == 0 && hci.length == 0 &&
+       if (    hb.length == 0 && hba.length == 0 && hs.length == 0 &&
+               hsa.length == 0 && hi.length == 0 && hia.length == 0 &&
                ho.length == 0 && hm.length == 0 && hinc.length == 0
        ) {
 
@@ -303,41 +309,57 @@ function _holdingsDrawMFHD(holdings, entryNum) {
                         * record is likely empty or corrupt. This gives cataloguers a
                         * chance to add holdings or correct the record
                         */
-                       hh = 'PLACEHOLDER';
+                       hb = ['PLACEHOLDER'];
                } else {
                        return null;
                }
        }
 
-       dojo.place("<table style='width: 100%;'><caption id='mfhdHoldingsCaption" + entryNum + "' class='rdetail_header color_1'>" +
-               dojo.string.substitute(opac_strings.HOLDINGS_TABLE_CAPTION, [hloc]) +
+       // Show entryNum + 1 in staff client for better menu correlation
+       // Maybe this should be holdings.sre_id() instead? (which could get long after time)
+       var entryNumString = '';
+       if (isXUL()) {
+               var entryNumInc = entryNum + 1;
+               entryNumString = ' [Entry #'+entryNumInc+'] ';
+       }
+
+       var refNode;
+       if (entryNum > 0) {
+               refNode = 'rdetail_holdings_table_' + (entryNum - 1);
+       } else {
+               refNode = 'rdetail_details_table';
+       }
+
+       dojo.place("<table style='width: 100%;' id='rdetail_holdings_table_"+entryNum+"'><caption id='mfhdHoldingsCaption" + entryNum + "' class='rdetail_header color_1'>" +
+               dojo.string.substitute(opac_strings.HOLDINGS_TABLE_CAPTION, [hloc]) + entryNumString +
                "</caption><tbody id='rdetail_holdings_tbody_" + entryNum +
-               "'></tbody></table>", "rdetail_details_table", "after"
+               "'></tbody></table>", refNode, "after"
        );
-       if (hh.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.HOLDINGS, hh); }
-       if (hch.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.CURRENT_HOLDINGS, hch); }
-       if (hs.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.SUPPLEMENTS, hs); }
-       if (hcs.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.CURRENT_SUPPLEMENTS, hcs); }
-       if (hi.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.INDEXES, hi); }
-       if (hci.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.CURRENT_INDEXES, hci); }
+       if (hb.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.BASIC_HOLDINGS, hb); }
+       if (hba.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.BASIC_HOLDINGS_ADD, hba); }
+       if (hs.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.SUPPLEMENT_HOLDINGS, hs); }
+       if (hsa.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.SUPPLEMENT_HOLDINGS_ADD, hsa); }
+       if (hi.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.INDEX_HOLDINGS, hi); }
+       if (hia.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.INDEX_HOLDINGS_ADD, hia); }
        if (ho.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.ONLINE_VOLUMES, ho); }
        if (hm.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.MISSING_VOLUMES, hm); }
        if (hinc.length > 0) { _holdingsDrawMFHDEntry(entryNum, opac_strings.INCOMPLETE_VOLUMES, hinc); }
 
        if (isXUL()) {
+               mfhdDetails.push({ 'id' : holdings.sre_id(), 'label' : hloc, 'entryNum' : entryNum, 'owning_lib' : holdings.owning_lib() });
                dojo.require('openils.Event');
                dojo.require('openils.PermaCrud');
                var mfhd_edit = new dijit.Menu({});
-               new dijit.MenuItem({onClick: function(){loadMarcEditor(holdings.id())}, label:opac_strings.EDIT_MFHD_RECORD}).placeAt(mfhd_edit, "first");
+               new dijit.MenuItem({onClick: function(){loadMarcEditor(holdings.sre_id())}, label:opac_strings.EDIT_MFHD_RECORD}).placeAt(mfhd_edit, "first");
                new dijit.MenuItem({onClick:function(){
                        var pcrud = new openils.PermaCrud({"authtoken": G.user.session});
-                       var mfhd_rec = pcrud.retrieve("sre", holdings.id());
+                       var mfhd_rec = pcrud.retrieve("sre", holdings.sre_id());
                        if (mfhd_rec) {
                                pcrud.eliminate(mfhd_rec);
-                               alert(dojo.string.substitute(opac_strings.DELETED_MFHD_RECORD, [holdings.id()]));
+                               alert(dojo.string.substitute(opac_strings.DELETED_MFHD_RECORD, [holdings.sre_id()]));
                        }
                }, label:opac_strings.DELETE_MFHD}).placeAt(mfhd_edit, "last");
-               // new dijit.MenuItem({onClick:function(){alert("Edit properties " + holdings.id());}, label:opac_strings.EDIT_PROPERTIES}).placeAt(mfhd_edit, "last");
+               // new dijit.MenuItem({onClick:function(){alert("Edit properties " + holdings.sre_id());}, label:opac_strings.EDIT_PROPERTIES}).placeAt(mfhd_edit, "last");
                var mfhd_mb = new dijit.form.DropDownButton({dropDown: mfhd_edit, label:opac_strings.EDIT_MFHD_MENU, style:"float:right"});
                mfhd_mb.placeAt("mfhdHoldingsCaption" + entryNum, "last");
                mfhd_edit.startup();
index 8d80222..48abee9 100644 (file)
@@ -5,6 +5,7 @@ var marc_edit_reset = true;
 var copy_browser_reset = true;
 var hold_browser_reset = true;
 var acq_orders_reset = true;
+var serctrl_view_reset = true;
 
 function $(id) { return document.getElementById(id); }
 
@@ -361,6 +362,40 @@ function set_opac() {
                         }
                     }
                 );
+
+                $('mfhd_add').setAttribute('oncommand','create_mfhd()');
+                var mfhd_edit_menu = $('mfhd_edit');
+                var mfhd_delete_menu = $('mfhd_delete');
+
+                // clear menus on subsequent loads
+                if (mfhd_edit_menu.firstChild) {
+                    mfhd_edit_menu.removeChild(mfhd_edit_menu.firstChild);
+                    mfhd_delete_menu.removeChild(mfhd_delete_menu.firstChild);
+                }
+
+                mfhd_edit_menu.disabled = true;
+                mfhd_delete_menu.disabled = true;
+
+                win.attachEvt("rdetail", "MFHDDrawn",
+                    function() {
+                        if (win.mfhdDetails && win.mfhdDetails.length > 0) {
+                            g.mfhd = {};
+                            g.mfhd.details = win.mfhdDetails;
+                            mfhd_edit_menu.disabled = false;
+                            mfhd_delete_menu.disabled = false;
+                            for (var i = 0; i < win.mfhdDetails.length; i++) {
+                                var mfhd_details = win.mfhdDetails[i];
+                                var num = mfhd_details.entryNum;
+                                num++;
+                                var label = mfhd_details.label + ' (' + num + ')';
+                                var item = mfhd_edit_menu.appendItem(label);
+                                item.setAttribute('oncommand','open_mfhd_editor('+mfhd_details.id+')');
+                                item = mfhd_delete_menu.appendItem(label);
+                                item.setAttribute('oncommand','delete_mfhd('+mfhd_details.id+')');
+                            }
+                        }
+                    }
+                );
             },
             'url_prefix' : xulG.url_prefix,
         };
@@ -392,6 +427,97 @@ function set_opac() {
     bottom_pane.get_contentWindow().addEventListener('load',opac_wrapper_set_help_context,false);
 }
 
+function set_serctrl_view() {
+    g.view = 'serctrl_view';
+    if (serctrl_view_reset) {
+        bottom_pane.reset_iframe( xulG.url_prefix( urls.XUL_SERIAL_SERCTRL_MAIN ) + '?docid=' + window.escape(docid), {}, xulG);
+        serctrl_view_reset =false;
+    } else {
+        bottom_pane.set_iframe( xulG.url_prefix( urls.XUL_SERIAL_SERCTRL_MAIN ) + '?docid=' + window.escape(docid), {}, xulG);
+    }
+}
+
+function create_mfhd() {
+    try {
+        g.data.create_mfhd_aou = '';
+        JSAN.use('util.window'); var win = new util.window();
+        win.open(
+            xulG.url_prefix(urls.XUL_SERIAL_SELECT_AOU),
+            'sel_bucket_win' + win.window_name_increment(),
+            'chrome,resizable,modal,centerscreen'
+        );
+        if (!g.data.create_mfhd_aou) {
+            return;
+        }
+        var r = g.network.simple_request(
+                'MFHD_XML_RECORD_CREATE',
+                [ ses(), 1, g.data.create_mfhd_aou, docid ]
+            );
+        if (typeof r.ilsevent != 'undefined') {
+            throw(r);
+        }
+        alert("MFHD record created."); //TODO: better success message
+        //TODO: refresh opac display
+    } catch(E) {
+        g.error.standard_unexpected_error_alert("Create MFHD failed", E); //TODO: better error handling
+    }
+}
+
+function delete_mfhd(sre_id) {
+    if (g.error.yns_alert(
+        document.getElementById('offlineStrings').getFormattedString('serial.delete_record.confirm', [sre_id]),
+        document.getElementById('offlineStrings').getString('cat.opac.delete_record'),
+        document.getElementById('offlineStrings').getString('cat.opac.delete'),
+        document.getElementById('offlineStrings').getString('cat.opac.cancel'),
+        null,
+        document.getElementById('offlineStrings').getString('cat.opac.record_deleted.confirm')) == 0) {
+        var robj = g.network.request(
+                'open-ils.permacrud',
+                'open-ils.permacrud.delete.sre',
+                [ses(),sre_id]);
+        if (typeof robj.ilsevent != 'undefined') {
+            alert(document.getElementById('offlineStrings').getFormattedString('cat.opac.record_deleted.error',  [docid, robj.textcode, robj.desc]) + '\n');
+        } else {
+            alert(document.getElementById('offlineStrings').getString('cat.opac.record_deleted'));
+            //TODO: refresh opac display
+        }
+    }
+}
+
+function open_mfhd_editor(sre_id) {
+    try {
+        var r = g.network.simple_request(
+                'FM_SRE_RETRIEVE',
+                [ ses(), sre_id ]
+              );
+        if (typeof r.ilsevent != 'undefined') {
+            throw(r);
+        }
+        open_marc_editor(r, 'MFHD');
+    } catch(E) {
+        g.error.standard_unexpected_error_alert("Create MFHD failed", E); //TODO: better error handling
+    }
+}
+
+function open_marc_editor(rec, label) {
+    win = window.open( xulG.url_prefix('/xul/server/cat/marcedit.xul') );
+
+    win.xulG = {
+        record : {marc : rec.marc()},
+        save : {
+            label: 'Save ' + label,
+            func: function(xmlString) {  // TODO: switch to pcrud, or define an sre update method in Serial.pm?
+                var method = 'open-ils.permacrud.update.' + rec.classname;
+                rec.marc(xmlString);
+                g.network.request(
+                    'open-ils.permacrud', method,
+                    [ses(), rec]
+                );
+            }
+        }
+    };
+}
+
 function bib_in_new_tab() {
     try {
         var url = browser_frame.contentWindow.g.browser.controller.view.browser_browser.contentWindow.wrappedJSObject.location.href;
@@ -431,7 +557,7 @@ function add_to_bucket() {
     win.open(
         xulG.url_prefix(urls.XUL_RECORD_BUCKETS_QUICK),
         'sel_bucket_win' + win.window_name_increment(),
-        'chrome,resizable,modal,center',
+        'chrome,resizable,modal,centerscreen',
         {
             record_ids: [ docid ]
         }
@@ -565,6 +691,7 @@ function refresh_display(id) {
             case 'copy_browser' : set_copy_browser(); break;
             case 'hold_browser' : set_hold_browser(); break;
             case 'acq_orders' : set_acq_orders(); break;
+            case 'serctrl_view' : set_serctrl_view(); break;
             case 'opac' :
             default: set_opac(); break;
         }
index 929ab38..a758c92 100644 (file)
                 <menuseparator/>
                 <menuitem label="&staff.cat.opac.default.label;" id="default" oncommand="set_default();"/>
                 <menuitem label="&staff.cat.opac.refresh_me.label;" id="refresh_me" oncommand="refresh_display(docid);"/>
+                <menuseparator/>
+                <menu id="mfhd_menu" label="&staff.serial.mfhd_menu.label;">
+                    <menupopup id="mfhd_popup">
+                        <menuitem id="mfhd_add" label="&staff.serial.mfhd_menu.add.label;"/>
+                        <menu id="mfhd_edit" label="&staff.serial.mfhd_menu.edit.label;"/>
+                        <menu id="mfhd_delete" label="&staff.serial.mfhd_menu.delete.label;"/>
+                    </menupopup>
+                </menu>
+                <menuitem id="serctrl_view" label="&staff.serial.serctrl_view.label;" oncommand="set_serctrl_view();"/>
                 </menupopup>
                 </menu>
             </menubar>
index cf50fd0..f236154 100644 (file)
@@ -276,6 +276,37 @@ var api = {
     'MARC_HTML_RETRIEVE' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.biblio.record.html', 'secure' : false },
     'FM_BLOB_RETRIEVE_VIA_Z3950_SEARCH' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.z3950.search_class' },
     'FM_BLOB_RETRIEVE_VIA_Z3950_RAW_SEARCH' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.z3950.search_service' },
+    'FM_SCAP_BATCH_RETRIEVE' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.caption_and_pattern.batch.retrieve', 'secure' : false },
+    'FM_SCAP_BATCH_RETRIEVE.authoritative' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.caption_and_pattern.batch.retrieve', 'secure' : false },
+    'FM_SDIST_FLESHED_BATCH_RETRIEVE' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.distribution.fleshed.batch.retrieve', 'secure' : false },
+    'FM_SDIST_FLESHED_BATCH_RETRIEVE.authoritative' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.distribution.fleshed.batch.retrieve.authoritative', 'secure' : false },
+    'FM_SDIST_ID_LIST' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.id_list.sdist'},
+    'FM_SDIST_RETRIEVE' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.retrieve.sdist'},
+    'FM_SDIST_SEARCH' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.sdist'},
+    'FM_SDISTN_CREATE' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.distribution_note.create' },
+    'FM_SDISTN_DELETE' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.distribution_note.delete', 'secure' : false },
+    'FM_SDISTN_RETRIEVE_ALL' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.distribution_note.retrieve.all', 'secure' : false },
+    'FM_SIN_CREATE' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.item_note.create' },
+    'FM_SIN_DELETE' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.item_note.delete', 'secure' : false },
+    'FM_SIN_RETRIEVE_ALL' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.item_note.retrieve.all', 'secure' : false },
+    'FM_SISS_FLESHED_BATCH_RETRIEVE' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.issuance.fleshed.batch.retrieve', 'secure' : false },
+    'FM_SISS_FLESHED_BATCH_RETRIEVE.authoritative' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.issuance.fleshed.batch.retrieve.authoritative', 'secure' : false },
+    'FM_SITEM_FLESHED_BATCH_RETRIEVE' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.item.fleshed.batch.retrieve', 'secure' : false },
+    'FM_SITEM_FLESHED_BATCH_RETRIEVE.authoritative' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.item.fleshed.batch.retrieve.authoritative', 'secure' : false },
+    'FM_SITEM_ID_LIST' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.id_list.sitem'},
+    'FM_SITEM_RETRIEVE' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.retrieve.sitem'},
+    'FM_SITEM_SEARCH' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.sitem'},
+    'FM_SRE_RETRIEVE' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.retrieve.sre'},
+    'FM_SRE_SEARCH' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.sre'},
+    'FM_SSUB_AOU_IDS_RETRIEVE_VIA_RECORD_ID' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.subscription.retrieve_orgs_by_title', 'secure' : false },
+    'FM_SSUB_AOU_IDS_RETRIEVE_VIA_RECORD_ID.authoritative' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.subscription.retrieve_orgs_by_title.authoritative', 'secure' : false },
+    'FM_SSUB_FLESHED_BATCH_RETRIEVE' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.subscription.fleshed.batch.retrieve', 'secure' : false },
+    'FM_SSUB_FLESHED_BATCH_RETRIEVE.authoritative' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.subscription.fleshed.batch.retrieve.authoritative', 'secure' : false },
+    'FM_SSUB_TREE_LIST_RETRIEVE_VIA_RECORD_ID_AND_ORG_IDS' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.subscription_tree.retrieve', 'secure' : false },
+    'FM_SSUB_TREE_LIST_RETRIEVE_VIA_RECORD_ID_AND_ORG_IDS.authoritative' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.subscription_tree.retrieve.authoritative', 'secure' : false },
+    'FM_SSUBN_CREATE' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.subscription_note.create' },
+    'FM_SSUBN_DELETE' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.subscription_note.delete', 'secure' : false },
+    'FM_SSUBN_RETRIEVE_ALL' : { 'app' : 'open-ils.serial', 'method' : 'open-ils.serial.subscription_note.retrieve.all', 'secure' : false },
     'RETRIEVE_Z3950_SERVICES' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.z3950.retrieve_services', 'secure' : false },
     'MARK_ITEM_DAMAGED' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.mark_item_damaged' },
     'MARK_ITEM_MISSING_PIECES' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.mark_item_missing_pieces' },
@@ -397,6 +428,11 @@ var urls = {
     'XUL_RECORD_BUCKETS' : '/xul/server/cat/record_buckets.xul',
     'XUL_RECORD_BUCKETS_QUICK' : '/xul/server/cat/record_buckets_quick.xul',
     'XUL_REMOTE_BROWSER' : '/xul/server/util/rbrowser.xul',
+    'XUL_SERIAL_ITEM_EDITOR' : '/xul/server/serial/sitem_editor.xul',
+    'XUL_SERIAL_NOTES' : '/xul/server/serial/notes.xul',
+    'XUL_SERIAL_SELECT_AOU' : '/xul/server/serial/select_aou.xul',
+    'XUL_SERIAL_SELECT_UNIT' : '/xul/server/serial/select_unit.xul',
+    'XUL_SERIAL_SERCTRL_MAIN' : '/xul/server/serial/serctrl_main.xul',
     'XUL_SPINE_LABEL' : '/xul/server/cat/spine_labels.xul',
     'XUL_STAGED_PATRONS' : '/xul/server/patron/staged.xul',
     'XUL_STANDALONE' : 'chrome://open_ils_staff_client/content/circ/offline.xul',
diff --git a/Open-ILS/xul/staff_client/server/locale/en-US/serial.properties b/Open-ILS/xul/staff_client/server/locale/en-US/serial.properties
new file mode 100644 (file)
index 0000000..0abd318
--- /dev/null
@@ -0,0 +1,55 @@
+staff.serial.editor_base.handle_update.error=serial update error:
+staff.serial.editor_base.handle_update.success=Save Successful
+staff.serial.scap_editor.count=1 caption and pattern
+staff.serial.scap_editor.count.plural=%1$s caption and patterns
+staff.serial.scap_editor.create=Create Caption and Pattern(s)
+staff.serial.scap_editor.create.accesskey=C
+staff.serial.scap_editor.modify=Modify Caption and Pattern(s)
+staff.serial.scap_editor.modify.accesskey=M
+staff.serial.scap_editor.notes=Caption and Pattern Notes
+staff.serial.sdist_editor.count=1 distribution
+staff.serial.sdist_editor.count.plural=%1$s distributions
+staff.serial.sdist_editor.create=Create Distribution(s)
+staff.serial.sdist_editor.create.accesskey=C
+staff.serial.sdist_editor.modify=Modify Distribution(s)
+staff.serial.sdist_editor.modify.accesskey=M
+staff.serial.sdist_editor.notes=Distribution Notes
+staff.serial.siss_editor.count=1 issuance
+staff.serial.siss_editor.count.plural=%1$s issuances
+staff.serial.siss_editor.create=Create Issuance(s)
+staff.serial.siss_editor.create.accesskey=C
+staff.serial.siss_editor.modify=Modify Issuance(s)
+staff.serial.siss_editor.modify.accesskey=M
+staff.serial.siss_editor.notes=Issuance Notes
+staff.serial.sitem_editor.count=1 item
+staff.serial.sitem_editor.count.plural=%1$s items
+staff.serial.sitem_editor.create=Create Item(s)
+staff.serial.sitem_editor.create.accesskey=C
+staff.serial.sitem_editor.modify=Modify Item(s)
+staff.serial.sitem_editor.modify.accesskey=M
+staff.serial.sitem_editor.notes=Item Notes
+staff.serial.ssub_editor.count=1 subscription
+staff.serial.ssub_editor.count.plural=%1$s subscriptions
+staff.serial.ssub_editor.create=Create Subscription(s)
+staff.serial.ssub_editor.create.accesskey=C
+staff.serial.ssub_editor.modify=Modify Subscription(s)
+staff.serial.ssub_editor.modify.accesskey=M
+staff.serial.ssub_editor.notes=Subscription Notes
+staff.serial.manage_subs.add.error=error adding object in manage_subs.js:
+staff.serial.manage_subs.delete.error=error deleting object in manage_subs.js:
+staff.serial.manage_subs.delete_scap.confirm=Are you sure you would like to delete this caption and pattern?
+staff.serial.manage_subs.delete_scap.confirm.plural=Are you sure you would like to delete these %1$s caption and patterns?
+staff.serial.manage_subs.delete_scap.title=Delete Caption and Patterns?
+staff.serial.manage_subs.delete_scap.override=Override Delete Failure? Doing so will delete all attached issuances and items as well!
+staff.serial.manage_subs.delete_sdist.confirm=Are you sure you would like to delete this distribution?
+staff.serial.manage_subs.delete_sdist.confirm.plural=Are you sure you would like to delete these %1$s distributions?
+staff.serial.manage_subs.delete_sdist.title=Delete Distributions?
+staff.serial.manage_subs.delete_sdist.override=Override Delete Failure?
+staff.serial.manage_subs.delete_siss.confirm=Are you sure you would like to delete this issuance?
+staff.serial.manage_subs.delete_siss.confirm.plural=Are you sure you would like to delete these %1$s issuances?
+staff.serial.manage_subs.delete_siss.title=Delete Issuances?
+staff.serial.manage_subs.delete_siss.override=Override Delete Failure? Doing so will delete all attached items as well!
+staff.serial.manage_subs.delete_ssub.confirm=Are you sure you would like to delete this subscription?
+staff.serial.manage_subs.delete_ssub.confirm.plural=Are you sure you would like to delete these %1$s subscriptions?
+staff.serial.manage_subs.delete_ssub.title=Delete Subscriptions?
+staff.serial.manage_subs.delete_ssub.override=Override Delete Failure? Doing so will delete all related data as well!
diff --git a/Open-ILS/xul/staff_client/server/serial/editor_base.js b/Open-ILS/xul/staff_client/server/serial/editor_base.js
new file mode 100644 (file)
index 0000000..4caf526
--- /dev/null
@@ -0,0 +1,593 @@
+dump('entering serial/editor_base.js\n');
+// vim:noet:sw=4:ts=4:
+
+if (typeof serial == 'undefined') serial = {};
+
+serial.editor_base = {
+
+    'editor_base_init' : function (params) {
+        var obj = this;
+        try {
+            /******************************************************************************************************/
+            /* setup JSAN and some initial libraries */
+
+            netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+            if (typeof JSAN == 'undefined') {
+                throw( $('commonStrings').getString('common.jsan.missing') );
+            }
+            JSAN.errorLevel = "die"; // none, warn, or die
+            JSAN.addRepository('/xul/server/');
+            JSAN.use('util.error'); obj.error = new util.error();
+            obj.error.sdump('D_TRACE','my_init() for serial/editor_base.js');
+
+            JSAN.use('util.functional');
+            JSAN.use('OpenILS.data'); obj.data = new OpenILS.data(); obj.data.init({'via':'stash'});
+            JSAN.use('util.network'); obj.network = new util.network();
+
+
+            /******************************************************************************************************/
+            /* base vars */
+
+            obj.docid = xul_param('docid',{'modal_xulG':true});
+            
+            if (typeof params.handle_update == 'undefined') {
+                obj.handle_update = xul_param('handle_update',{'modal_xulG':true});
+            } else {
+                obj.handle_update = params.handle_update;
+            }
+
+            obj.trigger_refresh = params.trigger_refresh;
+            obj.refresh_command = params.refresh_command;
+            var fm_type = obj.fm_type;
+            var fm_type_plural = obj.fm_type_plural;
+            var retrieve_function = params.retrieve_function;
+            var retrieve_params = params.retrieve_params;
+            if (!retrieve_params) {
+                retrieve_params = [];
+            }
+
+            /******************************************************************************************************/
+            /* Get the fm_type ids from various sources and flesh them */
+
+            var fm_type_ids = params[fm_type + '_ids'];
+            if (!fm_type_ids) fm_type_ids = xul_param(fm_type + '_ids',{'concat':true,'JSON2js_if_cgi':true,'JSON2js_if_xulG':true,'JSON2js_if_xpcom':true,'stash_name':'temp_' + fm_type + '_ids','clear_xpcom':true,'modal_xulG':true});
+            if (!fm_type_ids) fm_type_ids = [];
+
+            obj[fm_type_plural] = [];
+            retrieve_params.push(fm_type_ids);
+            if (fm_type_ids.length > 0) obj[fm_type_plural] = obj.network.simple_request(
+                retrieve_function,
+                retrieve_params
+            );
+
+
+            /******************************************************************************************************/
+            /* And other fleshed copies if any */
+
+            if (!obj[fm_type_plural]) obj[fm_type_plural] = [];
+            var fms = params[fm_type_plural];
+            if (!fms) fms = xul_param(fm_type_plural,{'concat':true,'JSON2js_if_cgi':true,'JSON2js_if_xpcom':true,'stash_name':'temp_' + fm_type_plural,'clear_xpcom':true,'modal_xulG':true})
+            if (fms) obj[fm_type_plural] = obj[fm_type_plural].concat(fms);
+
+
+            // If we have just one, wrap in array
+            if (!obj[fm_type_plural].length) {
+                obj[fm_type_plural] = [obj[fm_type_plural]];
+            }
+
+
+            /******************************************************************************************************/
+
+            //obj.init_panes0();
+            obj.init_panes();
+
+            /******************************************************************************************************/
+            /* Is the interface an editor or a viewer, single or multi copy, existing copies or new copies? */
+
+            var do_edit;
+            if (typeof params.do_edit == 'undefined') {
+                do_edit = xul_param('do_edit',{'modal_xulG':true});
+            } else {
+                do_edit = params.do_edit;
+            }
+
+            if (do_edit) { 
+
+                // Editor desired, but let's check permissions
+                obj.do_edit = false;
+
+                try {
+                    /* FIXME: add permission check
+                    var check = obj.network.simple_request(
+                        'PERM_MULTI_ORG_CHECK',
+                        [ 
+                            ses(), 
+                            obj.data.list.au[0].id(), 
+                            util.functional.map_list(
+                                obj[fm_type_plural],
+                                function (o) {
+                                    var lib;
+                                    var cn_id = o.call_number();
+                                    if (cn_id == -1) {
+                                        lib = o.circ_lib(); // base perms on circ_lib instead of owning_lib if pre-cat
+                                    } else {
+                                        if (! obj.map_acn[ cn_id ]) {
+                                            var req = obj.network.simple_request('FM_ACN_RETRIEVE.authoritative',[ cn_id ]);
+                                            if (typeof req.ilsevent == 'undefined') {
+                                                obj.map_acn[ cn_id ] = req;
+                                                lib = obj.map_acn[ cn_id ].owning_lib();
+                                            } else {
+                                                lib = o.circ_lib();
+                                            }
+                                        } else {
+                                            lib = obj.map_acn[ cn_id ].owning_lib();
+                                        }
+                                    }
+                                    return typeof lib == 'object' ? lib.id() : lib;
+                                }
+                            ),
+                            obj[fm_type_plural].length == 1 ? [ 'UPDATE_COPY' ] : [ 'UPDATE_COPY', 'UPDATE_BATCH_COPY' ]
+                        ]
+                    ); */
+                    var check = [];
+                    obj.do_edit = check.length == 0;
+                } catch(E) {
+                    obj.error.standard_unexpected_error_alert('batch permission check',E);
+                }
+
+                if (obj.do_edit) {
+                    $(fm_type + '_save').setAttribute('hidden','false'); 
+                } else {
+                    $('top_nav').setAttribute('hidden','true');
+                }
+            } else {
+                $('top_nav').setAttribute('hidden','true');
+            }
+
+
+            if (obj[fm_type_plural].length > 0 && obj[fm_type_plural][0].isnew()) {
+                obj.mode = 'create';
+                if (obj.can_have_notes) $(fm_type + '_notes').setAttribute('hidden','true');
+                $(fm_type + '_save').setAttribute('label', $('serialStrings').getString('staff.serial.' + fm_type + '_editor.create'));
+                $(fm_type + '_save').setAttribute('accesskey', $('serialStrings').getString('staff.serial.' + fm_type + '_editor.create.accesskey'));
+            } else if (obj.mode == 'create') { // switching from create to modify
+                obj.mode = 'modify';
+                if (obj.can_have_notes) $(fm_type + '_notes').setAttribute('hidden','false');
+                $(fm_type + '_save').setAttribute('label', $('serialStrings').getString('staff.serial.' + fm_type + '_editor.modify'));
+                $(fm_type + '_save').setAttribute('accesskey', $('serialStrings').getString('staff.serial.' + fm_type + '_editor.modify.accesskey'));
+            }
+/*else {
+                obj.panes_and_field_names.left_pane = 
+                    [
+                        [
+                            $('catStrings').getString('staff.cat.copy_editor.status'),
+                            { 
+                                render: 'typeof fm.status() == "object" ? fm.status().name() : obj.data.hash.ccs[ fm.status() ].name()', 
+                                input: obj.safe_to_edit_copy_status() ? 'c = function(v){ obj.apply("status",v); if (typeof post_c == "function") post_c(v); }; x = util.widgets.make_menulist( util.functional.map_list( obj.data.list.ccs, function(obj) { return [ obj.name(), obj.id(), typeof my_constants.magical_statuses[obj.id()] != "undefined" ? true : false ]; } ).sort() ); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);' : undefined,
+                                //input: 'c = function(v){ obj.apply("status",v); if (typeof post_c == "function") post_c(v); }; x = util.widgets.make_menulist( util.functional.map_list( util.functional.filter_list( obj.data.list.ccs, function(obj) { return typeof my_constants.magical_statuses[obj.id()] == "undefined"; } ), function(obj) { return [ obj.name(), obj.id() ]; } ).sort() ); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                            }
+                        ]
+                    ].concat(obj.panes_and_field_names.left_pane);
+            }*/
+
+            if (obj[fm_type_plural].length != 1) {
+                document.getElementById(fm_type + '_notes').setAttribute('hidden','true');
+            }
+
+            // clear change markers
+            obj.changed = {};
+
+            /******************************************************************************************************/
+            /* Show the Record Details? (only for 'in_modal' mode)*/
+
+            var bdb;
+            if (xul_param('in_modal',{'modal_xulG':true}) && obj.docid) {
+                bdb = document.getElementById('brief_display_box'); while(bdb.firstChild) bdb.removeChild(bdb.lastChild);
+                var brief_display = document.createElement('iframe'); bdb.appendChild(brief_display); 
+                brief_display.setAttribute( 'src', urls.XUL_BIB_BRIEF + '?docid=' + obj.docid); // this is a modal window, so can't push in xulG
+                brief_display.setAttribute( 'flex','1' );
+            }
+
+            /******************************************************************************************************/
+            /* Backup copies :) */
+
+            obj['original_' + fm_type_plural] = js2JSON( obj[fm_type_plural] );
+
+        } catch(E) {
+            var err_msg = $("commonStrings").getFormattedString('common.exception', ['serial/' + fm_type +'_editor.js - init', E]);
+            try { obj.error.sdump('D_ERROR',err_msg); } catch(E) { dump(err_msg); dump(js2JSON(E)); }
+            alert(err_msg);
+        }
+    },
+
+    /******************************************************************************************************/
+    /* Restore backup copies */
+
+    'editor_base_reset' : function() {
+        var obj = this;
+        var fm_type_plural = obj.fm_type_plural;
+
+        obj.changed = {};
+        obj[fm_type_plural] = JSON2js( obj['original_' + fm_type_plural] );
+        obj.summarize( obj[fm_type_plural] );
+        obj.render();
+    },
+
+    /******************************************************************************************************/
+    /* Apply a value to a specific field on all the copies being edited */
+    /* Don't forget to use util.money.sanitize if dealing with money values */
+
+    'editor_base_apply' : function(field, value, loop_func) {
+        var obj = this;
+        var fm_type_plural = obj.fm_type_plural;
+
+        var do_loop_func = (typeof loop_func == 'function');
+
+        obj.error.sdump('D_TRACE','applying field = <' + field + '>  value = <' + value + '>\n');
+        if (value == '<HACK:KLUDGE:NULL>') value = null;
+        for (var i = 0; i < obj[fm_type_plural].length; i++) {
+            var fm = obj[fm_type_plural][i];
+            try {
+                fm[field]( value ); fm.ischanged('1');
+                if (do_loop_func) {
+                    loop_func(fm);
+                }
+            } catch(E) {
+                alert(E);
+            }
+        }
+    },
+
+
+    /******************************************************************************************************/
+    /* This loops through all our fieldnames and all the copies, tallying up counts for the different values */
+
+    'editor_base_summarize' : function(my_fms) {
+        var obj = this;
+        /******************************************************************************************************/
+        /* Setup */
+
+        JSAN.use('util.date'); JSAN.use('util.money');
+        obj.summary = {};
+        obj.field_names = [];
+        for (var i in obj.panes_and_field_names) {
+            obj.field_names = obj.field_names.concat( obj.panes_and_field_names[i] );
+        }
+
+        /******************************************************************************************************/
+        /* Loop through the field names */
+
+        for (var i = 0; i < obj.field_names.length; i++) {
+
+            var field_name = obj.field_names[i][0];
+            var render = obj.field_names[i][1].render;
+            var attr = obj.field_names[i][1].attr;
+            var value_key = obj.field_names[i][1].value_key;
+            var dropdown_key = obj.field_names[i][1].dropdown_key;
+            obj.summary[ field_name ] = {};
+
+            /******************************************************************************************************/
+            /* Loop through the copies */
+
+            for (var j = 0; j < my_fms.length; j++) {
+
+                var fm = my_fms[j];
+                var cmd = render || ('fm.' + field_name + '();');
+                var value = '???';
+
+                /**********************************************************************************************/
+                /* Try to retrieve the value for this field for this copy */
+
+                try { 
+                    value = eval( cmd );
+                    if (typeof(value) == 'undefined') {
+                        value = "";
+                    }
+                    if (dropdown_key) {
+                        obj.editor_values[value_key] = eval(dropdown_key);
+                    } else if (value_key) {
+                        obj.editor_values[value_key] = value;
+                    }
+                    if (value == "") {
+                        value = "<Unset>";
+                    }
+                } catch(E) { 
+                    obj.error.sdump('D_ERROR','Attempted ' + cmd + '\n' +  E + '\n'); 
+                }
+                if (typeof value == 'object' && value != null) {
+                    alert('FIXME: field_name = <' + field_name + '>  value = <' + js2JSON(value) + '>\n');
+                }
+
+                /**********************************************************************************************/
+                /* Tally the count */
+
+                if (obj.summary[ field_name ][ value ]) {
+                    obj.summary[ field_name ][ value ]++;
+                } else {
+                    obj.summary[ field_name ][ value ] = 1;
+                }
+            }
+        }
+        obj.error.sdump('D_TRACE','summary = ' + js2JSON(obj.summary) + '\n');
+    },
+
+    /******************************************************************************************************/
+    /* Display the summarized data and inputs for editing */
+
+    'editor_base_render' : function() {
+        var obj = this;
+        var fm_type = obj.fm_type;
+
+        /******************************************************************************************************/
+        /* Library setup and clear any existing interface */
+
+        JSAN.use('util.widgets'); JSAN.use('util.date'); JSAN.use('util.money'); JSAN.use('util.functional');
+
+        for (var i in obj.panes_and_field_names) {
+            var p = document.getElementById(i);
+            if (p) util.widgets.remove_children(p);
+        }
+
+        /******************************************************************************************************/
+        /* Prepare the panes */
+
+        var groupbox; var caption; var vbox; var grid; var rows;
+        
+        /******************************************************************************************************/
+        /* Loop through the field names */
+
+        for (h in obj.panes_and_field_names) {
+            if (!document.getElementById(h)) continue;
+            for (var i = 0; i < obj.panes_and_field_names[h].length; i++) {
+                try {
+                    var f = obj.panes_and_field_names[h][i]; var fn = f[0]; var attr = f[1].attr;
+                    groupbox = document.createElement('groupbox'); document.getElementById(h).appendChild(groupbox);
+                    if (attr) {
+                        for (var a in attr) {
+                            groupbox.setAttribute(a,attr[a]);
+                        }
+                    }
+                    if (typeof obj.changed[fn] != 'undefined') {
+                        groupbox.setAttribute('class','copy_editor_field_changed');
+                    }
+                    caption = document.createElement('caption'); groupbox.appendChild(caption);
+                    caption.setAttribute('label',fn); caption.setAttribute('id','caption_'+fn);
+                    vbox = document.createElement('vbox'); groupbox.appendChild(vbox);
+                    grid = util.widgets.make_grid( [ { 'flex' : 1 }, {}, {} ] ); vbox.appendChild(grid);
+                    grid.setAttribute('flex','1');
+                    rows = grid.lastChild;
+                    var row;
+                    
+                    /**************************************************************************************/
+                    /* Loop through each value for the field */
+
+                    for (var j in obj.summary[fn]) {
+                        var value = j; var count = obj.summary[fn][j];
+                        row = document.createElement('row'); rows.appendChild(row);
+                        var label1 = document.createElement('description'); row.appendChild(label1);
+                        label1.setAttribute('id',fn + '_label');
+                        //if (obj.special_exception[ fn ]) {
+                        //     obj.special_exception[ fn ]( label1, value );
+                        //} else {
+                            label1.appendChild( document.createTextNode(value) );
+                        //}
+                        var label2 = document.createElement('description'); row.appendChild(label2);
+                        var fm_count;
+                        if (count == 1) {
+                            fm_count = $('serialStrings').getString('staff.serial.' + fm_type +'_editor.count');
+                        } else {
+                            fm_count = $('serialStrings').getFormattedString('staff.serial.' + fm_type +'_editor.count.plural', [count]);
+                        }
+                        label2.appendChild( document.createTextNode(fm_count) );
+                    }
+                    var hbox = document.createElement('hbox'); 
+                    hbox.setAttribute('id',fn);
+                    groupbox.appendChild(hbox);
+                    var hbox2 = document.createElement('hbox');
+                    groupbox.appendChild(hbox2);
+
+                    /**************************************************************************************/
+                    /* Render the input widget */
+
+                    if (f[1].input && obj.do_edit) {
+                        obj.render_input(hbox,f[1]);
+                    }
+
+                } catch(E) {
+                    obj.error.sdump('D_ERROR','copy editor: ' + E + '\n');
+                }
+            }
+        }
+        
+        
+        /******************************************************************************************************/
+        /* Synchronize stat cat visibility with library filter menu, and default template selection */
+        JSAN.use('util.file'); 
+        var file = new util.file(fm_type + '_editor_prefs.'+obj.data.server_unadorned);
+        obj[fm_type + '_editor_prefs'] = util.widgets.load_attributes(file);
+        for (var i in obj[fm_type + '_editor_prefs']) {
+            if (i.match(/filter_/) && obj[fm_type + '_editor_prefs'][i].checked == '') {
+                try { 
+                    obj.toggle_stat_cat_display( document.getElementById(i) ); 
+                } catch(E) { alert(E); }
+            }
+        }
+        if (obj.template_menu) obj.template_menu.value = obj.template_menu.getAttribute('value');
+
+    },
+
+    /******************************************************************************************************/
+    /* This actually draws the change button and input widget for a given field */
+    'editor_base_render_input' : function(node, blob) {
+        var obj = this;
+        var fm_type_plural = obj.fm_type_plural;
+
+        try {
+            // node = hbox ;    groupbox ->  hbox, hbox
+
+            var groupbox = node.parentNode;
+            var caption = groupbox.firstChild;
+            var vbox = node.previousSibling;
+            var hbox = node;
+            var hbox2 = node.nextSibling;
+
+            var input_cmd = blob.input;
+            var render_cmd = blob.render;
+            var attr = blob.attr;
+
+            var block = false; var first = true;
+
+            function on_mouseover(ev) {
+                groupbox.setAttribute('style','background: white');
+            }
+
+            function on_mouseout(ev) {
+                groupbox.setAttribute('style','');
+            }
+
+            vbox.addEventListener('mouseover',on_mouseover,false);
+            vbox.addEventListener('mouseout',on_mouseout,false);
+            groupbox.addEventListener('mouseover',on_mouseover,false);
+            groupbox.addEventListener('mouseout',on_mouseout,false);
+            groupbox.firstChild.addEventListener('mouseover',on_mouseover,false);
+            groupbox.firstChild.addEventListener('mouseout',on_mouseout,false);
+
+            function on_click(ev){
+                try {
+                    if (block) return; block = true;
+
+                    function post_c(v, unchanged) {
+                        try {
+                            /* dbw2 not needed?
+                            var t = input_cmd.match('apply_stat_cat') ? 'stat_cat' : ( input_cmd.match('apply_owning_lib') ? 'owning_lib' : 'attribute' );
+                            var f;
+                            switch(t) {
+                                case 'attribute' :
+                                    f = input_cmd.match(/apply.?\("(.+?)",/)[1];
+                                break;
+                                case 'stat_cat' :
+                                    f = input_cmd.match(/apply_stat_cat\((.+?),/)[1];
+                                break;
+                                case 'owning_lib' :
+                                    f = null;
+                                break;
+                            }
+                            obj.changed[ hbox.id ] = { 'type' : t, 'field' : f, 'value' : v }; */
+                            if (!unchanged) {
+                                obj.changed[ hbox.id ] = true;
+                            }
+                            block = false;
+                            setTimeout(
+                                function() {
+                                    obj.summarize( obj[fm_type_plural] );
+                                    obj.render();
+                                    document.getElementById(caption.id).focus();
+                                }, 0
+                            );
+                        } catch(E) {
+                            obj.error.standard_unexpected_error_alert('post_c',E);
+                        }
+                    }
+                    var x; var c; eval( input_cmd );
+                    if (x) {
+                        util.widgets.remove_children(vbox);
+                        util.widgets.remove_children(hbox);
+                        util.widgets.remove_children(hbox2);
+                        hbox.appendChild(x);
+                        var apply = document.createElement('button');
+                        apply.setAttribute('label', $('catStrings').getString('staff.cat.copy_editor.apply.label'));
+                        apply.setAttribute('accesskey', $('catStrings').getString('staff.cat.copy_editor.apply.accesskey'));
+                        hbox2.appendChild(apply);
+                        apply.addEventListener('command',function() { c(x.value); },false);
+                        var cancel = document.createElement('button');
+                        cancel.setAttribute('label', $('catStrings').getString('staff.cat.copy_editor.cancel.label'));
+                        cancel.addEventListener('command',function() { setTimeout( function() { obj.summarize( obj[fm_type_plural] ); obj.render(); document.getElementById(caption.id).focus(); }, 0); }, false);
+                        hbox2.appendChild(cancel);
+                        setTimeout( function() { x.focus(); }, 0 );
+                    }
+                } catch(E) {
+                    obj.error.standard_unexpected_error_alert('render_input',E);
+                }
+            }
+            vbox.addEventListener('click',on_click, false);
+            hbox.addEventListener('click',on_click, false);
+            caption.addEventListener('click',on_click, false);
+            caption.addEventListener('keypress',function(ev) {
+                if (ev.keyCode == 13 /* enter */ || ev.keyCode == 77 /* mac enter */) on_click();
+            }, false);
+            caption.setAttribute('style','-moz-user-focus: normal');
+            caption.setAttribute('onfocus','this.setAttribute("class","outline_me")');
+            caption.setAttribute('onblur','this.setAttribute("class","")');
+
+        } catch(E) {
+            obj.error.sdump('D_ERROR',E + '\n');
+        }
+    },
+
+    /******************************************************************************************************/
+    /* save or store the updated fms as appropriate */
+
+    'editor_base_save' : function(update_method) {
+        var obj = this;
+        var fm_type_plural = obj.fm_type_plural;
+        var fm_type= obj.fm_type;
+
+        try {
+            if (obj.handle_update) {
+                try {
+                    //send fms to the update function
+                    var r = obj.network.request(
+                        'open-ils.serial',
+                        update_method,
+                        [ ses(), obj[fm_type_plural] ]
+                    );
+                    if (typeof r.ilsevent != 'undefined') {
+                        obj.error.standard_unexpected_error_alert('serial ' + fm_type + ' update',r);
+                    } else {
+                        alert($('serialStrings').getString('staff.serial.editor_base.handle_update.success'));
+                        obj.changed = {};
+                        if (obj.trigger_refresh) {
+                            obj.refresh_command();
+                        } else {
+                            obj.render();
+                        }
+                    }
+                    /* FIXME -- revisit the return value here */
+                } catch(E) {
+                    alert($('serialStrings').getString('staff.serial.editor_base.handle_update.error') + ' ' + js2JSON(E));
+                }
+            } else if (xul_param('in_modal',{'modal_xulG':true})) {
+                // TODO: this is to perhaps allow this editor to be called
+                // in a modal window, but is unfinished functionality
+                var xulG = {};
+                xulG[fm_type_plural] = obj[fm_type_plural];
+                update_modal_xulG(xulG);
+            } else {
+                obj.data['temp_' + fm_type_plural] = js2JSON( obj[fm_type_plural] );
+                obj.data.stash('temp_' + fm_type_plural);
+            }
+
+            if (xul_param('in_modal',{'modal_xulG':true})) {
+                window.close();
+            }
+        } catch(E) {
+            obj.error.standard_unexpected_error_alert(fm_type + '_editor save',E);
+        }
+    },
+
+    /******************************************************************************************************/
+    'editor_base_save_attributes' : function() {
+        var obj = this;
+        var fm_type = obj.fm_type;
+
+        JSAN.use('util.widgets'); JSAN.use('util.file'); var file = new util.file(fm_type + '_editor_prefs.'+obj.data.server_unadorned);
+        var what_to_save = {};
+        for (var i in obj[fm_type + '_editor_prefs']) {
+            what_to_save[i] = [];
+            for (var j in obj[fm_type + '_editor_prefs'][i]) what_to_save[i].push(j);
+        }
+        util.widgets.save_attributes(file, what_to_save );
+    }
+};
+
+dump('exiting serial/editor_base.js\n');
diff --git a/Open-ILS/xul/staff_client/server/serial/manage_items.js b/Open-ILS/xul/staff_client/server/serial/manage_items.js
new file mode 100644 (file)
index 0000000..7e217ac
--- /dev/null
@@ -0,0 +1,988 @@
+dump('entering manage_items.js\n');
+
+function $(id) { return document.getElementById(id); }
+
+if (typeof serial == 'undefined') serial = {};
+serial.manage_items = function (params) {
+
+       JSAN.use('util.error'); this.error = new util.error();
+       JSAN.use('util.network'); this.network = new util.network();
+       JSAN.use('OpenILS.data'); this.data = new OpenILS.data(); this.data.init({'via':'stash'});
+
+    this.current_sunit_id = -1; //default to **AUTO**
+    this.mode = 'receive';
+
+}
+
+serial.manage_items.prototype = {
+
+       'list_sitem_map' : {},
+
+    'set_sdist_ids' : function () {
+               var obj = this;
+
+        try {
+            obj.holding_lib = $('serial_item_lib_menu').value;
+            var robj = obj.network.request(
+                'open-ils.pcrud',
+                'open-ils.pcrud.id_list.sdist',
+                [ ses(), {"holding_lib" : obj.holding_lib, "+ssub":{"record_entry" : obj.docid}}, {"join":"ssub"} ]
+            );
+            if (robj != null) {
+                if (typeof robj.ilsevent != 'undefined') throw(robj);
+                obj.sdist_ids = robj.length ? robj : [robj];
+            } else {
+                obj.sdist_ids = [];
+            }
+        } catch(E) {
+            obj.error.standard_unexpected_error_alert('set_sdist_ids failed!',E);
+        }
+    },
+
+    'build_menus' : function () {
+               var obj = this;
+
+        // draw library drop-down
+        obj.org_ids = obj.network.simple_request('FM_SSUB_AOU_IDS_RETRIEVE_VIA_RECORD_ID.authoritative',[ obj.docid ]);
+        if (typeof obj.org_ids.ilsevent != 'undefined') throw(obj.org_ids);
+        JSAN.use('util.functional');
+        obj.org_ids = util.functional.map_list( obj.org_ids, function (o) { return Number(o); });
+
+        var org = obj.data.hash.aou[ obj.data.list.au[0].ws_ou() ];
+
+        JSAN.use('util.file'); JSAN.use('util.widgets');
+
+        var file; var list_data; var ml;
+
+        file = new util.file('offline_ou_list');
+        if (file._file.exists()) {
+            list_data = file.get_object(); file.close();
+            ml = util.widgets.make_menulist( list_data[0], list_data[1] );
+            ml.setAttribute('id','serial_item_lib_menu'); document.getElementById('serial_item_lib_menu_box').appendChild(ml);
+            //TODO: class this menu properly
+            for (var i = 0; i < obj.org_ids.length; i++) {
+                ml.getElementsByAttribute('value',obj.org_ids[i])[0].setAttribute('class','has_distributions');
+            }
+            /*TODO: add/enable this legend?
+            ml.firstChild.addEventListener(
+                'popupshown',
+                function(ev) {
+                    document.getElementById('legend').setAttribute('hidden','false');
+                },
+                false
+            );
+            ml.firstChild.addEventListener(
+                'popuphidden',
+                function(ev) {
+                    document.getElementById('legend').setAttribute('hidden','true');
+                },
+                false
+            );*/
+            ml.addEventListener(
+                'command',
+                function(ev) {
+                    //if (document.getElementById('serial_item_refresh_button')) document.getElementById('serial_item_refresh_button').focus();
+                    obj.save_settings();
+                    // get latest sdist id list based on library drowdown
+                    obj.set_sdist_ids();
+                    obj.refresh_list('main');
+                    obj.refresh_list('workarea');
+                },
+                false
+            );
+
+        } else {
+            throw(document.getElementById('catStrings').getString('staff.cat.copy_browser.missing_library') + '\n');
+        }
+        file = new util.file('serial_items_prefs.'+obj.data.server_unadorned);
+        util.widgets.load_attributes(file);
+        ml.value = ml.getAttribute('value');
+        if (! ml.value) {
+            ml.value = org.id();
+            ml.setAttribute('value',ml.value);
+        }
+        
+        // deal with mode radio selectedIndex, as load_attributes is setting a "read-only" value
+        if ($('mode_receive').getAttribute('selected')) {
+            $('serial_manage_items_mode').selectedIndex = 0;
+        } else {
+            $('serial_manage_items_mode').selectedIndex = 1;
+        }
+
+        // setup recent sunits list
+        var recent_sunits_file = new util.file('serial_items_recent_sunits_'+obj.docid+'.'+obj.data.server_unadorned);
+        util.widgets.load_attributes(recent_sunits_file);
+        var recent_sunits_popup = $('serial_items_recent_sunits');
+        obj.sunit_entries = JSON2js(recent_sunits_popup.getAttribute('sunit_json'));
+        for (i = 0; i < obj.sunit_entries.length; i++) {
+            var sunit_info = obj.sunit_entries[i];
+            var new_menu_item = recent_sunits_popup.appendItem(sunit_info.label);
+            new_menu_item.setAttribute('id', 'serial_items_recent_sunits_entry_'+sunit_info.id);
+            new_menu_item.setAttribute('sunit_id', sunit_info.id);
+            new_menu_item.setAttribute('command', 'cmd_set_sunit');
+        }
+    },
+
+       'init' : function( params ) {
+               var obj = this;
+
+               obj.docid = params['docid'];
+
+        obj.build_menus();
+        obj.set_sunit($('serial_items_current_sunit').getAttribute('sunit_id'), $('serial_items_current_sunit').getAttribute('sunit_label'), $('serial_items_current_sunit').getAttribute('sdist_id'), $('serial_items_current_sunit').getAttribute('sstr_id'));
+        obj.set_sdist_ids();
+               obj.init_lists();
+
+        var mode_radio_group = $('serial_manage_items_mode');
+        obj.set_mode(mode_radio_group.selectedItem.id.substr(5));
+        mode_radio_group.addEventListener(
+            'command',
+            function(ev) {
+                obj.save_settings();
+                var mode = ev.target.id.substr(5); //strip out 'mode_'
+                obj.set_mode(mode);
+                obj.refresh_list('main');
+                obj.refresh_list('workarea');
+            },
+            false
+        );
+        $('serial_manage_items_show_all').addEventListener(
+            'command',
+            function(ev) {
+                obj.save_settings();
+                obj.set_mode();
+                obj.refresh_list('main');
+                obj.refresh_list('workarea');
+            },
+            false
+        );
+
+               JSAN.use('util.controller'); obj.controller = new util.controller();
+               obj.controller.init(
+                       {
+                               'control_map' : {
+                                       'save_columns' : [ [ 'command' ], function() { obj.lists.main.save_columns(); } ],
+                                       'cmd_broken' : [ ['command'], function() { alert('Not Yet Implemented'); } ],
+                                       'sel_clip' : [ ['command'], function() { obj.lists.main.clipboard(); } ],
+                    'cmd_add_item' : [
+                        ['command'],
+                        function() {
+                            try {
+                                var new_item = new sitem();
+                                new_item.issuance(new siss());
+                                new_item.stream(1); //FIXME: hard-coded stream
+                                new_item.issuance().subscription(1); //FIXME: hard-coded subscription
+                                new_item.isnew(1);
+                                new_item.issuance().isnew(1);
+                                spawn_sitem_editor( {'sitems' : [new_item], 'do_edit' : 1 } );
+
+                                obj.refresh_list('main');
+
+                            } catch(E) {
+                                obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.edit_items.error'),E);
+                            }
+                        }
+                    ],
+                    'cmd_edit_items' : [
+                        ['command'],
+                        function() {
+                            try {
+                                if (!obj.retrieve_ids || obj.retrieve_ids.length == 0) return;
+
+                                JSAN.use('util.functional');
+                                var list = util.functional.map_list(
+                                        obj.retrieve_ids,
+                                        function (o) {
+                                            return o.sitem_id;
+                                        }
+                                    );
+
+                                spawn_sitem_editor( { 'sitem_ids' : list, 'do_edit' : 1 } );
+
+                                obj.refresh_list(obj.selected_list);
+
+                            } catch(E) {
+                                obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.edit_items.error'),E);
+                            }
+                        }
+                    ],
+                    'cmd_delete_items' : [
+                        ['command'],
+                        function() {
+                            try {
+                                JSAN.use('util.functional');
+                                var list = util.functional.map_list(
+                                        obj.retrieve_ids,
+                                        function (o) {
+                                            return obj.list_sitem_map[o.sitem_id];
+                                        }
+                                    );
+                                var delete_msg;
+                                if (list.length != 1) {
+                                    delete_msg = document.getElementById('catStrings').getFormattedString('staff.cat.copy_browser.delete_items.confirm.plural', [list.length]);
+                                } else {
+                                    delete_msg = document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_items.confirm');
+                                }
+                                var r = obj.error.yns_alert(
+                                        delete_msg,
+                                        document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_items.title'),
+                                        document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_items.delete'),
+                                        document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_items.cancel'),
+                                        null,
+                                        document.getElementById('commonStrings').getString('common.confirm')
+                                );
+
+                                if (r == 0) {
+                                    for (var i = 0; i < list.length; i++) {
+                                        list[i].isdeleted('1');
+                                    }
+                                    var robj = obj.network.request(
+                                            'open-ils.serial',
+                                            'open-ils.serial.item.fleshed.batch.update',
+                                        [ ses(), list, true ],
+                                        null,
+                                        {
+                                            'title' : document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_items.override'),
+                                            'overridable_events' : [ // FIXME: replace or delete these events
+                                                1208 /* TITLE_LAST_COPY */,
+                                                1227 /* COPY_DELETE_WARNING */,
+                                            ]
+                                        }
+                                    );
+                                    if (robj == null) throw(robj);
+                                    if (typeof robj.ilsevent != 'undefined') {
+                                        if ( (robj.ilsevent != 0) && (robj.ilsevent != 1227 /* COPY_DELETE_WARNING */) && (robj.ilsevent != 1208 /* TITLE_LAST_COPY */) ) throw(robj);
+                                    }
+                                    obj.refresh_list(obj.selected_list);
+                                }
+
+
+                            } catch(E) {
+                                obj.error.standard_unexpected_error_alert('staff.serial.manage_items.delete_items.error',E);
+                                obj.refresh_list();
+                            }
+                        }
+                    ],
+                    'cmd_set_sunit' : [
+                        ['command'],
+                        function(evt) {
+                            try {
+                                var target = evt.explicitOriginalTarget;
+                                var label = target.label;
+                                var sunit_id = target.getAttribute('sunit_id');
+                                var sdist_id = target.getAttribute('sdist_id');
+                                var sstr_id = target.getAttribute('sstr_id');
+                                obj.set_sunit(sunit_id, label, sdist_id, sstr_id);
+                                obj.save_sunit(sunit_id, label, sdist_id, sstr_id);
+                                if (obj.mode == 'bind') obj.refresh_list('workarea');
+                            } catch(E) {
+                                obj.error.standard_unexpected_error_alert('cmd_set_sunit failed!',E);
+                            }
+                        }
+                    ],
+                    'cmd_set_other_sunit' : [
+                        ['command'],
+                        function() {
+                            obj.set_other_sunit();
+                            if (obj.mode == 'bind') obj.refresh_list('workarea');
+                        }
+                    ],
+                    'cmd_predict_items' : [
+                        ['command'],
+                        function() {
+                            alert('Subscription selection needed here'); //FIXME: make this prompt, or discard this feature
+                        }
+                    ],
+                    'cmd_receive_items' : [
+                        ['command'],
+                        function() {
+                            try {
+                                JSAN.use('util.functional');
+                                var list = util.functional.map_list(
+                                        obj.retrieve_ids,
+                                        function (o) {
+                                            var item = obj.list_sitem_map[o.sitem_id];
+                                            item.unit(obj.current_sunit_id);
+                                            return item;
+                                        }
+                                    );
+
+                                var method; var success_label;
+                                if (obj.mode == 'receive') {
+                                    method = 'open-ils.serial.receive_items';
+                                    success_label = 'received';
+                                } else { // bind mode
+                                    method = 'open-ils.serial.bind_items';
+                                    success_label = 'bound';
+                                } 
+
+                                // deal with barcodes for *NEW* units
+                                var barcodes = {};
+                                if (obj.current_sunit_id < 0) { // **AUTO** or **NEW** units
+                                    new_unit_barcode = '';
+                                    for (var i = 0; i < list.length; i++) {
+                                        var item = list[i];
+                                        if (new_unit_barcode) {
+                                            barcodes[item.id()] = new_unit_barcode;
+                                            continue;
+                                        }
+                                        var prompt_text;
+                                        if (obj.current_sunit_id == -1) {
+                                            prompt_text = 'Please enter a barcode for '+item.issuance().label()+ ' from Distribution: '+item.stream().distribution().label()+'/'+item.stream().id()+':';
+                                        } else { // must be -2
+                                            prompt_text = 'Please enter a barcode for new unit:';
+                                        }
+                                        var barcode = window.prompt(prompt_text,
+                                            '',
+                                            'Unit Barcode Prompt');
+                                        barcode = String( barcode ).replace(/\s/g,'');
+                                        /* Casting a possibly null input value to a String turns it into "null" */
+                                        if (!barcode || barcode == 'null') {
+                                            alert('Invalid barcode entered, defaulting to system-generated.');
+                                            barcode = 'auto';
+                                        }
+
+                                        var test = obj.network.simple_request('FM_ACP_RETRIEVE_VIA_BARCODE',[ barcode ]);
+                                        if (typeof test.ilsevent == 'undefined') {
+                                            alert('Another copy has barcode "' + barcode + '", defaulting to system-generated.');
+                                            barcode = 'auto';
+                                        }
+                                        barcodes[item.id()] = barcode;
+                                        if (obj.current_sunit_id == -2) {
+                                            new_unit_barcode = barcode;
+                                        }
+                                    }
+                                }
+
+                                var robj = obj.network.request(
+                                            'open-ils.serial',
+                                            method,
+                                            [ ses(), list, barcodes ]
+                                        );
+                                if (typeof robj.ilsevent != 'undefined') throw(robj); //TODO: catch for override
+
+                                alert('Successfully '+success_label+' '+robj.num_items+' item(s)');
+
+                                if (obj.current_sunit_id == -2) {
+                                    obj.current_sunit_id = robj.new_unit_id;
+                                }
+
+                                obj.rebuild_current_sunit(list[0].stream().distribution().label(), list[0].stream().distribution().id(), list[0].stream().id());
+                                obj.refresh_list('main');
+                                obj.refresh_list('workarea');
+                                
+                            } catch(E) {
+                                obj.error.standard_unexpected_error_alert('cmd_receive_items failed!',E);
+                            }
+                        }
+                    ],
+                    'cmd_edit_sunit' : [
+                        ['command'],
+                        function() {
+                            try {
+                                /*if (!obj.retrieve_ids || obj.retrieve_ids.length == 0) return;
+
+                                JSAN.use('util.functional');
+                                var list = util.functional.map_list(
+                                        obj.retrieve_ids,
+                                        function (o) {
+                                            return o.sitem_id;
+                                        }
+                                    );
+*/
+                                spawn_sunit_editor( { 'sunit_ids' : [1], 'edit' : 1 } ); //FIXME: hard-coded sunit
+
+                            } catch(E) {
+                                obj.error.standard_unexpected_error_alert('cmd_edit_sunit failed!',E);
+                            }
+                        }
+                    ],
+
+                    'cmd_items_print' : [ ['command'], function() { obj.items_print(obj.selected_list); } ],
+                                       'cmd_items_export' : [ ['command'], function() { obj.items_export(obj.selected_list); } ],
+                                       'cmd_refresh_list' : [ ['command'], function() { obj.set_sdist_ids(); obj.refresh_list('main'); obj.refresh_list('workarea'); } ]
+                               }
+                       }
+               );
+        
+               obj.retrieve('main'); // retrieve main list
+        obj.retrieve('workarea'); // retrieve shelving unit list
+
+               obj.controller.view.sel_clip.setAttribute('disabled','true');
+
+       },
+
+       'items_print' : function(which) {
+               var obj = this;
+               try {
+                       var list = obj.lists[which];
+/* FIXME: serial items print template?                 JSAN.use('patron.util');
+                       var params = { 
+                               'patron' : patron.util.retrieve_fleshed_au_via_id(ses(),obj.patron_id), 
+                               'template' : 'items_out'
+                       }; */
+                       list.print( params );
+               } catch(E) {
+                       obj.error.standard_unexpected_error_alert('manage_items printing',E);
+               }
+       },
+
+       'items_export' : function(which) {
+               var obj = this;
+               try {
+                       var list = obj.lists[which];
+                       list.dump_csv_to_clipboard();
+               } catch(E) {
+                       obj.error.standard_unexpected_error_alert('manage_items export',E);
+               }
+       },
+
+       'rebuild_current_sunit' : function(sdist_label, sdist_id, sstr_id) {
+               var obj = this;
+               try {
+            var robj = obj.network.request(
+                'open-ils.pcrud',
+                'open-ils.pcrud.retrieve.sunit',
+                [ ses(),  obj.current_sunit_id]
+            );
+            if (!robj) return; // current sunit is NEW or AUTO
+
+            var label = '[' + sdist_label + '/' + sstr_id + ' #' + obj.current_sunit_id + '] ' + robj.summary_contents();
+            obj.set_sunit(obj.current_sunit_id, label, sdist_id, sstr_id);
+            obj.save_sunit(obj.current_sunit_id, label, sdist_id, sstr_id);
+               } catch(E) {
+                       obj.error.standard_unexpected_error_alert('serial items set_sunit',E);
+               }
+       },
+
+       'set_sunit' : function(sunit_id, label, sdist_id, sstr_id) {
+               var obj = this;
+               try {
+            obj.current_sunit_id = sunit_id;
+            obj.current_sunit_sdist_id = sdist_id;
+            obj.current_sunit_sstr_id = sstr_id;
+            if (sunit_id < 0) {
+                $('serial_workarea_sunit_desc').firstChild.nodeValue = '**' + label + '**';
+            } else {
+                $('serial_workarea_sunit_desc').firstChild.nodeValue = label;
+                obj.add_sunit_to_menu(sunit_id, label, sdist_id, sstr_id);
+            }
+               } catch(E) {
+                       obj.error.standard_unexpected_error_alert('serial items set_sunit',E);
+               }
+       },
+
+       'save_sunit' : function(sunit_id, label, sdist_id, sstr_id) {
+               var obj = this;
+               try {
+            $('serial_items_current_sunit').setAttribute('sunit_id', sunit_id);
+            $('serial_items_current_sunit').setAttribute('sunit_label', label);
+            if (sunit_id > 0) {
+                $('serial_items_current_sunit').setAttribute('sdist_id', sdist_id);
+                $('serial_items_current_sunit').setAttribute('sstr_id', sstr_id);
+            }
+            var recent_sunits_file = new util.file('serial_items_recent_sunits_'+obj.docid+'.'+obj.data.server_unadorned);
+            util.widgets.save_attributes(recent_sunits_file, { 'serial_items_recent_sunits' : [ 'sunit_json' ], 'serial_items_current_sunit' : [ 'sunit_id', 'sunit_label', 'sdist_id', 'sstr_id' ] });
+               } catch(E) {
+                       obj.error.standard_unexpected_error_alert('serial items save_sunit',E);
+               }
+       },
+
+       'set_other_sunit' : function() {
+               var obj = this;
+               try {
+            g.serial_items_sunit_select = '';
+            g.serial_items_sdist_ids = obj.sdist_ids;
+            JSAN.use('util.window'); var win = new util.window();
+            win.open(
+                xulG.url_prefix(urls.XUL_SERIAL_SELECT_UNIT),
+                'sel_serial_sunit_win_' + win.window_name_increment(),
+                'chrome,resizable,modal,centerscreen'
+            );
+            if (!g.serial_items_sunit_select) {
+                return;
+            }
+
+            var selection = g.serial_items_sunit_select;
+            var sunit_id = selection.sunit;
+            var sdist_id = selection.sdist;
+            var sstr_id = selection.sstr;
+            var label = selection.label;
+
+            obj.set_sunit(sunit_id, label, sdist_id, sstr_id);
+            obj.save_sunit(sunit_id, label, sdist_id, sstr_id);
+               } catch(E) {
+                       obj.error.standard_unexpected_error_alert('serial items set_other_sunit',E);
+               }
+       },
+
+       'add_sunit_to_menu' : function(sunit_id, label, sdist_id, sstr_id) {
+               var obj = this;
+               try {
+            if (sunit_id > 0) {
+                // check if it is already in sunit_entries, remove it
+                for (i = 0; i < obj.sunit_entries.length; i++) {
+                    if (obj.sunit_entries[i].id == sunit_id) {
+                        obj.sunit_entries.splice(i,1);
+                        var menu_item = $('serial_items_recent_sunits_entry_'+sunit_id);
+                        menu_item.parentNode.removeChild(menu_item);
+                        i--;
+                    }
+                }
+                // add to front of array
+                obj.sunit_entries.unshift({"id" : sunit_id, "label" : label, "sdist_id" : sdist_id, "sstr_id" : sstr_id});
+                var recent_sunits_popup = $('serial_items_recent_sunits');
+                var new_menu_item = recent_sunits_popup.insertItemAt(0,label);
+                new_menu_item.setAttribute('id', 'serial_items_recent_sunits_entry_'+sunit_id);
+                new_menu_item.setAttribute('sunit_id', sunit_id);
+                new_menu_item.setAttribute('sdist_id', sdist_id);
+                new_menu_item.setAttribute('sstr_id', sstr_id);
+                new_menu_item.setAttribute('command', 'cmd_set_sunit');
+
+                // pop off from sunit_entries if it already has 10 sunits
+                if (obj.sunit_entries.length > 10) {
+                    var sunit_info = obj.sunit_entries.pop();
+                    var menu_item = $('serial_items_recent_sunits_entry_'+sunit_info.id);
+                    menu_item.parentNode.removeChild(menu_item);
+                }
+
+                recent_sunits_popup.setAttribute('sunit_json', js2JSON(obj.sunit_entries));
+            }
+               } catch(E) {
+                       obj.error.standard_unexpected_error_alert('serial items add_sunit_to_menu',E);
+               }
+       },
+
+       'set_mode' : function(mode) {
+               var obj = this;
+
+        if (!mode) {
+            mode = obj.mode;
+        } else {
+            obj.mode = mode;
+        }
+
+        if (mode == 'receive') {
+            $('serial_workarea_mode_label').value = 'Recently Received';
+            if ($('serial_manage_items_show_all').checked) {
+                obj.lists.main.sitem_retrieve_params = {};
+            } else {
+                obj.lists.main.sitem_retrieve_params = {'date_received' : null };
+            }
+            obj.lists.main.sitem_extra_params ={'order_by' : {'sitem' : 'date_expected ASC, stream ASC'}};
+
+            obj.lists.workarea.sitem_retrieve_params = {'date_received' : {"!=" : null}};
+            obj.lists.workarea.sitem_extra_params ={'order_by' : {'sitem' : 'date_received DESC'}, 'limit' : 30};
+        } else { // bind mode
+            $('serial_workarea_mode_label').value = 'Bound Items in Current Working Unit';
+            if ($('serial_manage_items_show_all').checked) {
+                obj.lists.main.sitem_retrieve_params = {};
+            } else {
+                obj.lists.main.sitem_retrieve_params = {'date_received' : {'!=' : null}, 'status' : {'!=' : 'Bindery'} };
+            }
+            obj.lists.main.sitem_extra_params ={'order_by' : {'sitem' : 'date_expected ASC, stream ASC'}};
+
+            obj.lists.workarea.sitem_retrieve_params = {'status' : 'Bindery'}; // unit set dynamically in 'retrieve'
+            obj.lists.workarea.sitem_extra_params ={'order_by' : {'sitem' : 'date_received DESC'}};
+
+            // default to **NEW UNIT**
+            obj.set_sunit(-2, 'New Unit', '', '');
+        }
+    },
+
+       'save_settings' : function() {
+               var obj = this;
+
+        JSAN.use('util.file'); var file = new util.file('serial_items_prefs.'+obj.data.server_unadorned);
+        util.widgets.save_attributes(file, { 'serial_item_lib_menu' : [ 'value' ], 'mode_receive' : [ 'selected' ], 'mode_bind' : [ 'selected' ], 'serial_manage_items_show_all' : [ 'checked' ] });
+    },
+
+       'init_lists' : function() {
+               var obj = this;
+
+               JSAN.use('circ.util');
+        var columns = item_columns({});
+
+        function retrieve_row(params) {
+                       try { 
+                               var row = params.row;
+                obj.network.simple_request(
+                    'FM_SITEM_FLESHED_BATCH_RETRIEVE.authoritative',
+                    [[row.my.sitem_id]],
+                    //[ ses(), row.my.sitem_id, {"flesh":2,"flesh_fields":{"sitem":["creator","editor","issuance","stream","unit","notes"], "sunit":["call_number"], "sstr":["distribution"]}}],
+                    function(req) {
+                        try {
+                            var robj = req.getResultObject();
+                            if (typeof robj.ilsevent != 'undefined') throw(robj);
+                            if (typeof robj.ilsevent == 'null') throw('null result');
+                            var sitem = robj[0];
+                            obj.list_sitem_map[sitem.id()] = sitem;
+                            row.my.sitem = sitem;
+                            //params.row_node.setAttribute( 'retrieve_id', js2JSON({'copy_id':copy_id,'circ_id':row.my.circ.id(),'barcode':row.my.acp.barcode(),'doc_id': ( row.my.record ? row.my.record.id() : null ) }) );
+                            params.row_node.setAttribute( 'retrieve_id', js2JSON({'sitem_id':sitem.id()}) );
+                            dump('dumping... ' + js2JSON(obj.list_sitem_map[sitem.id()]));
+                            if (typeof params.on_retrieve == 'function') {
+                                params.on_retrieve(row);
+                            }
+
+                        } catch(E) {
+                            obj.error.standard_unexpected_error_alert('staff.serial.manage_items.retrieve_row.callback_error', E);
+                        }
+                    }
+                );
+                               return row;
+                       } catch(E) {
+                               obj.error.standard_unexpected_error_alert('staff.serial.manage_items.retrieve_row.error_in_retrieve_row',E);
+                               return params.row;
+                       }
+               }
+
+               JSAN.use('util.list');
+
+        obj.lists = {};
+        obj.lists.main = new util.list('item_tree');
+               obj.lists.main.init(
+                       {
+                               'columns' : columns,
+                               'map_row_to_columns' : circ.util.std_map_row_to_columns(),
+                               'retrieve_row' : retrieve_row,
+                               'on_select' : function(ev) {
+                    obj.selected_list = 'main';
+                                       JSAN.use('util.functional');
+                                       var sel = obj.lists.main.retrieve_selection();
+                                       obj.controller.view.sel_clip.setAttribute('disabled',sel.length < 1);
+                                       var list = util.functional.map_list(
+                                               sel,
+                                               function(o) { return JSON2js( o.getAttribute('retrieve_id') ); }
+                                       );
+                                       if (typeof obj.on_select == 'function') {
+                                               obj.on_select(list);
+                                       }
+                                       if (typeof window.xulG == 'object' && typeof window.xulG.on_select == 'function') {
+                                               obj.error.sdump('D_CAT','manage_items: Calling external .on_select()\n');
+                                               window.xulG.on_select(list);
+                                       }
+                               }
+                       }
+               );
+
+        obj.lists.workarea = new util.list('workarea_tree');
+               obj.lists.workarea.init(
+                       {
+                               'columns' : columns,
+                               'map_row_to_columns' : circ.util.std_map_row_to_columns(),
+                               'retrieve_row' : retrieve_row,
+                               'on_select' : function(ev) {
+                    obj.selected_list = 'workarea';
+                                       JSAN.use('util.functional');
+                                       var sel = obj.lists.workarea.retrieve_selection();
+                                       obj.controller.view.sel_clip.setAttribute('disabled',sel.length < 1);
+                                       var list = util.functional.map_list(
+                                               sel,
+                                               function(o) { return JSON2js( o.getAttribute('retrieve_id') ); }
+                                       );
+                                       if (typeof obj.on_select == 'function') {
+                                               obj.on_select(list);
+                                       }
+                                       if (typeof window.xulG == 'object' && typeof window.xulG.on_select == 'function') {
+                                               obj.error.sdump('D_CAT','serctrl: Calling external .on_select()\n');
+                                               window.xulG.on_select(list);
+                                       } else {
+                                               obj.error.sdump('D_CAT','serctrl: No external .on_select()\n');
+                                       }
+                               }
+                       }
+               );
+    },
+
+       'refresh_list' : function(list_name) {
+        var obj = this;
+
+        //TODO Optimize this?
+        obj.retrieve(list_name);
+    },
+
+       'retrieve' : function(list_name) {
+               var obj = this;
+        var list = obj.lists[list_name];
+        
+               list.clear();
+
+        if (!obj.sdist_ids.length) { // no sdists to retrieve items for
+            return;
+        }
+
+        var rparams = list.sitem_retrieve_params;
+        var robj;
+        rparams['+sstr'] = { "distribution" : obj.sdist_ids };
+
+        if (obj.mode == 'bind' && list_name == 'workarea') {
+             rparams['unit'] = obj.current_sunit_id;
+        }
+
+        var other_params = list.sitem_extra_params;
+        other_params.join = 'sstr';
+
+        robj = obj.network.simple_request(
+            'FM_SITEM_ID_LIST',
+            [ ses(), rparams, other_params ]
+        );
+        if (!robj) {
+            robj = [];
+        } else if (typeof robj.ilsevent!='undefined') {
+            obj.error.standard_unexpected_error_alert('Failed to retrieve serial item ID list',E);
+        } else if (!robj.length) {
+            robj = [robj];
+        }
+
+        for (i = 0; i < robj.length; i++) {
+            list.append( { 'row' : { 'my' : { 'sitem_id' : robj[i] } }, 'to_bottom' : true, 'no_auto_select' : true } );
+        }
+       },
+
+       'on_select' : function(list) {
+
+               dump('manage_items.on_select list = ' + js2JSON(list) + '\n');
+
+               var obj = this;
+
+               /*obj.controller.view.cmd_items_claimed_returned.setAttribute('disabled','false');
+               obj.controller.view.sel_mark_items_missing.setAttribute('disabled','false');*/
+
+               obj.retrieve_ids = list;
+       }
+}
+
+function item_columns(modify,params) {
+
+    JSAN.use('OpenILS.data'); var data = new OpenILS.data(); data.init({'via':'stash'});
+    //JSAN.use('util.network'); var network = new util.network();
+
+    var c = [
+        {
+            'id' : 'sitem_id',
+            'label' : 'Item ID',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : false,
+            'render' : function(my) { return my.sitem.id(); },
+            'persist' : 'hidden width ordinal'
+        },
+        {
+            'id' : 'label',
+            'label' : 'Issuance Label',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : false,
+            'render' : function(my) { return my.sitem.issuance().label(); },
+            'persist' : 'hidden width ordinal'
+        },
+        {
+            'id' : 'distribution',
+            'label' : 'Distribution',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : false,
+            'persist' : 'hidden width ordinal',
+            'render' : function(my) { return my.sitem.stream().distribution().label(); }
+        },
+        {
+            'id' : 'stream_id',
+            'label' : 'Stream ID',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : false,
+            'persist' : 'hidden width ordinal',
+            'render' : function(my) { return my.sitem.stream().id(); }
+        },
+        {
+            'id' : 'date_published',
+            'label' : 'Date Published',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : false,
+            'render' : function(my) { return my.sitem.issuance().date_published().substr(0,10); },
+            'persist' : 'hidden width ordinal'
+        },
+        {
+            'id' : 'date_expected',
+            'label' : 'Date Expected',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : false,
+            'render' : function(my) { return my.sitem.date_expected().substr(0,10); },
+            'persist' : 'hidden width ordinal'
+        },
+        {
+            'id' : 'date_received',
+            'label' : 'Date Received',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : false,
+            'render' : function(my) { return my.sitem.date_received().substr(0,10); },
+            'persist' : 'hidden width ordinal'
+        },
+        {
+            'id' : 'notes',
+            'label' : 'Notes',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : false,
+            'render' : function(my) { return my.sitem.notes().length; },
+            'persist' : 'hidden width ordinal'
+        },
+        {
+            'id' : 'call_number',
+            'label' : 'Call Number',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : false,
+            'persist' : 'hidden width ordinal',
+            'render' : function(my) { return my.sitem.unit().call_number().label(); }
+        },
+        {
+            'id' : 'unit_id_contents',
+            'label' : 'Unit ID / Contents',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : false,
+            'render' : function(my) { return '[' + my.sitem.unit().id() + '] ' + my.sitem.unit().summary_contents() ; },
+            'persist' : 'hidden width ordinal'
+        },
+        {
+            'id' : 'creator',
+            'label' : 'Creator',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : true,
+            'persist' : 'hidden width ordinal',
+            'render' : function(my) { return my.sitem.creator().usrname(); }
+        },
+        {
+            'id' : 'create_date',
+            'label' : document.getElementById('circStrings').getString('staff.circ.utils.create_date'),
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : true,
+            'persist' : 'hidden width ordinal',
+            'render' : function(my) { return my.sitem.create_date().substr(0,10); }
+        },
+        {
+            'id' : 'editor',
+            'label' : 'Editor',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : true,
+            'persist' : 'hidden width ordinal',
+            'render' : function(my) { return my.sitem.editor().usrname(); }
+        },
+        {
+            'id' : 'edit_date',
+            'label' : document.getElementById('circStrings').getString('staff.circ.utils.edit_date'),
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : false,
+            'persist' : 'hidden width ordinal',
+            'render' : function(my) { return my.sitem.edit_date().substr(0,10); }
+        },
+        {
+            'id' : 'holding_code',
+            'label' : 'Holding Code',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : true,
+            'render' : function(my) { return my.sitem.issuance().holding_code(); },
+            'persist' : 'hidden width ordinal'
+        },
+        {
+            'id' : 'holding_type',
+            'label' : 'Holding Type',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : true,
+            'render' : function(my) { return my.sitem.issuance().holding_type(); },
+            'persist' : 'hidden width ordinal'
+        },
+        {
+            'id' : 'holding_link_id',
+            'label' : 'Holding Link ID',
+            'flex' : 1,
+            'primary' : false,
+            'hidden' : true,
+            'render' : function(my) { return my.sitem.issuance().holding_link_id(); },
+            'persist' : 'hidden width ordinal'
+        }
+    ];
+    for (var i = 0; i < c.length; i++) {
+        if (modify[ c[i].id ]) {
+            for (var j in modify[ c[i].id ]) {
+                c[i][j] = modify[ c[i].id ][j];
+            }
+        }
+    }
+    if (params) {
+        if (params.just_these) {
+            JSAN.use('util.functional');
+            var new_c = [];
+            for (var i = 0; i < params.just_these.length; i++) {
+                var x = util.functional.find_list(c,function(d){return(d.id==params.just_these[i]);});
+                new_c.push( function(y){ return y; }( x ) );
+            }
+            c = new_c;
+        }
+        if (params.except_these) {
+            JSAN.use('util.functional');
+            var new_c = [];
+            for (var i = 0; i < c.length; i++) {
+                var x = util.functional.find_list(params.except_these,function(d){return(d==c[i].id);});
+                if (!x) new_c.push(c[i]);
+            }
+            c = new_c;
+        }
+    }
+    //return c.sort( function(a,b) { if (a.label < b.label) return -1; if (a.label > b.label) return 1; return 0; } );
+    return c;
+};
+
+spawn_sitem_editor = function(params) {
+    try {
+        if (!params.sitem_ids && !params.sitems) return;
+        if (params.sitem_ids && params.sitem_ids.length == 0) return;
+        if (params.sitems && params.sitems.length == 0) return;
+        if (params.sitem_ids) params.sitem_ids = js2JSON(params.sitem_ids); // legacy
+        if (!params.caller_handles_update) params.handle_update = 1; // legacy
+
+        var obj = {};
+        JSAN.use('util.network'); obj.network = new util.network();
+        JSAN.use('util.error'); obj.error = new util.error();
+
+        var title = '';
+        if (params.sitem_ids && params.sitem_ids.length > 1 && params.do_edit == 1)
+            title = 'Batch Edit Items';
+        else /* if(params.sitems && params.sitems.length > 1 && params.do_edit == 1)
+            title = 'Batch View Items';
+        else if(params.sitem_ids && params.sitem_ids.length == 1) */
+            title = 'Edit Item';/*
+        else
+            title = 'View Item';*/
+
+        JSAN.use('util.window'); var win = new util.window();
+        params.in_modal = true;
+        var my_xulG = win.open(
+            (urls.XUL_SERIAL_ITEM_EDITOR),
+            title,
+            'chrome,modal,resizable',
+            params
+        );
+        if (my_xulG.sitems && params.do_edit) {
+            return my_xulG.sitems;
+        } else {
+            return [];
+        }
+    } catch(E) {
+        JSAN.use('util.error'); var error = new util.error();
+        error.standard_unexpected_error_alert('error in spawn_sitem_editor',E);
+    }
+}
+
+dump('exiting manage_items.js\n');
diff --git a/Open-ILS/xul/staff_client/server/serial/manage_items.xul b/Open-ILS/xul/staff_client/server/serial/manage_items.xul
new file mode 100644 (file)
index 0000000..c10c436
--- /dev/null
@@ -0,0 +1,112 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Manage Items Overlay -->
+<!--
+vim:noet:sw=4:ts=4:
+-->
+<!DOCTYPE overlay PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<overlay id="serial_manage_items_overlay" 
+       xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+    <script type="text/javascript" src="/xul/server/serial/manage_items.js"/>
+    <script>
+    <![CDATA[
+        function my_init() {
+            try {
+                netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+                if (typeof JSAN == 'undefined') { 
+                    throw( document.getElementById("commonStrings").getString('common.jsan.missing') );
+                }
+                JSAN.errorLevel = "die"; // none, warn, or die
+                JSAN.addRepository('/xul/server/');
+                JSAN.use('util.error'); g.error = new util.error();
+                g.error.sdump('D_TRACE','my_init() for manage_items.xul');
+
+                JSAN.use('serial.manage_items'); g.manage_items = new serial.manage_items();
+
+                //g.manage_items.init( { 'sre_id' : xul_param('sre_id'), 'sdist_id' : xul_param('sdist_id') } );
+                g.manage_items.init( { 'docid' : xul_param('docid') } );
+
+            } catch(E) {
+                var err_msg = document.getElementById("commonStrings").getFormattedString('common.exception', ['serial/manage_items.xul', E]);
+                try { g.error.sdump('D_ERROR',err_msg); } catch(E) { dump(err_msg); }
+                alert(err_msg);
+            }
+        }
+
+    ]]>
+    </script>
+    <popupset>
+        <popup id="serial_manage_items_popup">
+            <menuitem command="cmd_edit_items" label="Edit Item Attributes" accesskey="&staff.cat.copy_browser.actions.cmd_edit_items.accesskey;"/>
+            <menuitem command="cmd_delete_items" label="Delete Item" accesskey="&staff.cat.copy_browser.actions.cmd_delete_items.accesskey;"/>
+        </popup>
+    </popupset>
+    <tabpanel id="serial_manage_items" orient="vertical" flex="1">
+        <hbox align="center">
+            <hbox id="serial_item_lib_menu_box"/>
+            <label value="Mode:" control="mode_receive"/><radiogroup id="serial_manage_items_mode" orient="horizontal"><radio id="mode_receive" label="Receive"/><radio id="mode_bind" label="Bind"/></radiogroup><checkbox id="serial_manage_items_show_all" label="Show All" />
+            <button id="refresh_button" label="&staff.cat.copy_browser.holdings_maintenance.refresh_button.label;" command="cmd_refresh_list" />
+            <spacer flex="1"/>
+            <menubar>
+                <!--
+                <menu label="Actions for this Serial Control" accesskey="C">
+                    <menupopup>
+                        <menuitem command="cmd_predict_items" label="Predict Items"/>
+                        <menuitem command="cmd_add_item" label="Add Custom Item"/>
+                        <menuitem command="cmd_edit_mfhd" label="Edit MFHD Record"/>
+                    </menupopup>
+                </menu>
+                -->
+                <menu label="&staff.cat.copy_browser.holdings_maintenance.actions.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.actions.accesskey;">
+                    <menupopup>
+                        <menuitem command="cmd_edit_items" label="Edit Item Attributes" accesskey="&staff.cat.copy_browser.actions.cmd_edit_items.accesskey;"/>
+                        <menuitem command="cmd_delete_items" label="Delete Item" accesskey="&staff.cat.copy_browser.actions.cmd_delete_items.accesskey;"/>
+                        <menuseparator/>
+                        <menuitem command="cmd_refresh_list" label="&staff.cat.copy_browser.holdings_maintenance.cmd_refresh_list.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_refresh_list.accesskey;"/>
+                        <menuitem command="save_columns" label="&staff.cat.copy_browser.holdings_maintenance.save_columns.label;"/>
+                        <!-- <menuitem command="sel_clip" label="&staff.cat.copy_browser.holdings_maintenance.sel_clip.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.sel_clip.accesskey;"/>
+                        <menuitem command="cmd_transfer_items" label="&staff.cat.copy_browser.holdings_maintenance.cmd_transfer_items.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_transfer_items.accesskey;"/>
+                        <menuseparator/>
+                        <menuitem command="cmd_add_volumes" label="&staff.cat.copy_browser.holdings_maintenance.cmd_add_volumes.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_add_volumes.accesskey;"/>
+                        <menuitem command="cmd_edit_volumes" label="&staff.cat.copy_browser.holdings_maintenance.cmd_edit_volumes.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_edit_volumes.accesskey;"/>
+                        <menuitem command="cmd_mark_volume" label="&staff.cat.copy_browser.holdings_maintenance.cmd_mark_volume.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_mark_volume.accesskey;"/>
+                        <menuitem command="cmd_transfer_volume" label="&staff.cat.copy_browser.holdings_maintenance.cmd_transfer_volume.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_transfer_volume.accesskey;"/>
+                        <menuitem command="cmd_delete_volumes" label="&staff.cat.copy_browser.holdings_maintenance.cmd_delete_volumes.label;" accesskey=""/>
+                        <menuseparator/>
+                        <menuitem command="cmd_print_spine_labels" label="&staff.cat.copy_browser.holdings_maintenance.cmd_print_spine_labels.label;" accesskey="&staff.cat.copy_browser.holdings_maintenance.cmd_print_spine_labels.accesskey;"/>
+                        <menuitem command="cmd_replace_barcode" label="&staff.cat.copy_browser.holdings_maintenance.cmd_replace_barcode.label;" accesskey=""/> -->
+                    </menupopup>
+                </menu>
+            </menubar>
+        </hbox>
+        <tree id="item_tree" flex="2" enableColumnDrag="true" context="serial_manage_items_popup"/>
+        <splitter state="open" collapse="after" resizebefore="closest" resizeafter="farthest"/>
+        <hbox align="center">
+            <label style="font-weight: bold" value="Showing: "/>
+            <label id="serial_workarea_mode_label" value="Recently Received"/>
+            <spacer flex="1"/>
+            <button label="Receive/Move Selected &#8595;" command="cmd_receive_items"/>
+        </hbox>
+        <hbox align="center">
+            <label style="font-weight: bold" value="Current Working Unit: "/>
+            <description flex="1" id="serial_workarea_sunit_desc">**Auto per Item**</description>
+            <spacer flex="1"/>
+            <menubar>
+                <menu label="Set Current Unit" id="serial_items_current_sunit" sunit_id="-1" sunit_label="Auto per Item" sdist_id="" sstr_id="">
+                    <menupopup>
+                        <menuitem command="cmd_set_sunit" label="New Unit" sunit_id="-2" sdist_id="" sstr_id=""/>
+                        <menuitem command="cmd_set_sunit" label="Auto per Item" sunit_id="-1" sdist_id="" sstr_id=""/>
+                        <menu label="Recent" id="serial_items_recent_sunits" sunit_json='[]'/>
+                        <menuitem command="cmd_set_other_sunit" label="Other..."/>
+                    </menupopup>
+                </menu>
+                <button command="cmd_broken" label="Edit Current Unit..."/>
+            </menubar>
+        </hbox>
+        <tree id="workarea_tree" flex="1" enableColumnDrag="true" context="serial_manage_items_popup"/>
+    </tabpanel>
+
+</overlay>
diff --git a/Open-ILS/xul/staff_client/server/serial/manage_subs.js b/Open-ILS/xul/staff_client/server/serial/manage_subs.js
new file mode 100644 (file)
index 0000000..307cc49
--- /dev/null
@@ -0,0 +1,1741 @@
+dump('entering serial/manage_subs.js\n');
+// vim:noet:sw=4:ts=4:
+
+if (typeof serial == 'undefined') serial = {};
+serial.manage_subs = function (params) {
+    try {
+        netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+        JSAN.use('util.error'); this.error = new util.error();
+    } catch(E) {
+        dump('serial/manage_subs: ' + E + '\n');
+    }
+};
+
+serial.manage_subs.prototype = {
+
+    'map_tree' : {},
+    'map_ssub' : {},
+    'map_sdist' : {},
+    'map_siss' : {},
+    'map_scap' : {},
+    'sel_list' : [],
+    'funcs' : [],
+    'editor_indexes' : { 'ssub' : 1, 'sdist' : 2, 'siss' : 3, 'scap' : 4 },
+
+    'ids_from_sel_list' : function(type) {
+        var obj = this;
+        JSAN.use('util.functional');
+
+        var list = util.functional.map_list(
+            util.functional.filter_list(
+                obj.sel_list,
+                function (o) {
+                    return o.split(/_/)[0] == type;
+                }
+            ),
+            function (o) {
+                return o.split(/_/)[1];
+            }
+        );
+
+        return list;
+    },
+
+    'editor_init' : function(type, mode, params) {
+        var obj = this;
+        try {
+            $('serial_manage_subs_editor_deck').selectedIndex = obj.editor_indexes[type];
+            var editor_type = type + '_editor';
+            if (typeof obj[editor_type] == 'undefined') {
+                JSAN.use('serial.' + editor_type);
+                obj[editor_type] = new serial[editor_type](); 
+            }
+
+            params.do_edit = true;
+            params.handle_update = true;
+            if (mode == 'add') {
+                params.trigger_refresh = true;
+                params.refresh_command = function () {obj.refresh_list();};
+            }
+            obj[editor_type].init(params);
+        } catch(E) {
+            obj.error.standard_unexpected_error_alert('editor_init() error',E);
+        }
+    },
+
+    'do_delete' : function(type, method, overridable_events) {
+        var obj = this;
+        try {
+            JSAN.use('util.functional');
+
+            var list = util.functional.filter_list(
+                obj.sel_list,
+                function (o) {
+                    return o.split(/_/)[0] == type;
+                }
+            );
+
+            list = util.functional.map_list(
+                list,
+                function (o) {
+                    return JSON2js( js2JSON( obj['map_' + type][ type + '_' + o.split(/_/)[1] ] ) );
+                }
+            );
+
+            //TODO: proper messages
+            var delete_msg;
+            if (list.length != 1) {
+                delete_msg = document.getElementById('serialStrings').getFormattedString('staff.serial.manage_subs.delete_' + type + '.confirm.plural', [list.length]);
+            } else {
+                delete_msg = document.getElementById('serialStrings').getString('staff.serial.manage_subs.delete_' + type + '.confirm');
+            }
+            var r = obj.error.yns_alert(
+                    delete_msg,
+                    document.getElementById('serialStrings').getString('staff.serial.manage_subs.delete_' + type + '.title'),
+                    document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_items.delete'),
+                    document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_items.cancel'),
+                    null,
+                    document.getElementById('commonStrings').getString('common.confirm')
+            );
+
+            if (r == 0) {
+                for (var i = 0; i < list.length; i++) {
+                    list[i].isdeleted('1');
+                }
+                var robj = obj.network.request(
+                    'open-ils.serial', 
+                    method, 
+                    [ ses(), list, true ],
+                    null,
+                    {
+                        'title' : document.getElementById('serialStrings').getString('staff.serial.manage_subs.delete_' + type + '.override'),
+                        'overridable_events' : overridable_events
+                    }
+                );
+                if (robj == null) throw(robj);
+                if (typeof robj.ilsevent != 'undefined') {
+                    if (robj.ilsevent != 0) {
+                        var overridable = false;
+                        for (i = 0; i < overridable_events.length; i++) {
+                            if (overridable_events[i] == robj.ilsevent) {
+                                overridable = true;
+                                break;
+                            }
+                        }
+                        if (!overridable) throw(robj);
+                    }
+                }
+                obj.refresh_list();
+            }
+        } catch(E) {
+            obj.error.standard_unexpected_error_alert(document.getElementById('serialStrings').getString('staff.serial.manage_subs.delete.error'),E);
+            obj.refresh_list();
+        }
+    },
+
+    'init' : function( params ) {
+
+        try {
+            netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+            var obj = this;
+
+            obj.docid = params.docid;
+
+            JSAN.use('util.network'); obj.network = new util.network();
+            JSAN.use('OpenILS.data'); obj.data = new OpenILS.data(); obj.data.init({'via':'stash'});
+            JSAN.use('util.controller'); obj.controller = new util.controller();
+            obj.controller.init(
+                {
+                    control_map : {
+                        'save_columns' : [ [ 'command' ], function() { obj.list.save_columns(); } ],
+                        'sel_clip' : [
+                            ['command'],
+                            function() { obj.list.clipboard(); }
+                        ],
+                        'cmd_broken' : [
+                            ['command'],
+                            function() { 
+                                alert(document.getElementById('commonStrings').getString('common.unimplemented'));
+                            }
+                        ],
+                        'cmd_show_my_libs' : [
+                            ['command'],
+                            function() { 
+                                obj.show_my_libs(); 
+                            }
+                        ],
+                        'cmd_show_all_libs' : [
+                            ['command'],
+                            function() {
+                                obj.show_all_libs();
+                            }
+                        ],
+                        'cmd_show_libs_with_distributions' : [
+                            ['command'],
+                            function() {
+                                obj.show_libs_with_distributions();
+                            }
+                        ],
+                        'cmd_clear' : [
+                            ['command'],
+                            function() {
+                                obj.map_tree = {};
+                                obj.list.clear();
+                            }
+                        ],
+                        'cmd_add_scap' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    var list = obj.ids_from_sel_list('ssub');
+                                    if (list.length == 0) list = obj.ids_from_sel_list('scap-group');
+                                    if (list.length == 0) return;
+
+                                    /*TODO: permission check?
+                                    //populate 'list' with owning_libs of subs, TODO
+                                    var edit = 0;
+                                    try {
+                                        edit = obj.network.request(
+                                            api.PERM_MULTI_ORG_CHECK.app,
+                                            api.PERM_MULTI_ORG_CHECK.method,
+                                            [ 
+                                                ses(), 
+                                                obj.data.list.au[0].id(), 
+                                                list,
+                                                [ 'CREATE_COPY' ]
+                                            ]
+                                        ).length == 0 ? 1 : 0;
+                                    } catch(E) {
+                                        obj.error.sdump('D_ERROR','batch permission check: ' + E);
+                                    }
+
+                                    if (edit==0) return; // no read-only view for this interface */
+                                    var new_scap = new scap();
+                                    new_scap.subscription(list[0]);//TODO: add multiple at once support?
+                                    new_scap.isnew(1);
+                                    var params = {};
+                                    params.scaps = [new_scap];
+                                    obj.editor_init('scap', 'add', params);
+                                } catch(E) {
+                                    obj.error.standard_unexpected_error_alert(document.getElementById('serialStrings').getString('staff.serial.manage_subs.add.error'),E);
+                                }
+                            }
+                        ],
+                        'cmd_add_siss' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    var list = obj.ids_from_sel_list('ssub');
+                                    if (list.length == 0) list = obj.ids_from_sel_list('siss-group');
+                                    if (list.length == 0) return;
+
+                                    /*TODO: permission check?
+                                    //populate 'list' with owning_libs of subs, TODO
+                                    var edit = 0;
+                                    try {
+                                        edit = obj.network.request(
+                                            api.PERM_MULTI_ORG_CHECK.app,
+                                            api.PERM_MULTI_ORG_CHECK.method,
+                                            [ 
+                                                ses(), 
+                                                obj.data.list.au[0].id(), 
+                                                list,
+                                                [ 'CREATE_COPY' ]
+                                            ]
+                                        ).length == 0 ? 1 : 0;
+                                    } catch(E) {
+                                        obj.error.sdump('D_ERROR','batch permission check: ' + E);
+                                    }
+
+                                    if (edit==0) return; // no read-only view for this interface */
+                                    var new_siss = new siss();
+                                    new_siss.subscription(list[0]);//TODO: add multiple at once support?
+                                    new_siss.isnew(1);
+                                    var params = {};
+                                    params.sisses = [new_siss];
+                                    obj.editor_init('siss', 'add', params);
+                                } catch(E) {
+                                    obj.error.standard_unexpected_error_alert(document.getElementById('serialStrings').getString('staff.serial.manage_subs.add.error'),E);
+                                }
+                            }
+                        ],
+                        'cmd_add_sdist' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    var list = obj.ids_from_sel_list('ssub');
+                                    if (list.length == 0) list = obj.ids_from_sel_list('sdist-group');
+                                    if (list.length == 0) return;
+
+                                    /*TODO: permission check?
+                                    //populate 'list' with owning_libs of subs, TODO
+                                    var edit = 0;
+                                    try {
+                                        edit = obj.network.request(
+                                            api.PERM_MULTI_ORG_CHECK.app,
+                                            api.PERM_MULTI_ORG_CHECK.method,
+                                            [ 
+                                                ses(), 
+                                                obj.data.list.au[0].id(), 
+                                                list,
+                                                [ 'CREATE_COPY' ]
+                                            ]
+                                        ).length == 0 ? 1 : 0;
+                                    } catch(E) {
+                                        obj.error.sdump('D_ERROR','batch permission check: ' + E);
+                                    }
+
+                                    if (edit==0) return; // no read-only view for this interface */
+                                    var new_sdist = new sdist();
+                                    new_sdist.subscription(list[0]);//TODO: add multiple at once support?
+                                    new_sdist.holding_lib(obj.map_ssub['ssub_' + list[0]].owning_lib());//default to sub owning lib
+                                    new_sdist.label('Default');
+                                    new_sdist.isnew(1);
+                                    var params = {};
+                                    params.sdists = [new_sdist];
+                                    obj.editor_init('sdist', 'add', params);
+                                } catch(E) {
+                                    obj.error.standard_unexpected_error_alert(document.getElementById('serialStrings').getString('staff.serial.manage_subs.add.error'),E);
+                                }
+                            }
+                        ],
+                        'cmd_delete_scap' : [
+                            ['command'],
+                            function() {
+                                var overridable_events = [
+                                    11001 // SERIAL_CAPTION_AND_PATTERN_HAS_ISSUANCES
+                                ];
+                                obj.do_delete('scap', 'open-ils.serial.caption_and_pattern.batch.update', overridable_events);
+                            }
+                        ],
+                        'cmd_delete_sdist' : [
+                            ['command'],
+                            function() {
+                                var overridable_events = [ //TODO: proper overrides
+                                ];
+                                obj.do_delete('sdist', 'open-ils.serial.distribution.fleshed.batch.update', overridable_events);
+                            }
+                        ],
+                        'cmd_delete_siss' : [
+                            ['command'],
+                            function() {
+                                var overridable_events = [ //TODO: proper overrides
+                                ];
+                                obj.do_delete('siss', 'open-ils.serial.issuance.fleshed.batch.update', overridable_events);
+                            }
+                        ],
+                        'cmd_delete_ssub' : [
+                            ['command'],
+                            function() {
+                                var overridable_events = [
+                                    11000 // SERIAL_SUBSCRIPTION_NOT_EMPTY
+                                ];
+                                obj.do_delete('ssub', 'open-ils.serial.subscription.fleshed.batch.update', overridable_events);
+                            }
+                        ],
+                        /*dbw2 'cmd_delete_ssub' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    JSAN.use('util.functional');
+
+                                    var list = util.functional.filter_list(
+                                        obj.sel_list,
+                                        function (o) {
+                                            return o.split(/_/)[0] == 'ssub';
+                                        }
+                                    );
+
+                                    list = util.functional.map_list(
+                                        list,
+                                        function (o) {
+                                            return JSON2js( js2JSON( obj.map_ssub[ 'ssub_' + o.split(/_/)[1] ] ) );
+                                        }
+                                    );
+
+                                    var del_prompt;
+                                    if (list.length == 1) {
+                                        //TODO: correct prompts
+                                        del_prompt = document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.prompt');
+                                    } else {
+                                        del_prompt = document.getElementById('catStrings').getFormattedString('staff.cat.copy_browser.delete_volume.prompt.plural', [list.length]);
+                                    }
+
+                                    var r = obj.error.yns_alert(
+                                            del_prompt,
+                                            document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.title'),
+                                            document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.delete'),
+                                            document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.cancel'),
+                                            null,
+                                            document.getElementById('commonStrings').getString('common.confirm')
+                                    );
+
+                                    if (r == 0) {
+                                        for (var i = 0; i < list.length; i++) {
+                                            list[i].isdeleted('1');
+                                        }
+                                        var robj = obj.network.simple_request(
+                                            'FM_ACN_TREE_UPDATE', 
+                                            [ ses(), list, true ],
+                                            null,
+                                            {
+                                                'title' : document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.override'),
+                                                'overridable_events' : [
+                                                ]
+                                            }
+                                        );
+                                        if (robj == null) throw(robj);
+                                        if (typeof robj.ilsevent != 'undefined') {
+                                            if (robj.ilsevent == 1206 ) { // VOLUME_NOT_EMPTY
+                                                alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.copies_remain'));
+                                                return;
+                                            }
+                                            if (robj.ilsevent != 0) throw(robj);
+                                        }
+                                        alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.success'));
+                                        obj.refresh_list();
+                                    }
+                                } catch(E) {
+                                    obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.delete_volume.exception'),E);
+                                    obj.refresh_list();
+                                }
+
+                            }
+                        ], dbw2*/
+                        'cmd_mark_library' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    var list = obj.ids_from_sel_list('aou');
+                                    if (list.length == 1) {
+                                        obj.data.marked_library = { 'lib' : list[0], 'docid' : obj.docid };
+                                        obj.data.stash('marked_library');
+                                        alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.mark_library.alert'));
+                                    } else {
+                                        obj.error.yns_alert(
+                                                document.getElementById('catStrings').getString('staff.cat.copy_browser.mark_library.prompt'),
+                                                document.getElementById('catStrings').getString('staff.cat.copy_browser.mark_library.title'),
+                                                document.getElementById('commonStrings').getString('common.ok'),
+                                                null,
+                                                null,
+                                                document.getElementById('commonStrings').getString('common.confirm')
+                                                );
+                                    }
+                                } catch(E) {
+                                    obj.error.standard_unexpected_error_alert('manage_subs.js -> mark library',E);
+                                }
+                            }
+                        ],
+
+                        'cmd_mark_subscription' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    var list = obj.ids_from_sel_list('ssub');
+                                    if (list.length == 1) {
+                                        obj.data.marked_subscription = list[0];
+                                        obj.data.stash('marked_subscription');
+                                        alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.mark_volume.alert'));
+                                    } else {
+                                        obj.error.yns_alert(
+                                                document.getElementById('catStrings').getString('staff.cat.copy_browser.mark_volume.prompt'),
+                                                document.getElementById('catStrings').getString('staff.cat.copy_browser.mark_volume.title'),
+                                                document.getElementById('commonStrings').getString('common.ok'),
+                                                null,
+                                                null,
+                                                document.getElementById('commonStrings').getString('common.confirm')
+                                                );
+                                    }
+                                } catch(E) {
+                                    obj.error.standard_unexpected_error_alert('manage_subs.js -> mark subscription',E);
+                                }
+                            }
+                        ],
+                        'cmd_add_subscriptions' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    var list = obj.ids_from_sel_list('aou');
+                                    if (list.length == 0) return;
+                                    //TODO: permission check?
+                                    /*var edit = 0;
+                                    try {
+                                        edit = obj.network.request(
+                                            api.PERM_MULTI_ORG_CHECK.app,
+                                            api.PERM_MULTI_ORG_CHECK.method,
+                                            [ 
+                                                ses(), 
+                                                obj.data.list.au[0].id(), 
+                                                list,
+                                                [ 'CREATE_VOLUME', 'CREATE_COPY' ]
+                                            ]
+                                        ).length == 0 ? 1 : 0;
+                                    } catch(E) {
+                                        obj.error.sdump('D_ERROR','batch permission check: ' + E);
+                                    }
+
+                                    if (edit==0) {
+                                        alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.add_volume.permission_error'));
+                                        return; // no read-only view for this interface
+                                    } */
+                                    var new_ssub = new ssub();
+                                    new_ssub.owning_lib(list[0]);//TODO: add multiple at once support?
+                                    new_ssub.isnew(1);
+                                    new_ssub.record_entry(obj.docid);
+                                    var params = {};
+                                    params.ssubs = [new_ssub];
+                                    obj.editor_init('ssub', 'add', params);
+                                } catch(E) {
+                                    obj.error.standard_unexpected_error_alert(document.getElementById('serialStrings').getString('staff.serial.manage_subs.add.error'),E);
+                                }
+                            }
+                        ],
+                        'cmd_transfer_subscription' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    obj.data.stash_retrieve();
+                                    if (!obj.data.marked_library) {
+                                        alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.transfer_volume.alert'));
+                                        return;
+                                    }
+                                    
+                                    var list = obj.ids_from_sel_list('ssub');
+
+                                    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect UniversalBrowserWrite');
+
+                                    JSAN.use('util.functional');
+
+                                    var ssub_list = util.functional.map_list(
+                                        list,
+                                        function (o) {
+                                            return obj.map_ssub[ 'ssub_' + o ].start_date();
+                                        }
+                                    ).join(document.getElementById('commonStrings').getString('common.grouping_string'));
+
+                                    var xml = '<vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" flex="1" style="overflow: auto">';
+                                    xml += '<description>';
+                                    xml += document.getElementById('catStrings').getFormattedString('staff.cat.copy_browser.transfer.prompt', [ssub_list, obj.data.hash.aou[ obj.data.marked_library.lib ].shortname()]);
+                                    xml += '</description>';
+                                    xml += '<hbox><button label="' + document.getElementById('catStrings').getString('staff.cat.copy_browser.transfer.submit.label') + '" name="fancy_submit"/>';
+                                    xml += '<button label="' 
+                                        + document.getElementById('catStrings').getString('staff.cat.copy_browser.transfer.cancel.label') 
+                                        + '" accesskey="' 
+                                        + document.getElementById('catStrings').getString('staff.cat.copy_browser.transfer.cancel.accesskey') 
+                                        + '" name="fancy_cancel"/></hbox>';
+                                    xml += '<iframe style="overflow: scroll" flex="1" src="' + urls.XUL_BIB_BRIEF + '?docid=' + obj.data.marked_library.docid + '"/>';
+                                    xml += '</vbox>';
+                                    JSAN.use('OpenILS.data');
+                                    var data = new OpenILS.data(); data.init({'via':'stash'});
+                                    //data.temp_transfer = xml; data.stash('temp_transfer');
+                                    JSAN.use('util.window'); var win = new util.window();
+                                    var fancy_prompt_data = win.open(
+                                        urls.XUL_FANCY_PROMPT,
+                                        //+ '?xml_in_stash=temp_transfer'
+                                        //+ '&title=' + window.escape('Volume Transfer'),
+                                        'fancy_prompt', 'chrome,resizable,modal,width=500,height=300',
+                                        {
+                                            'xml' : xml,
+                                            'title' : document.getElementById('catStrings').getString('staff.cat.copy_browser.transfer.title')
+                                        }
+                                    );
+
+                                    if (fancy_prompt_data.fancy_status == 'incomplete') {
+                                        alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.transfer.incomplete'));
+                                        return;
+                                    }
+
+                                    var robj = obj.network.simple_request(
+                                        'FM_ACN_TRANSFER', 
+                                        [ ses(), { 'docid' : obj.data.marked_library.docid, 'lib' : obj.data.marked_library.lib, 'subscriptions' : list } ],
+                                        null,
+                                        {
+                                            'title' : document.getElementById('catStrings').getString('staff.cat.copy_browser.transfer.override.failure'),
+                                            'overridable_events' : [
+                                                1208, // TITLE_LAST_COPY
+                                                1219, // COPY_REMOTE_CIRC_LIB
+                                            ],
+                                        }
+                                    );
+
+                                    if (typeof robj.ilsevent != 'undefined') {
+                                        if (robj.ilsevent == 1221) { // ORG_CANNOT_HAVE_VOLS
+                                            alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.transfer.ineligible_destination'));
+                                        } else {
+                                            throw(robj);
+                                        }
+                                    } else {
+                                        alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.transfer.success'));
+                                    }
+
+                                } catch(E) {
+                                    obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.transfer.unexpected_error'),E);
+                                }
+                                obj.refresh_list();
+                            }
+                        ],
+
+                        'cmd_transfer_sdists' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    obj.data.stash_retrieve();
+                                    if (!obj.data.marked_subscription) {
+                                        alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.transfer_items.missing_volume'));
+                                        return;
+                                    }
+                                    
+                                    JSAN.use('util.functional');
+
+                                    var list = obj.ids_from_sel_list('sdist');
+                                    var subscription = obj.network.simple_request('FM_ACN_RETRIEVE.authoritative',[ obj.data.marked_subscription ]);
+
+                                    JSAN.use('cat.util'); cat.util.transfer_copies( { 
+                                        'distribution_ids' : list, 
+                                        'docid' : subscription.record(),
+                                        'subscription_label' : subscription.start_date(),
+                                        'owning_lib' : subscription.owning_lib(),
+                                    } );
+
+                                } catch(E) {
+                                    obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.transfer_items.unexpected_error'),E);
+                                }
+                                obj.refresh_list();
+                            }
+                        ],
+                        'cmd_refresh_list' : [
+                            ['command'],
+                            function() {
+                                obj.refresh_list();
+                            }
+                        ],
+                        'cmd_make_predictions' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    var list = obj.ids_from_sel_list('ssub');
+                                    if (list.length == 0) {
+                                        alert('You must select a subscription before predicting issuances.'); //TODO: better error
+                                        return;
+                                    }
+
+                                    var num_to_predict = prompt('How many items would you like to predict?',
+                                            '12',
+                                            'Number of Predicted Items');
+                                    num_to_predict = String( num_to_predict ).replace(/\D/g,'');
+                                    if (num_to_predict == '') {
+                                        alert('Invalid number entered!'); //TODO: better error
+                                        return;
+                                    }
+
+                                    for (i = 0; i < list.length; i++) {
+                                        var robj = obj.network.request(
+                                                'open-ils.serial',
+                                                'open-ils.serial.make_predictions',
+                                                [ ses(), {"ssub_id":list[i], "num_to_predict":num_to_predict, "last_rec_date":"2010-07-07"}]
+                                        );
+                                        util.functional.map_list(
+                                            robj,
+                                            function(o) {
+                                                alert('debug: ' + o.date_expected());
+                                            }
+                                        );
+                                    }
+                                    return;
+
+                                    /*JSAN.use('util.functional');
+                                    var list = util.functional.map_list(
+                                            robj,
+                                            function (o) {
+                                                o.distribution(obj.sdist_id);
+                                                return o;
+                                            }
+                                        );*/
+
+                                    var robj = obj.network.request(
+                                                'open-ils.serial',
+                                                'open-ils.serial.item.fleshed.batch.update',
+                                                [ ses(), list ]
+                                            );
+
+                                    //obj.refresh_list('main');
+
+                                } catch(E) {
+                                    obj.error.standard_unexpected_error_alert('cmd_make_predictions failed!',E);
+                                }
+                            }
+                        ],
+/*dbw2                      'sel_distribution_details' : [
+                            ['command'],
+                            function() {
+                                JSAN.use('util.functional');
+
+                                var list = util.functional.filter_list(
+                                    obj.sel_list,
+                                    function (o) {
+                                        return o.split(/_/)[0] == 'sdist';
+                                    }
+                                );
+
+                                list = util.functional.map_list(
+                                    list,
+                                    function (o) {
+                                        return o.split(/_/)[1];
+                                    }
+                                );
+    
+                                JSAN.use('circ.util');
+                                for (var i = 0; i < list.length; i++) {
+                                    circ.util.show_copy_details( list[i] );
+                                }
+                            }
+                        ],
+                        'cmd_edit_sdists' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    JSAN.use('util.functional');
+
+                                    var list = util.functional.filter_list(
+                                        obj.sel_list,
+                                        function (o) {
+                                            return o.split(/_/)[0] == 'sdist';
+                                        }
+                                    );
+
+                                    list = util.functional.map_list(
+                                        list,
+                                        function (o) {
+                                            return o.split(/_/)[1];
+                                        }
+                                    );
+
+                                    JSAN.use('cat.util'); cat.util.spawn_copy_editor( { 'copy_ids' : list, 'edit' : 1 } );
+                                    obj.refresh_list();
+
+                                } catch(E) {
+                                    obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.edit_items.error'),E);
+                                }
+                            }
+                        ], dbw2*/
+
+/*dbw2                      'cmd_print_spine_labels' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    JSAN.use('util.functional');
+                                    
+                                    var list = util.functional.filter_list(
+                                        obj.sel_list,
+                                        function (o) {
+                                            return o.split(/_/)[0] == 'sdist';
+                                        }
+                                    );
+
+                                    list = util.functional.map_list(
+                                        list,
+                                        function (o) {
+                                            return obj.map_sdist[ o ];
+                                        }
+                                    );
+
+                                    obj.data.temp_barcodes_for_labels = util.functional.map_list( list, function(o){return o.barcode();}) ; 
+                                    obj.data.stash('temp_barcodes_for_labels');
+                                    xulG.new_tab(
+                                        xulG.url_prefix( urls.XUL_SPINE_LABEL ),
+                                        { 'tab_name' : document.getElementById('catStrings').getString('staff.cat.copy_browser.print_spine.tab') },
+                                        {}
+                                    );
+                                } catch(E) {
+                                    obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.print_spine.error'),E);
+                                }
+                            }
+                        ],
+                        'cmd_edit_subscriptions' : [
+                            ['command'],
+                            function() {
+                                try {
+                                    JSAN.use('util.functional');
+                                    var list = util.functional.map_list(
+                                        util.functional.filter_list(
+                                            obj.sel_list,
+                                            function (o) {
+                                                return o.split(/_/)[0] == 'ssub';
+                                            }
+                                        ),
+                                        function (o) {
+                                            return o.split(/_/)[1];
+                                        }
+                                    );
+                                    if (list.length == 0) return;
+
+                                    var edit = 0;
+                                    try {
+                                        edit = obj.network.request(
+                                            api.PERM_MULTI_ORG_CHECK.app,
+                                            api.PERM_MULTI_ORG_CHECK.method,
+                                            [ 
+                                                ses(), 
+                                                obj.data.list.au[0].id(), 
+                                                util.functional.map_list(
+                                                    list,
+                                                    function (o) {
+                                                        return obj.map_ssub[ 'ssub_' + o ].owning_lib();
+                                                    }
+                                                ),
+                                                [ 'UPDATE_VOLUME' ]
+                                            ]
+                                        ).length == 0 ? 1 : 0;
+                                    } catch(E) {
+                                        obj.error.sdump('D_ERROR','batch permission check: ' + E);
+                                    }
+
+                                    if (edit==0) {
+                                        alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.edit_volume.permission_error'));
+                                        return; // no read-only view for this interface
+                                    }
+
+                                    list = util.functional.map_list(
+                                        list,
+                                        function (o) {
+                                            var my_ssub = obj.map_ssub['ssub_' + o];
+                                            return function(r){return r;}(my_ssub);
+                                        }
+                                    );
+
+                                    var title;
+                                    if (list.length == 1) {
+                                        title = document.getElementById('catStrings').getString('staff.cat.copy_browser.edit_volume.title');
+                                    } else {
+                                        title = document.getElementById('catStrings').getString('staff.cat.copy_browser.edit_volume.title.plural');
+                                    }
+
+                                    JSAN.use('util.window'); var win = new util.window();
+                                    //obj.data.volumes_temp = js2JSON( list );
+                                    //obj.data.stash('volumes_temp');
+                                    var my_xulG = win.open(
+                                        window.xulG.url_prefix(urls.XUL_VOLUME_EDITOR),
+                                        title,
+                                        'chrome,modal,resizable',
+                                        { 'subscriptions' : JSON2js(js2JSON(list)) }
+                                    );
+
+                                    // FIXME -- need to unique the temp space, and not rely on modalness of window
+                                    //obj.data.stash_retrieve();
+                                    if (typeof my_xulG.update_these_subscriptions == 'undefined') { return; }
+                                    var subscriptions = my_xulG.subscriptions;
+                                    if (!subscriptions) return;
+                                
+                                    subscriptions = util.functional.filter_list(
+                                        subscriptions,
+                                        function (o) {
+                                            return o.ischanged() == '1';
+                                        }
+                                    );
+
+                                    subscriptions = util.functional.map_list(
+                                        subscriptions,
+                                        function (o) {
+                                            o.record( obj.docid ); // staff client 2 did not do this.  Does it matter?
+                                            return o;
+                                        }
+                                    );
+
+                                    if (subscriptions.length == 0) return;
+
+                                    try {
+                                        var r = obj.network.request(
+                                            api.FM_ACN_TREE_UPDATE.app,
+                                            api.FM_ACN_TREE_UPDATE.method,
+                                            [ ses(), subscriptions, true ]
+                                        );
+                                        if (typeof r.ilsevent != 'undefined') {
+                                            switch(Number(r.ilsevent)) {
+                                                case 1705 : // VOLUME_LABEL_EXISTS
+                                                    alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.edit_volume.failed'));
+                                                    break;
+                                                default: throw(r);
+                                            }
+                                        } else {
+                                            alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.edit_volume.success'));
+                                        }
+                                    } catch(E) {
+                                        obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.edit_volume.error'),E);
+                                    }
+                                    obj.refresh_list();
+
+                                } catch(E) {
+                                    obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.edit_volume.exception'),E);
+                                }
+                            }
+                        ], dbw2*/
+                    }
+                }
+            );
+
+            obj.list_init(params);
+
+            obj.org_ids = obj.network.simple_request('FM_SSUB_AOU_IDS_RETRIEVE_VIA_RECORD_ID.authoritative',[ obj.docid ]);
+            if (typeof obj.org_ids.ilsevent != 'undefined') throw(obj.org_ids);
+            JSAN.use('util.functional'); 
+            obj.org_ids = util.functional.map_list( obj.org_ids, function (o) { return Number(o); });
+
+            var org = obj.data.hash.aou[ obj.data.list.au[0].ws_ou() ];
+            //obj.show_libs( org );
+
+            //obj.show_my_libs();
+
+            JSAN.use('util.file'); JSAN.use('util.widgets');
+
+            var file; var list_data; var ml; 
+
+            file = new util.file('offline_ou_list'); 
+            if (file._file.exists()) {
+                list_data = file.get_object(); file.close();
+                ml = util.widgets.make_menulist( list_data[0], list_data[1] );
+                ml.setAttribute('id','lib_menu'); document.getElementById('serial_sub_lib_menu').appendChild(ml);
+                //TODO: class this menu properly
+                for (var i = 0; i < obj.org_ids.length; i++) {
+                    ml.getElementsByAttribute('value',obj.org_ids[i])[0].setAttribute('class','has_distributions');
+                }
+                ml.firstChild.addEventListener(
+                    'popupshown',
+                    function(ev) {
+                        document.getElementById('legend').setAttribute('hidden','false');
+                    },
+                    false
+                );
+                ml.firstChild.addEventListener(
+                    'popuphidden',
+                    function(ev) {
+                        document.getElementById('legend').setAttribute('hidden','true');
+                    },
+                    false
+                );
+                ml.addEventListener(
+                    'command',
+                    function(ev) {
+                        if (document.getElementById('refresh_button')) document.getElementById('refresh_button').focus(); 
+                        JSAN.use('util.file'); var file = new util.file('manage_subs_prefs.'+obj.data.server_unadorned);
+                        util.widgets.save_attributes(file, { 'lib_menu' : [ 'value' ], 'show_ssubs' : [ 'checked' ], 'show_groups' : [ 'checked' ] });
+                        obj.refresh_list();
+                    },
+                    false
+                );
+            } else {
+                throw(document.getElementById('catStrings').getString('staff.cat.copy_browser.missing_library') + '\n');
+            }
+
+            file = new util.file('manage_subs_prefs.'+obj.data.server_unadorned);
+            util.widgets.load_attributes(file);
+            ml.value = ml.getAttribute('value');
+            if (! ml.value) {
+                ml.value = org.id();
+                ml.setAttribute('value',ml.value);
+            }
+
+            document.getElementById('show_ssubs').addEventListener(
+                'command',
+                function(ev) {
+                    JSAN.use('util.file'); var file = new util.file('manage_subs_prefs.'+obj.data.server_unadorned);
+                    util.widgets.save_attributes(file, { 'lib_menu' : [ 'value' ], 'show_ssubs' : [ 'checked' ], 'show_groups' : [ 'checked' ] });
+                },
+                false
+            );
+
+            document.getElementById('show_groups').addEventListener(
+                'command',
+                function(ev) {
+                    JSAN.use('util.file'); var file = new util.file('manage_subs_prefs.'+obj.data.server_unadorned);
+                    util.widgets.save_attributes(file, { 'lib_menu' : [ 'value' ], 'show_ssubs' : [ 'checked' ], 'show_groups' : [ 'checked' ] });
+                },
+                false
+            );
+
+            obj.show_my_libs( ml.value );
+
+            JSAN.use('util.exec'); var exec = new util.exec(20); exec.timer(obj.funcs,100);
+
+            obj.toggle_actions(); // disable menus initially
+
+        } catch(E) {
+            this.error.standard_unexpected_error_alert('serial/manage_subs.init: ',E);
+        }
+    },
+
+    'show_my_libs' : function(org) {
+        var obj = this;
+        try {
+            if (!org) {
+                org = obj.data.hash.aou[ obj.data.list.au[0].ws_ou() ];
+            } else {
+                if (typeof org != 'object') org = obj.data.hash.aou[ org ];
+            }
+            obj.show_libs( org, false );
+        
+            var p_org = obj.data.hash.aou[ org.parent_ou() ];
+            if (p_org) {
+                obj.funcs.push( function() { 
+                    document.getElementById('cmd_refresh_list').setAttribute('disabled','true'); 
+                    document.getElementById('cmd_show_libs_with_distributions').setAttribute('disabled','true'); 
+                    document.getElementById('lib_menu').setAttribute('disabled','true'); 
+                } );
+                for (var i = 0; i < p_org.children().length; i++) {
+                    obj.funcs.push(
+                        function(o) {
+                            return function() {
+                                obj.show_libs( o, false );
+                            }
+                        }( p_org.children()[i] )
+                    );
+                }
+                obj.funcs.push( function() { 
+                    document.getElementById('cmd_refresh_list').setAttribute('disabled','false'); 
+                    document.getElementById('cmd_show_libs_with_distributions').setAttribute('disabled','false'); 
+                    document.getElementById('lib_menu').setAttribute('disabled','false'); 
+                } );
+            }
+        } catch(E) {
+            alert(E);
+        }
+    },
+
+    'show_all_libs' : function() {
+        var obj = this;
+        try {
+            obj.show_my_libs();
+
+            obj.show_libs( obj.data.tree.aou );
+
+            obj.funcs.push( function() { 
+                document.getElementById('cmd_refresh_list').setAttribute('disabled','true'); 
+                document.getElementById('cmd_show_libs_with_distributions').setAttribute('disabled','true'); 
+                document.getElementById('lib_menu').setAttribute('disabled','true'); 
+            } );
+
+            for (var i = 0; i < obj.data.tree.aou.children().length; i++) {
+                obj.funcs.push(
+                    function(o) {
+                        return function() {
+                            obj.show_libs( o );
+                        }
+                    }( obj.data.tree.aou.children()[i] )
+                );
+            }
+            obj.funcs.push( function() { 
+                document.getElementById('cmd_refresh_list').setAttribute('disabled','false'); 
+                document.getElementById('cmd_show_libs_with_distributions').setAttribute('disabled','false'); 
+                document.getElementById('lib_menu').setAttribute('disabled','false'); 
+            } );
+
+        } catch(E) {
+            alert(E);
+        }
+    },
+
+    'show_libs_with_distributions' : function() {
+        var obj = this;
+        try {
+            JSAN.use('util.functional');
+
+            var orgs = util.functional.map_list(
+                obj.org_ids,
+                function(id) { return obj.data.hash.aou[id]; }
+            ).sort(
+                function( a, b ) {
+                    if (a.shortname() < b.shortname()) return -1;
+                    if (a.shortname() > b.shortname()) return 1;
+                    return 0;
+                }
+            );
+            obj.funcs.push( function() { 
+                document.getElementById('cmd_refresh_list').setAttribute('disabled','true'); 
+                document.getElementById('cmd_show_libs_with_distributions').setAttribute('disabled','true'); 
+                document.getElementById('lib_menu').setAttribute('disabled','true'); 
+            } );
+
+            for (var i = 0; i < orgs.length; i++) {
+                obj.funcs.push(
+                    function(o) {
+                        return function() {
+                            obj.show_libs(o,false);
+                        }
+                    }( orgs[i] )
+                );
+            }
+            obj.funcs.push( function() { 
+                document.getElementById('cmd_refresh_list').setAttribute('disabled','false'); 
+                document.getElementById('cmd_show_libs_with_distributions').setAttribute('disabled','false'); 
+                document.getElementById('lib_menu').setAttribute('disabled','false'); 
+            } );
+
+        } catch(E) {
+            alert(E);
+        }
+    },
+
+    'show_libs' : function(start_aou,show_open) {
+        var obj = this;
+        try {
+            if (!start_aou) throw('show_libs: Need a start_aou');
+            JSAN.use('OpenILS.data'); obj.data = new OpenILS.data(); obj.data.init({'via':'stash'});
+            JSAN.use('util.functional'); 
+
+            var parents = [];
+            var temp_aou = start_aou;
+            while ( temp_aou.parent_ou() ) {
+                temp_aou = obj.data.hash.aou[ temp_aou.parent_ou() ];
+                parents.push( temp_aou );
+            }
+            parents.reverse();
+
+            for (var i = 0; i < parents.length; i++) {
+                obj.funcs.push(
+                    function(o,p) {
+                        return function() { 
+                            obj.append_org(o,p,{'container':'true','open':'true'}); 
+                        };
+                    }(parents[i], obj.data.hash.aou[ parents[i].parent_ou() ])
+                );
+            }
+
+            obj.funcs.push(
+                function(o,p) {
+                    return function() { obj.append_org(o,p); };
+                }(start_aou,obj.data.hash.aou[ start_aou.parent_ou() ])
+            );
+
+            obj.funcs.push(
+                function() {
+                    if (start_aou.children()) {
+                        var x = obj.map_tree[ 'aou_' + start_aou.id() ];
+                        x.setAttribute('container','true');
+                        if (show_open) x.setAttribute('open','true');
+                        for (var i = 0; i < start_aou.children().length; i++) {
+                            obj.funcs.push(
+                                function(o,p) {
+                                    return function() { obj.append_org(o,p); };
+                                }( start_aou.children()[i], start_aou )
+                            );
+                        }
+                    }
+                }
+            );
+
+        } catch(E) {
+            alert(E);
+        }
+    },
+
+    'on_select' : function(list,twisty) {
+        var obj = this;
+        var sel_lists = {};
+
+        for (var i = 0; i < list.length; i++) {
+            var row_type = list[i].split('_')[0];
+            var id = list[i].split('_')[1];
+
+            if (!sel_lists[row_type]) sel_lists[row_type] = [];
+            sel_lists[row_type].push(id);
+
+            if (twisty) {
+                switch(row_type) {
+                    case 'aou' : obj.on_click_aou(id,twisty); break;
+                    case 'ssub' : obj.on_select_ssub(id,twisty); break;
+                    default: break;
+                }
+            }
+        }
+
+        if (!obj.focused_node_retrieve_id) return;
+
+        var row_type = obj.focused_node_retrieve_id.split('_')[0];
+        var id = obj.focused_node_retrieve_id.split('_')[1];
+
+        if (sel_lists[row_type]) { // the type focused is in the selection (usually the case)
+            switch(row_type) {
+                case 'aou' : obj.on_click_aou(id,twisty); break;
+                default: if (obj['on_click_' + row_type]) obj['on_click_' + row_type](sel_lists[row_type],twisty);
+            }
+        }
+    },
+
+    'on_select_ssub' : function(ssub_id,twisty) {
+        var obj = this;
+        try {
+            //typo? var ssub_tree = obj.map_sdist[ 'ssub_' + ssub_id ];
+            var ssub_tree = obj.map_ssub[ 'ssub_' + ssub_id ];
+            obj.funcs.push( function() { 
+                document.getElementById('cmd_refresh_list').setAttribute('disabled','true'); 
+                document.getElementById('cmd_show_libs_with_distributions').setAttribute('disabled','true'); 
+                document.getElementById('lib_menu').setAttribute('disabled','true'); 
+            } );
+            if (ssub_tree.distributions()) {
+                for (var i = 0; i < ssub_tree.distributions().length; i++) {
+                    obj.funcs.push(
+                        function(c,a) {
+                            return function() {
+                                obj.append_member(c,a,[],'sdist');
+                            }
+                        }( ssub_tree.distributions()[i], ssub_tree )
+                    )
+                }
+            }
+            if (ssub_tree.issuances()) {
+                for (var i = 0; i < ssub_tree.issuances().length; i++) {
+                    obj.funcs.push(
+                        function(c,a) {
+                            return function() {
+                                obj.append_member(c,a,[],'siss');
+                            }
+                        }( ssub_tree.issuances()[i], ssub_tree )
+                    )
+                }
+            }
+            if (ssub_tree.scaps()) {
+                for (var i = 0; i < ssub_tree.scaps().length; i++) {
+                    obj.funcs.push(
+                        function(c,a) {
+                            return function() {
+                                obj.append_member(c,a,[],'scap');
+                            }
+                        }( ssub_tree.scaps()[i], ssub_tree )
+                    )
+                }
+            }
+            obj.funcs.push( function() { 
+                document.getElementById('cmd_refresh_list').setAttribute('disabled','false'); 
+                document.getElementById('cmd_show_libs_with_distributions').setAttribute('disabled','false'); 
+                document.getElementById('lib_menu').setAttribute('disabled','false'); 
+            } );
+        } catch(E) {
+            alert(E);
+        }
+    },
+
+    'on_click_ssub' : function(ssub_ids,twisty) {
+        var obj = this;
+        try {
+            // draw sdist editor
+            if (typeof twisty == 'undefined') {
+                var params = {};
+                params.ssub_ids = ssub_ids;
+                obj.editor_init('ssub', 'edit', params);
+            }
+        } catch(E) {
+            alert(E);
+        }
+    },
+
+    'on_click_sdist' : function(sdist_ids,twisty) {
+        var obj = this;
+        try {
+            // draw sdist editor
+            if (typeof twisty == 'undefined') {
+                var params = {};
+                params.sdist_ids = sdist_ids;
+                obj.editor_init('sdist', 'edit', params);
+            }
+        } catch(E) {
+            alert(E);
+        }
+    },
+
+    'on_click_siss' : function(siss_ids,twisty) {
+        var obj = this;
+        try {
+            // draw siss editor
+            if (typeof twisty == 'undefined') {
+                var params = {};
+                params.siss_ids = siss_ids;
+                obj.editor_init('siss', 'edit', params);
+            }
+        } catch(E) {
+            alert(E);
+        }
+    },
+
+    'on_click_scap' : function(scap_ids,twisty) {
+        var obj = this;
+        try {
+            // draw scap editor
+            if (typeof twisty == 'undefined') {
+                var params = {};
+                params.scap_ids = scap_ids;
+                obj.editor_init('scap', 'edit', params);
+            }
+        } catch(E) {
+            alert(E);
+        }
+    },
+
+    'on_click_aou' : function(org_id,twisty) {
+        var obj = this;
+        var org = obj.data.hash.aou[ org_id ];
+        obj.funcs.push( function() { 
+            document.getElementById('cmd_refresh_list').setAttribute('disabled','true'); 
+            document.getElementById('cmd_show_libs_with_distributions').setAttribute('disabled','true'); 
+            document.getElementById('lib_menu').setAttribute('disabled','true'); 
+        } );
+        if (org.children()) {
+            for (var i = 0; i < org.children().length; i++) {
+                obj.funcs.push(
+                    function(o,p) {
+                        return function() {
+                            obj.append_org(o,p)
+                        }
+                    }(org.children()[i],org)
+                );
+            }
+        } 
+        if (obj.map_ssub[ 'aou_' + org_id ]) {
+            for (var i = 0; i < obj.map_ssub[ 'aou_' + org_id ].length; i++) {
+                obj.funcs.push(
+                    function(o,a) {
+                        return function() {
+                            obj.append_ssub(o,a);
+                        }
+                    }( org, obj.map_ssub[ 'aou_' + org_id ][i] )
+                );
+            }
+        }
+        obj.funcs.push( function() { 
+            document.getElementById('cmd_refresh_list').setAttribute('disabled','false'); 
+            document.getElementById('cmd_show_libs_with_distributions').setAttribute('disabled','false'); 
+            document.getElementById('lib_menu').setAttribute('disabled','false'); 
+        } );
+
+        // remove current editor
+        if (typeof twisty == 'undefined') {
+            document.getElementById('serial_manage_subs_editor_deck').selectedIndex = 0;
+        }
+    },
+
+    'append_org' : function (org,parent_org,params) {
+        var obj = this;
+        try {
+            if (obj.map_tree[ 'aou_' + org.id() ]) {
+                var x = obj.map_tree[ 'aou_' + org.id() ];
+                if (params) {
+                    for (var i in params) {
+                        x.setAttribute(i,params[i]);
+                    }
+                }
+                return x;
+            }
+
+            var data = {
+                'row' : {
+                    'my' : {
+                        'aou' : org,
+                    }
+                },
+                'skip_all_columns_except' : [0,1,2],
+                'retrieve_id' : 'aou_' + org.id(),
+                'to_bottom' : true,
+                'no_auto_select' : true,
+            };
+        
+            var ssub_tree_list;
+            if ( obj.org_ids.indexOf( Number( org.id() ) ) == -1 ) {
+                if ( get_bool( obj.data.hash.aout[ org.ou_type() ].can_have_vols() ) ) {
+                    data.row.my.subscription_count = '0';
+                    //data.row.my.distribution_count = '<0>';
+                } else {
+                    data.row.my.subscription_count = '';
+                    //data.row.my.distribution_count = '';
+                }
+            } else {
+                var v_count = 0; var d_count = 0;
+                ssub_tree_list = obj.network.simple_request(
+                    'FM_SSUB_TREE_LIST_RETRIEVE_VIA_RECORD_ID_AND_ORG_IDS.authoritative',
+                    [ ses(), obj.docid, [ org.id() ] ]
+                );
+                for (var i = 0; i < ssub_tree_list.length; i++) {
+                    v_count++;
+                    obj.map_ssub[ 'ssub_' + ssub_tree_list[i].id() ] = function(r){return r;}(ssub_tree_list[i]);
+                    var distributions = ssub_tree_list[i].distributions();
+                    //if (distributions) d_count += distributions.length;
+                    for (var j = 0; j < distributions.length; j++) {
+                        obj.map_sdist[ 'sdist_' + distributions[j].id() ] = function(r){return r;}(distributions[j]);
+                    }
+                    var issuances = ssub_tree_list[i].issuances();
+                    for (var j = 0; j < issuances.length; j++) {
+                        obj.map_siss[ 'siss_' + issuances[j].id() ] = function(r){return r;}(issuances[j]);
+                    }
+                    var scaps = ssub_tree_list[i].scaps();
+                    for (var j = 0; j < scaps.length; j++) {
+                        obj.map_scap[ 'scap_' + scaps[j].id() ] = function(r){return r;}(scaps[j]);
+                    }
+                }
+                data.row.my.subscription_count = v_count;
+                //data.row.my.distribution_count = '<' + d_count + '>';
+            }
+            if (parent_org) {
+                data.node = obj.map_tree[ 'aou_' + parent_org.id() ];
+            }
+            var nparams = obj.list.append(data);
+            var node = nparams.my_node;
+            if (params) {
+                for (var i in params) {
+                    node.setAttribute(i,params[i]);
+                }
+            }
+            obj.map_tree[ 'aou_' + org.id() ] = node;
+
+            if (org.children()) {
+                node.setAttribute('container','true');
+            }
+
+            if (parent_org) {
+                if ( obj.data.hash.aou[ obj.data.list.au[0].ws_ou() ].parent_ou() == parent_org.id() ) {
+                    data.node.setAttribute('open','true');
+                }
+            } else {
+                obj.map_tree[ 'aou_' + org.id() ].setAttribute('open','true');
+            }
+
+            if (ssub_tree_list) {
+                obj.map_ssub[ 'aou_' + org.id() ] = ssub_tree_list;
+                node.setAttribute('container','true');
+            }
+
+            if (document.getElementById('show_ssubs').checked) {
+                obj.funcs.push( function() { obj.on_click_aou( org.id() ); } );
+                node.setAttribute('open','true');
+            }
+
+        } catch(E) {
+            dump(E+'\n');
+            alert(E);
+        }
+    },
+
+    'append_ssub' : function( org, ssub_tree, params ) {
+        var obj = this;
+        try {
+            if (obj.map_tree[ 'ssub_' + ssub_tree.id() ]) {
+                var x = obj.map_tree[ 'ssub_' + ssub_tree.id() ];
+                if (params) {
+                    for (var i in params) {
+                        x.setAttribute(i,params[i]);
+                    }
+                }
+                return x;
+            }
+
+            var parent_node = obj.map_tree[ 'aou_' + org.id() ];
+            var data = {
+                'row' : {
+                    'my' : {
+                        'aou' : org,
+                        'ssub' : ssub_tree,
+                        'subscription_count' : '',
+                        //'distribution_count' : ssub_tree.distributions() ? ssub_tree.distributions().length : '0',
+                    }
+                },
+                'skip_all_columns_except' : [0,1,2],
+                'retrieve_id' : 'ssub_' + ssub_tree.id(),
+                'node' : parent_node,
+                'to_bottom' : true,
+                'no_auto_select' : true,
+            };
+            var nparams = obj.list.append(data);
+            var node = nparams.my_node;
+            obj.map_tree[ 'ssub_' + ssub_tree.id() ] =  node;
+            if (params) {
+                for (var i in params) {
+                    node.setAttribute(i,params[i]);
+                }
+            }
+            if (ssub_tree.distributions() || ssub_tree.scaps() || ssub_tree.issuances()) {
+                //did this support a later typo? obj.map_sdist[ 'ssub_' + ssub_tree.id() ] = ssub_tree;
+                node.setAttribute('container','true');
+            }
+            if (document.getElementById('show_groups').checked) {
+                node.setAttribute('open','true');
+                obj.funcs.push( function() { obj.on_select_ssub( ssub_tree.id(), true ); } );
+            }
+            var sdist_group_node_data = {
+                'row' : {
+                    'my' : {
+                        'label' : 'Distributions',
+                    }
+                },
+                'retrieve_id' : 'sdist-group_' + ssub_tree.id(),
+                'node' : node,
+                'to_bottom' : true,
+                'no_auto_select' : true,
+            };
+            nparams = obj.list.append(sdist_group_node_data);
+            obj.map_tree[ 'ssub_sdist_group_' + ssub_tree.id() ] =  nparams.my_node;
+
+            var siss_group_node_data = {
+                'row' : {
+                    'my' : {
+                        'label' : 'Issuances',
+                    }
+                },
+                'retrieve_id' : 'siss-group_' + ssub_tree.id(),
+                'node' : node,
+                'to_bottom' : true,
+                'no_auto_select' : true,
+            };
+            nparams = obj.list.append(siss_group_node_data);
+            obj.map_tree[ 'ssub_siss_group_' + ssub_tree.id() ] =  nparams.my_node;
+
+            var scap_group_node_data = {
+                'row' : {
+                    'my' : {
+                        'label' : 'Captions/Patterns',
+                    }
+                },
+                'retrieve_id' : 'scap-group_' + ssub_tree.id(),
+                'node' : node,
+                'to_bottom' : true,
+                'no_auto_select' : true,
+            };
+            nparams = obj.list.append(scap_group_node_data);
+            obj.map_tree[ 'ssub_scap_group_' + ssub_tree.id() ] =  nparams.my_node;
+        } catch(E) {
+            dump(E+'\n');
+            alert(E);
+        }
+    },
+
+    'append_member' : function( item, ssub_tree, attributes, type ) {
+        var obj = this;
+        try {
+            if (obj.map_tree[ type + '_' + item.id() ]) {
+                var x = obj.map_tree[ type + '_' + item.id() ];
+                if (attributes) {
+                    for (var i in attributes) {
+                        x.setAttribute(i,attributes[i]);
+                    }
+                }
+                return x;
+            }
+
+            var parent_node = obj.map_tree[ 'ssub_' + type + '_group_' + ssub_tree.id() ];
+            var data = {
+                'row' : {
+                    'my' : {
+                        'aou' : obj.data.hash.aou[ ssub_tree.owning_lib() ],
+                        'ssub' : ssub_tree,
+                        'subscription_count' : '',
+                        //'distribution_count' : '',
+                    }
+                },
+                'retrieve_id' : type + '_' + item.id(),
+                'node' : parent_node,
+                'to_bottom' : true,
+                'no_auto_select' : true,
+            };
+            data['row']['my'][type] = item; // TODO: future optimization: get only the IDs of these leaves, then fetch the full row in 'retrieve_row'
+            var nparams = obj.list.append(data);
+            var node = nparams.my_node;
+            obj.map_tree[ type + '_' + item.id() ] =  node;
+            if (attributes) {
+                for (var i in attributes) {
+                    node.setAttribute(i,attributes[i]);
+                }
+            }
+
+        } catch(E) {
+            dump(E+'\n');
+            alert(E);
+        }
+    },
+
+    'list_init' : function( params ) {
+
+        try {
+            netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+            var obj = this;
+            
+            JSAN.use('circ.util');
+            var columns = [
+                {
+                    'id' : 'tree_location',
+                    'label' : document.getElementById('catStrings').getString('staff.cat.copy_browser.list_init.tree_location'),
+                    'flex' : 1, 'primary' : true, 'hidden' : false, 
+                    'render' : function(my) { return my.sdist ? my.sdist.label() : my.siss ? my.siss.label() : my.scap ? 'C/P : #' + my.scap.id() : my.ssub ? 'Subscription : #' + my.ssub.id() : my.aou ? my.aou.shortname() + " : " + my.aou.name() : my.label ? my.label : "???"; },
+                },
+                {
+                    'id' : 'subscription_count',
+                    'label' : 'Subscriptions',
+                    'flex' : 0, 'primary' : false, 'hidden' : false, 
+                    'render' : function(my) { return my.subscription_count; },
+                },
+                /*{
+                    'id' : 'distribution_count',
+                    'label' : 'Members',
+                    'flex' : 0,
+                    'primary' : false, 'hidden' : false, 
+                    'render' : function(my) { return my.distribution_count; },
+                },*/
+            ];
+            JSAN.use('util.list'); obj.list = new util.list('subs_tree');
+            obj.list.init(
+                {
+                    'no_auto_select' : true,
+                    'columns' : columns,
+                    'map_row_to_columns' : circ.util.std_map_row_to_columns(' '),
+                    'retrieve_row' : function(params) {
+
+                        var row = params.row;
+                        obj.funcs.push(
+                            function() {
+
+                                if (typeof params.on_retrieve == 'function') {
+                                    params.on_retrieve(row);
+                                }
+
+                            }
+                        );
+
+                        return row;
+                    },
+                    'on_click' : function(ev) {
+                        netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect UniversalBrowserRead');
+                        var row = {}; var col = {}; var nobj = {};
+                        obj.list.node.treeBoxObject.getCellAt(ev.clientX,ev.clientY,row,col,nobj); 
+                        if ((row.value == -1)||(nobj.value != 'twisty')) { return; } // on_click runs for twistys only
+
+                        var node = obj.list.node.contentView.getItemAtIndex(row.value);
+                        var list = [ node.getAttribute('retrieve_id') ];
+                        if (typeof obj.on_select == 'function') {
+                            obj.on_select(list,true);
+                        }
+                        if (typeof window.xulG == 'object' && typeof window.xulG.on_select == 'function') {
+                            window.xulG.on_select(list);
+                        }
+                    },
+                    'on_select' : function(ev) {
+                        JSAN.use('util.functional');
+                        
+                        // get the actual node clicked to determine which editor to use
+                        if (obj.list.node.view.selection.currentIndex > -1) {
+                            var node = obj.list.node.contentView.getItemAtIndex(obj.list.node.view.selection.currentIndex);
+                            obj.focused_node_retrieve_id = node.getAttribute('retrieve_id');
+                        }
+
+                        var sel = obj.list.retrieve_selection();
+                        obj.controller.view.sel_clip.disabled = sel.length < 1;
+                        obj.sel_list = util.functional.map_list(
+                            sel,
+                            function(o) { return o.getAttribute('retrieve_id'); }
+                        );
+                        obj.toggle_actions();
+                        if (typeof obj.on_select == 'function') {
+                            obj.on_select(obj.sel_list);
+                        }
+                        if (typeof window.xulG == 'object' && typeof window.xulG.on_select == 'function') {
+                            window.xulG.on_select(obj.sel_list);
+                        }
+                    },
+                }
+            );
+
+            obj.controller.render();
+
+        } catch(E) {
+            this.error.sdump('D_ERROR','serial/manage_subs.list_init: ' + E + '\n');
+            alert(E);
+        }
+    },
+
+    'toggle_actions' : function() {
+        var obj = this;
+        try {
+            var found_aou = false; var found_ssub = false; var found_sdist = false; var found_siss = false; var found_scap = false; var found_sdist_group = false; var found_siss_group = false; var found_scap_group = false;
+            for (var i = 0; i < obj.sel_list.length; i++) {
+                var type = obj.sel_list[i].split(/_/)[0];
+                switch(type) {
+                    case 'aou' : 
+                        found_aou = true; 
+                    break;
+                    case 'ssub' : found_ssub = true; break;
+                    case 'sdist' : found_sdist = true; break;
+                    case 'siss' : found_siss = true; break;
+                    case 'scap' : found_scap = true; break;
+                    case 'sdist-group' : found_sdist_group = true; break;
+                    case 'siss-group' : found_siss_group = true; break;
+                    case 'scap-group' : found_scap_group = true; break;
+                }
+            }
+            obj.controller.view.cmd_add_sdist.setAttribute('disabled','true');
+            obj.controller.view.cmd_add_siss.setAttribute('disabled','true');
+            obj.controller.view.cmd_add_scap.setAttribute('disabled','true');
+            obj.controller.view.cmd_make_predictions.setAttribute('disabled','true');
+            obj.controller.view.cmd_delete_sdist.setAttribute('disabled','true');
+            obj.controller.view.cmd_delete_siss.setAttribute('disabled','true');
+            obj.controller.view.cmd_delete_scap.setAttribute('disabled','true');
+            obj.controller.view.cmd_add_subscriptions.setAttribute('disabled','true');
+            obj.controller.view.cmd_mark_library.setAttribute('disabled','true');
+            obj.controller.view.cmd_delete_ssub.setAttribute('disabled','true');
+            obj.controller.view.cmd_mark_subscription.setAttribute('disabled','true');
+            obj.controller.view.cmd_transfer_subscription.setAttribute('disabled','true');
+            obj.controller.view.cmd_transfer_sdists.setAttribute('disabled','true');
+            if (found_aou) {
+                obj.controller.view.cmd_add_subscriptions.setAttribute('disabled','false');
+                obj.controller.view.cmd_mark_library.setAttribute('disabled','false');
+            }
+            if (found_ssub) {
+                obj.controller.view.cmd_delete_ssub.setAttribute('disabled','false');
+                obj.controller.view.cmd_mark_subscription.setAttribute('disabled','false');
+                obj.controller.view.cmd_add_sdist.setAttribute('disabled','false');
+                obj.controller.view.cmd_add_siss.setAttribute('disabled','false');
+                obj.controller.view.cmd_add_scap.setAttribute('disabled','false');
+                obj.controller.view.cmd_transfer_subscription.setAttribute('disabled','false');
+                obj.controller.view.cmd_make_predictions.setAttribute('disabled','false');
+            }
+            if (found_sdist_group) {
+                obj.controller.view.cmd_add_sdist.setAttribute('disabled','false');
+            }
+            if (found_siss_group) {
+                obj.controller.view.cmd_add_siss.setAttribute('disabled','false');
+            }
+            if (found_scap_group) {
+                obj.controller.view.cmd_add_scap.setAttribute('disabled','false');
+            }
+            if (found_sdist) {
+                obj.controller.view.cmd_delete_sdist.setAttribute('disabled','false');
+                obj.controller.view.cmd_transfer_sdists.setAttribute('disabled','false');
+            }
+            if (found_siss) {
+                obj.controller.view.cmd_delete_siss.setAttribute('disabled','false');
+            }
+            if (found_scap) {
+                obj.controller.view.cmd_delete_scap.setAttribute('disabled','false');
+            }
+        } catch(E) {
+            obj.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.actions.error'),E);
+        }
+    },
+
+    'refresh_list' : function() { 
+        try {
+            var obj = this;
+            obj.list.clear();
+            obj.map_tree = {};
+            obj.map_ssub = {};
+            obj.map_sdist = {};
+            obj.map_siss = {};
+            obj.map_scap = {};
+            obj.org_ids = obj.network.simple_request('FM_SSUB_AOU_IDS_RETRIEVE_VIA_RECORD_ID.authoritative',[ obj.docid ]);
+            if (typeof obj.org_ids.ilsevent != 'undefined') throw(obj.org_ids);
+            JSAN.use('util.functional'); 
+            obj.org_ids = util.functional.map_list( obj.org_ids, function (o) { return Number(o); });
+            /*
+            var org = obj.data.hash.aou[ obj.data.list.au[0].ws_ou() ];
+            obj.show_libs( org );
+            */
+            obj.show_my_libs( document.getElementById('lib_menu').value );
+        } catch(E) {
+            this.error.standard_unexpected_error_alert(document.getElementById('catStrings').getString('staff.cat.copy_browser.refresh_list.error'),E);
+        }
+    },
+};
+
+dump('exiting serial/manage_subs.js\n');
diff --git a/Open-ILS/xul/staff_client/server/serial/manage_subs.xul b/Open-ILS/xul/staff_client/server/serial/manage_subs.xul
new file mode 100644 (file)
index 0000000..f2212a1
--- /dev/null
@@ -0,0 +1,106 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Manage Subscriptions Overlay -->
+<!--
+vim:noet:sw=4:ts=4:
+-->
+<!DOCTYPE overlay PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<?xul-overlay href="/xul/server/serial/ssub_editor.xul"?>
+<?xul-overlay href="/xul/server/serial/sdist_editor.xul"?>
+<?xul-overlay href="/xul/server/serial/siss_editor.xul"?>
+<?xul-overlay href="/xul/server/serial/scap_editor.xul"?>
+<overlay id="serial_manage_subs_overlay" 
+       xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+            <tab id="serial_manage_subs_tab" label="Subscriptions" oncommand="manage_subs_init()"/>
+            <tabpanel id="serial_manage_subs" orient="vertical" flex="1">
+                 <script>
+                    <![CDATA[
+                        manage_subs_inited = 0;
+                        function manage_subs_init() {
+                            if (manage_subs_inited) {
+                                return;
+                            }
+                            try {
+                                netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+                                if (typeof JSAN == 'undefined') { 
+                                    throw( document.getElementById("commonStrings").getString('common.jsan.missing') );
+                                }
+                                JSAN.errorLevel = "die"; // none, warn, or die
+                                JSAN.addRepository('/xul/server/');
+                                JSAN.use('util.error'); g.error = new util.error();
+                                g.error.sdump('D_TRACE','manage_subs_init() for serial/manage_subs.xul');
+                                JSAN.use('serial.manage_subs'); g.manage_subs = new serial.manage_subs();
+
+                                g.manage_subs.init( { 'docid' : xul_param('docid') } );
+                                manage_subs_inited = 1;
+
+                            } catch(E) {
+                                var err_msg = document.getElementById("commonStrings").getFormattedString('common.exception', ['serial/manage_subs.xul', E]);
+                                try { g.error.sdump('D_ERROR',err_msg); } catch(E) { dump(err_msg); }
+                                alert(err_msg);
+                            }
+                        }
+
+                    ]]>
+                </script>
+
+                <popupset>
+                    <popup id="serial_manage_subs_popup">
+                        <menuitem command="cmd_add_subscriptions" label="Add Subscription"/>
+                        <menuitem command="cmd_add_sdist" label="Add Distribution"/>
+                        <menuitem command="cmd_add_siss" label="Add Issuance"/>
+                        <menuitem command="cmd_add_scap" label="Add Caption/Pattern"/>
+                        <menuseparator/>
+                        <menuitem command="cmd_make_predictions" label="Make Predictions"/>
+                        <menuseparator/>
+                        <menuitem command="cmd_delete_ssub" label="Delete Subscription"/>
+                        <menuitem command="cmd_delete_sdist" label="Delete Distribution"/>
+                        <menuitem command="cmd_delete_siss" label="Delete Issuance"/>
+                        <menuitem command="cmd_delete_scap" label="Delete Caption/Pattern"/>
+                    </popup>
+                </popupset>
+
+                <hbox flex="1">
+                    <vbox flex="1">
+                        <hbox id="serial_sub_lib_menu"/>
+                        <hbox>
+                            <checkbox id="show_ssubs" label="Show Subs." />
+                            <checkbox id="show_groups" label="Show Groups" />
+                            <button id="serial_sub_add_button" label="&staff.cat.copy_browser.holdings_maintenance.refresh_button.label;" command="cmd_refresh_list" />
+                            <spacer flex="1"/>
+                            <menubar>
+                                <menu label="Actions for Selected Row">
+                                    <menupopup>
+                                        <menuitem command="cmd_add_subscriptions" label="Add Subscription"/>
+                                        <menuitem command="cmd_add_sdist" label="Add Distribution"/>
+                                        <menuitem command="cmd_add_siss" label="Add Issuance"/>
+                                        <menuitem command="cmd_add_scap" label="Add Caption/Pattern"/>
+                                        <menuseparator/>
+                                        <menuitem command="cmd_make_predictions" label="Make Predictions"/>
+                                        <menuseparator/>
+                                        <menuitem command="cmd_delete_ssub" label="Delete Subscription"/>
+                                        <menuitem command="cmd_delete_sdist" label="Delete Distribution"/>
+                                        <menuitem command="cmd_delete_siss" label="Delete Issuance"/>
+                                        <menuitem command="cmd_delete_scap" label="Delete Caption/Pattern"/>
+                                    </menupopup>
+                                </menu>
+                            </menubar>
+                        </hbox>
+                        <tree id="subs_tree" flex="15" enableColumnDrag="true" context="serial_manage_subs_popup"/>
+                    </vbox>
+                    <splitter state="open" collapse="before" resizebefore="closest" resizeafter="farthest"/>
+                    <deck id="serial_manage_subs_editor_deck" flex="20">
+                        <description value="Please select an object to edit"/>
+                        <vbox id="serial_ssub_editor_panel" />
+                        <vbox id="serial_sdist_editor_panel" />
+                        <vbox id="serial_siss_editor_panel" />
+                        <vbox id="serial_scap_editor_panel" />
+                    </deck>
+                </hbox>
+            </tabpanel>
+
+</overlay>
diff --git a/Open-ILS/xul/staff_client/server/serial/notes.xul b/Open-ILS/xul/staff_client/server/serial/notes.xul
new file mode 100644 (file)
index 0000000..8ec3eaf
--- /dev/null
@@ -0,0 +1,227 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Serial Notes -->
+<!--
+ vim:noet:sw=4:ts=4
+-->
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- STYLESHEETS -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://open_ils_staff_client/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/patron_display.css" type="text/css"?>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- LOCALIZATION -->
+<!DOCTYPE window PUBLIC "" ""[
+       <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- OVERLAYS -->
+<?xul-overlay href="/xul/server/OpenILS/util_overlay.xul"?>
+
+<window id="notes_win" width="700" height="550"
+       onload="try{ my_init(); font_helper(); } catch(E) { alert(E); }"
+       xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+       <!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+       <!-- BEHAVIOR -->
+        <script type="text/javascript">var myPackageDir = 'open_ils_staff_client'; var IAMXUL = true; var g = {};</script>
+        <scripts id="openils_util_scripts"/>
+
+       <script type="text/javascript" src="/xul/server/main/JSAN.js"/>
+       <script>
+       <![CDATA[
+
+               function $(id) { return document.getElementById(id); }
+
+               function my_init() {
+                       try {
+                               netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+                               if (typeof JSAN == 'undefined') { 
+                                       throw( $("commonStrings").getString('common.jsan.missing') );
+                               }
+                               JSAN.errorLevel = "die"; // none, warn, or die
+                               JSAN.addRepository('/xul/server/');
+
+                               JSAN.use('util.error'); g.error = new util.error();
+                               JSAN.use('util.network'); g.network = new util.network();
+                               JSAN.use('util.date'); JSAN.use('util.money'); 
+                               JSAN.use('OpenILS.data'); g.data = new OpenILS.data(); g.data.init({'via':'stash'});
+
+                               g.error.sdump('D_TRACE','my_init() for notes.xul');
+
+                               g.object_type = xul_param('object_type',{'modal_xulG':true});
+                               g.object_id = xul_param('object_id',{'modal_xulG':true});
+                               g.function_type = xul_param('function_type',{'modal_xulG':true});
+                               g.constructor = xul_param('constructor',{'modal_xulG':true});
+
+                               refresh();
+
+                       } catch(E) {
+                               var err_msg = $("commonStrings").getFormattedString('common.exception', ['serial/notes.xul', js2JSON(E)]);
+                               try { g.error.sdump('D_ERROR',err_msg); } catch(E) { dump(err_msg); }
+                               alert(err_msg);
+                       }
+               }
+
+               function refresh() {
+                       retrieve_notes(); render_notes();
+               }
+
+               function retrieve_notes() {
+                       g.notes = g.network.simple_request('FM_'+g.function_type+'_RETRIEVE_ALL',[ { 'authtoken' : ses(), 'object_id' : g.object_id } ]).reverse();
+               }
+
+               function apply(node,field,value) {
+                       util.widgets.apply(
+                               node,'name',field,
+                               function(n) {
+                                       switch(n.nodeName) {
+                                               case 'description' : n.appendChild( document.createTextNode( value ) ); break;
+                                               case 'label' : n.value = value; break;
+                                               default : n.value = value; break;
+                                       }
+                               }
+                       );
+               }
+
+               function render_notes() {
+                       JSAN.use('util.widgets'); util.widgets.remove_children('notes_panel');
+                       var np = $('notes_panel');
+                               var hbox = document.createElement('hbox'); np.appendChild(hbox);
+                                       var btn = document.createElement('button'); hbox.appendChild(btn);
+                                               btn.setAttribute('label', $('catStrings').getString('staff.cat.copy_notes.render_notes.label'));
+                                               btn.setAttribute('accesskey', $('catStrings').getString('staff.cat.copy_notes.render_notes.accesskey'));
+                                               btn.setAttribute('oncommand','new_note()');
+
+                       for (var i = 0; i < g.notes.length; i++) {
+
+                               /* template */
+                               var node = $('note_template').cloneNode(true); np.appendChild(node); node.hidden = false;
+                               apply(node,'create_date',g.notes[i].create_date().toString().substr(0,10));
+                               util.widgets.apply(node,'name','create_date',
+                                       function(n) {
+                                               n.setAttribute(
+                                                       "tooltiptext",
+                                                       $('catStrings').getString('staff.cat.copy_notes.widgets_apply.note_id') + " "
+                                                       + g.notes[i].id() + " " 
+                                                       + $('catStrings').getString('staff.cat.copy_notes.widgets_apply.creator_id') + " "
+                                                       + g.notes[i].creator()
+                                               );
+                                       }
+                               );
+                               apply(node,'title',g.notes[i].title());
+                               apply(node,'pub',get_bool( g.notes[i].pub() ) ? $('catStrings').getString('staff.cat.copy_notes.widgets.public') : $('catStrings').getString('staff.cat.copy_notes.widgets.private'));
+                               apply(node,'value',g.notes[i].value());
+                               apply(node,'id',g.notes[i].id());
+                               apply(node,'creator',g.notes[i].creator());
+
+                               /* button bar */
+                               var hb = document.createElement('hbox'); np.appendChild(hb);
+                                       var btn1 = document.createElement('button'); hb.appendChild(btn1);
+                                               btn1.setAttribute('label', $('catStrings').getString('staff.cat.copy_notes.delete_note.label'));
+                                               btn1.setAttribute('image',"/xul/server/skin/media/images/up_arrow.gif");
+
+                                               btn1.addEventListener(
+                                                       'command',
+                                                       function(id){ return function() { 
+                                                               var r = g.error.yns_alert(
+                                                                       $('catStrings').getFormattedString('staff.cat.copy_notes.delete_note.prompt.msg', [g.notes[id].title(), g.notes[id].create_date().toString().substr(0,10)]),
+                                                                       $('catStrings').getString('staff.cat.copy_notes.delete_note.prompt.title'),
+                                                                       $('catStrings').getString('staff.cat.copy_notes.delete_note.prompt.yes'),
+                                                                       $('catStrings').getString('staff.cat.copy_notes.delete_note.prompt.no'),
+                                                                       null,
+                                                                       $('commonStrings').getString('common.confirm')
+                                                               ); 
+                                                               if (r == 0) {
+                                                                       g.network.simple_request('FM_'+g.function_type+'_DELETE',[ses(),g.notes[id].id()]);
+                                                                       setTimeout(function() { 
+                                                                               alert($('catStrings').getString('staff.cat.copy_notes.delete_note.success'));
+                                                                               refresh(); },0
+                                                                       );
+                                                               }
+                                                       } }(i),
+                                                       false
+                                               );
+                                       var spacer = document.createElement('spacer'); hb.appendChild(spacer); spacer.flex = 1;
+                                       var btn2 = document.createElement('button'); hb.appendChild(btn2);
+                                               btn2.setAttribute('label', $('catStrings').getString('staff.cat.copy_notes.delete_note.close_window'));
+                                               btn2.setAttribute('oncommand','window.close();');
+                       }
+
+               }
+               
+               function new_note() {
+                       try {
+                               netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect UniversalBrowserWrite");
+                               var xml = '<groupbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" flex="1"> \
+                                       <caption label="' + $('catStrings').getString('staff.cat.copy_notes.new_note.label') + '"/> \
+                                       <grid flex="1"><columns><column/><column flex="1"/></columns> \
+                                               <rows> \
+                                                       <row><label value="' + $('catStrings').getString('staff.cat.copy_notes.new_note.public') + '"/><checkbox id="pub" name="fancy_data" checked="false"/></row> \
+                                                       <row><label value="' + $('catStrings').getString('staff.cat.copy_notes.new_note.title') + '"/><textbox id="title" name="fancy_data" context="clipboard"/></row> \
+                                                       <row><label value="' + $('catStrings').getString('staff.cat.copy_notes.new_note.note') + '"/><textbox multiline="true" id="note" name="fancy_data" context="clipboard"/></row> \
+                                                       <row><spacer/><hbox> \
+                                                               <button label="' + $('catStrings').getString('staff.cat.copy_notes.new_note.cancel.label') + '" name="fancy_cancel" accesskey="' + $('catStrings').getString('staff.cat.copy_notes.new_note.cancel.accesskey') + '"/> \
+                                                               <button label="' + $('catStrings').getString('staff.cat.copy_notes.new_note.add_note.label') + '" accesskey="' + $('catStrings').getString('staff.cat.copy_notes.new_note.add_note.accesskey') + '" name="fancy_submit"/></hbox></row> \
+                                               </rows></grid></groupbox>';
+                               //g.data.init({'via':'stash'});
+                               //g.data.temp_note_xml = xml; g.data.stash('temp_note_xml');
+                               JSAN.use('util.window'); var win = new util.window();
+                               var fancy_prompt_data = win.open(
+                                       urls.XUL_FANCY_PROMPT,
+                                       //+ '?xml_in_stash=temp_note_xml'
+                                       //+ '&focus=' + window.escape('title')
+                                       //+ '&title=' + window.escape('Add Note'),
+                                       'fancy_prompt', 'chrome,resizable,modal,width=700,height=500',
+                                       { 'xml' : xml, 'focus' : 'title', 'title' : $('catStrings').getString('staff.cat.copy_notes.new_note.add_note.label') }
+                               );
+                               //g.data.init({'via':'stash'});
+                               if (fancy_prompt_data.fancy_status == 'complete') {
+                                       //alert(js2JSON(g.data.fancy_prompt_data));
+                                       var note = new g.constructor();
+                                       note.isnew(1);
+                                       note.title( fancy_prompt_data.title );
+                                       note.value( fancy_prompt_data.note );
+                                       note.pub( get_bool( fancy_prompt_data.pub ) ? get_db_true() : get_db_false() );
+                                       note[g.object_type]( g.object_id );
+                                       var r = g.network.simple_request('FM_'+g.function_type+'_CREATE',[ ses(), note ]);
+                                       if (typeof r.ilsevent != 'undefined') throw(r);
+                                       setTimeout(function() {
+                                               alert($('catStrings').getString('staff.cat.copy_notes.new_note.success'));
+                                               refresh();},0
+                                       );
+                               }
+                       } catch(E) {
+                               g.error.standard_unexpected_error_alert($('catStrings').getString('staff.cat.copy_notes.new_note.error'),E);
+                       }
+               }
+
+       ]]>
+       </script>
+
+       <messagecatalog id="catStrings" src="/xul/server/locale/<!--#echo var='locale'-->/cat.properties" />
+       <messagecatalog id="circStrings" src="/xul/server/locale/<!--#echo var='locale'-->/circ.properties" />
+
+       <stack hidden="true" id="note_template" flex="1">
+               <groupbox flex="1" style="background-color: black;"/>
+               <groupbox flex="1" style="background-color: #FFDE00; -moz-border-radius-topright: 35px;" >
+                       <hbox>
+                               <description name="title" style="font-weight: bold"/>
+                               <spacer flex="1"/>
+                               <description name="create_date" style="font-weight: bold"/>
+                               <description name="pub" style="font-weight: bold"/>
+                       </hbox>
+                       <description name="value"/>
+               </groupbox>
+       </stack>
+
+       <vbox flex="1" class="my_overflow" id="notes_panel">
+       </vbox>
+
+
+</window>
+
diff --git a/Open-ILS/xul/staff_client/server/serial/scap_editor.js b/Open-ILS/xul/staff_client/server/serial/scap_editor.js
new file mode 100644 (file)
index 0000000..f76f5f4
--- /dev/null
@@ -0,0 +1,139 @@
+dump('entering serial/scap_editor.js\n');
+// vim:noet:sw=4:ts=4:
+
+JSAN.use('serial.editor_base');
+
+if (typeof serial == 'undefined') serial = {};
+serial.scap_editor = function (params) {
+    try {
+        netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+        JSAN.use('util.error'); this.error = new util.error();
+        JSAN.use('OpenILS.data'); this.data = new OpenILS.data(); this.data.init({'via':'stash'});
+        JSAN.use('util.network'); this.network = new util.network();
+    } catch(E) {
+        dump('serial/scap_editor: ' + E + '\n');
+    }
+
+    /* This keeps track of what fields have been edited for styling purposes */
+    this.changed = {};
+
+    /* This holds the original values for prepopulating the field editors */
+    this.editor_values = {};
+
+};
+
+serial.scap_editor.prototype = {
+    // we could do this with non-standard '__proto__' property instead
+    'editor_base_init' : serial.editor_base.editor_base_init,
+    'editor_base_apply' : serial.editor_base.editor_base_apply,
+    'editor_base_save' : serial.editor_base.editor_base_save,
+
+    'fm_type' : 'scap',
+    'fm_type_plural' : 'scaps',
+
+    'init' : function (params) {
+        var obj = this;
+
+        params.retrieve_function = 'FM_SCAP_BATCH_RETRIEVE.authoritative';
+
+        obj.editor_base_init(params);
+
+        /* Do it */
+        obj.summarize( obj.scaps );
+        obj.render();
+    },
+
+    /******************************************************************************************************/
+    /* Restore backup copies */
+
+    'reset' :  serial.editor_base.editor_base_reset,
+
+    /******************************************************************************************************/
+    /* Apply a value to a specific field on all the copies being edited */
+
+    'apply' : function(field,value) {
+        var obj = this;
+
+        obj.editor_base_apply(field, value);
+    },
+
+    /******************************************************************************************************/
+
+    'init_panes' : function () {
+        var obj = this;
+        obj.panes_and_field_names = {
+
+        /* These get shown in the left panel */
+        'scap_editor_left_pane' :
+        [
+            [
+                'ID',
+                { 
+                    render: '"ID : " + fm.id();', 
+                    //input: 'c = function(v){ obj.apply("distribution",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+
+                }
+            ],
+            [
+                $('catStrings').getString('staff.cat.copy_editor.field.creation_date.label'),
+                {
+                    render: 'fm.create_date() == null ? "<Unset>" : util.date.formatted_date( fm.create_date(), "%F");',
+                }
+            ],
+            [
+                'Type',
+                {
+                    render: 'fm.type();',
+                    input: 'c = function(v){ obj.apply("type",v); if (typeof post_c == "function") post_c(v); }; x = util.widgets.make_menulist( [ ["basic", "basic"], ["index", "index"], ["supplement", "supplement"] ] ); x.setAttribute("value",obj.editor_values.type); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'type'
+                }
+            ],
+            [
+                'Active?',
+                {
+                    render: 'fm.active() == null ? $("catStrings").getString("staff.cat.copy_editor.field.unset_or_null") : ( get_bool( fm.active() ) ? $("catStrings").getString("staff.cat.copy_editor.field.circulate.yes_or_true") : $("catStrings").getString("staff.cat.copy_editor.field.circulate.no_or_false") )',
+                    input: 'c = function(v){ obj.apply("active",v); if (typeof post_c == "function") post_c(v); }; x = util.widgets.make_menulist( [ [ $("catStrings").getString("staff.cat.copy_editor.field.circulate.yes_or_true"), get_db_true() ], [ $("catStrings").getString("staff.cat.copy_editor.field.circulate.no_or_false"), get_db_false() ] ] ); x.setAttribute("value",obj.editor_values.active); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'active',
+                    dropdown_key: 'fm.active()'
+                }
+            ],
+            [
+                'Pattern Code (temporary)',
+                { 
+                    render: 'fm.pattern_code() == null ? "" : fm.pattern_code();',
+                    input: 'c = function(v){ obj.apply("pattern_code",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.pattern_code); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'pattern_code'
+                }
+            ]
+        ]
+
+        };
+    },
+
+    /******************************************************************************************************/
+    /* This loops through all our fieldnames and all the copies, tallying up counts for the different values */
+
+    'summarize' :  serial.editor_base.editor_base_summarize,
+
+    /******************************************************************************************************/
+    /* Display the summarized data and inputs for editing */
+
+    'render' :  serial.editor_base.editor_base_render,
+
+    /******************************************************************************************************/
+    /* This actually draws the change button and input widget for a given field */
+    'render_input' : serial.editor_base.editor_base_render_input,
+
+    /******************************************************************************************************/
+    /* save the caption/patterns */
+
+    'save' : function() {
+        var obj = this;
+        obj.editor_base_save('open-ils.serial.caption_and_pattern.batch.update');
+    },
+
+    /******************************************************************************************************/
+    'save_attributes' : serial.editor_base.editor_base_save_attributes
+};
+
+dump('exiting serial/scap_editor.js\n');
diff --git a/Open-ILS/xul/staff_client/server/serial/scap_editor.xul b/Open-ILS/xul/staff_client/server/serial/scap_editor.xul
new file mode 100644 (file)
index 0000000..9c272f7
--- /dev/null
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Caption/Pattern Editor Overlay -->
+
+<!-- LOCALIZATION -->
+<!DOCTYPE overlay PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<overlay id="serial_scap_editor_panel_overlay" 
+       xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+       <vbox flex="1" id="serial_scap_editor_panel" class="my_overflow">
+        <vbox id="brief_display_box"/>
+
+               <hbox flex="1" style="overflow: auto">
+                       <vbox flex="1">
+                               <label value="Caption and Pattern" style="font-weight: bold; font-size: large"/>
+                               <vbox id="scap_editor_left_pane" flex="1"/>
+                       </vbox>
+        </hbox>
+
+               <hbox id="scap_editor_nav">
+                       <spacer flex="1"/>
+                       <button id="scap_save" label="&staff.serial.scap_editor.modify;" hidden="true" accesskey="&staff.serial.scap_editor.modify.accesskey;" oncommand="g.manage_subs.scap_editor.save()" />
+                       <!--<button id="cancel" label="&staff.cat.copy_editor.cancel.label;" accesskey="&staff.cat.copy_editor.cancel.accesskey;" oncommand="window.close();"/>-->
+               </hbox>
+
+               <spacer/>
+       </vbox>
+
+</overlay>
+
diff --git a/Open-ILS/xul/staff_client/server/serial/sdist_editor.js b/Open-ILS/xul/staff_client/server/serial/sdist_editor.js
new file mode 100644 (file)
index 0000000..3272a33
--- /dev/null
@@ -0,0 +1,400 @@
+dump('entering serial/sdist_editor.js\n');
+// vim:noet:sw=4:ts=4:
+
+JSAN.use('serial.editor_base');
+
+if (typeof serial == 'undefined') serial = {};
+serial.sdist_editor = function (params) {
+    try {
+        netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+        JSAN.use('util.error'); this.error = new util.error();
+        JSAN.use('OpenILS.data'); this.data = new OpenILS.data(); this.data.init({'via':'stash'});
+        JSAN.use('util.network'); this.network = new util.network();
+    } catch(E) {
+        dump('serial/sdist_editor: ' + E + '\n');
+    }
+
+    /* This keeps track of what fields have been edited for styling purposes */
+    this.changed = {};
+
+    /* This holds the original values for prepopulating the field editors */
+    this.editor_values = {};
+
+    // setup sre arrays
+    this.sre_id_map = {};
+    this.sres_ou_map = {};
+    var parent_g = window.parent.g;
+    if (parent_g.mfhd) {
+        var mfhd_details = parent_g.mfhd.details;
+        for (var i = 0; i < mfhd_details.length; i++) {
+            var mfhd_detail = mfhd_details[i];
+            mfhd_detail.label = mfhd_detail.label + ' (' + (mfhd_detail.entryNum + 1) + ')';
+            var sre_id = mfhd_detail.id;
+            var org_unit_id = mfhd_detail.owning_lib;
+            this.sre_id_map[sre_id] = mfhd_detail;
+            if (!this.sres_ou_map[org_unit_id]) {
+                this.sres_ou_map[org_unit_id] = [];
+            }
+            this.sres_ou_map[org_unit_id].push(mfhd_detail);
+        }
+    }
+};
+
+serial.sdist_editor.prototype = {
+    // we could do this with non-standard '__proto__' property instead
+    'editor_base_init' : serial.editor_base.editor_base_init,
+    'editor_base_apply' : serial.editor_base.editor_base_apply,
+    'editor_base_save' : serial.editor_base.editor_base_save,
+
+    'fm_type' : 'sdist',
+    'fm_type_plural' : 'sdists',
+    'can_have_notes' : true,
+
+    'init' : function (params) {
+        var obj = this;
+
+        params.retrieve_function = 'FM_SDIST_FLESHED_BATCH_RETRIEVE.authoritative';
+
+        obj.editor_base_init(params);
+
+        obj.multi_org_edit = false;
+        var org_unit = obj.sdists[0].holding_lib();
+        for (var i = 1; i < obj.sdists.length; i++) {
+            if (obj.sdists[i].holding_lib() != org_unit) {
+                obj.multi_org_edit = true;
+                break;
+            }
+        }        
+
+        /* Do it */
+        obj.summarize( obj.sdists );
+        obj.render();
+    },
+
+    /******************************************************************************************************/
+    /* Restore backup copies */
+
+    'reset' :  serial.editor_base.editor_base_reset,
+
+    /******************************************************************************************************/
+    /* Apply a value to a specific field on all the copies being edited */
+
+    'apply' : function(field,value) {
+        var obj = this;
+
+        // null out call number if the holding lib is changed
+        obj.holding_lib_changed = (field == 'holding_lib');
+        var loop_func = function(sdist) {
+            if (obj.holding_lib_changed) {
+                sdist['bind_call_number'](null);
+                obj.changed['Bind Call Number'] = true;
+                sdist['receive_call_number'](null);
+                obj.changed['Receive Call Number'] = true;
+                sdist['bind_unit_template'](null);
+                obj.changed['Bind Unit Template'] = true;
+                sdist['receive_unit_template'](null);
+                obj.changed['Receive Unit Template'] = true;
+                sdist['record_entry'](null);
+                obj.changed['Legacy Record Entry'] = true;
+            }
+        }
+        obj.editor_base_apply(field, value, loop_func);
+        obj.holding_lib_changed = false;
+    },
+
+    /******************************************************************************************************/
+
+    'render_call_number' : function(cn) {
+        var obj = this;
+        if (cn === null) {
+            return "<Unset>";
+        } else if (typeof cn != 'object') {
+            return obj.acn_label_map[cn];
+        } else {
+            return cn.label()
+        }
+    },
+
+    'render_unit_template' : function(ut) {
+        var obj = this;
+        if (ut === null) {
+            return "<Unset>";
+        } else if (typeof ut != 'object') {
+            return obj.act_name_map[ut];
+        } else {
+            return ut.name()
+        }
+    },
+
+    'render_record_entry' : function(sre) {
+        var obj = this;
+        var sre_id;
+        if (sre === null) {
+            return "<Unset>";
+        } else if (typeof sre != 'object') {
+            sre_id = sre;
+        } else {
+            sre_id = sre.id();
+        }
+        return obj.sre_id_map[sre_id].label;
+    },
+
+    'init_panes' : function () {
+        var obj = this;
+        obj.panes_and_field_names = {
+
+        /* These get shown in the left panel */
+        'sdist_editor_left_pane' :
+        [
+            [
+                'ID',
+                { 
+                    render: '"ID : " + fm.id();', 
+                    //input: 'c = function(v){ obj.apply("distribution",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+
+                }
+            ],
+            [
+                'Label',
+                { 
+                    render: 'fm.label() == null ? "" : fm.label();',
+                    input: 'c = function(v){ obj.apply("label",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.label); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'label'
+                }
+            ],
+            [
+                'Unit Label Prefix',
+                {
+                    render: 'fm.unit_label_prefix() == null ? "" : fm.unit_label_prefix();',
+                    input: 'c = function(v){ obj.apply("unit_label_prefix",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.unit_label_prefix); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'unit_label_prefix'
+                }
+            ],
+            [
+                'Unit Label Suffix',
+                { 
+                    render: 'fm.unit_label_suffix() == null ? "" : fm.unit_label_suffix();',
+                    input: 'c = function(v){ obj.apply("unit_label_suffix",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.unit_label_suffix); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'unit_label_suffix'
+                }
+            ],
+        ],
+        /* These get shown in the right panel */
+            'sdist_editor_right_pane' :
+        [
+            [
+                'Holding Lib',
+                {
+                    render: 'typeof fm.holding_lib() == "object" ? fm.holding_lib().shortname() : obj.data.hash.aou[ fm.holding_lib() ].shortname()',
+                    input: 'c = function(v){ if (obj.editor_values.holding_lib != v) obj.apply("holding_lib",v); if (typeof post_c == "function") post_c(v); }; x = util.widgets.make_menulist( util.functional.map_list( obj.data.list.aou, function(myobj) { var sname = myobj.shortname(); for (i = sname.length; i < 20; i++) sname += " "; return [ myobj.name() ? sname + " " + myobj.name() : myobj.shortname(), myobj.id(), ( ! get_bool( obj.data.hash.aout[ myobj.ou_type() ].can_have_vols() ) ), ( obj.data.hash.aout[ myobj.ou_type() ].depth() * 2), ]; }), obj.data.list.au[0].ws_ou()); x.setAttribute("value",obj.editor_values.holding_lib); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'holding_lib',
+                    dropdown_key: 'typeof fm.holding_lib() == "object" ? fm.holding_lib().id() : fm.holding_lib()',
+                }
+            ],
+        ],
+        /* These get shown in the right 'library-specific-options' panel */
+        'sdist_editor_lso_pane' :
+        [
+            [
+                'Legacy Record Entry',
+                {
+                    render: 'obj.render_record_entry(fm.record_entry())',
+                    input: 'if(!obj.multi_org_edit) { c = function(v){ obj.apply("record_entry",v); if (typeof post_c == "function") post_c(v); }; x = util.widgets.make_menulist( util.functional.map_list( obj.get_sre_details_list(), function(obj) { return [ obj.label, obj.id ]; }).sort()); x.setAttribute("value",obj.editor_values.record_entry); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false); }',
+                    value_key: 'record_entry',
+                    dropdown_key: 'fm.record_entry() == null ? null : typeof fm.record_entry() == "object" ? fm.record_entry().id() : fm.record_entry()'
+                }
+            ],
+            [
+                'Receive Call Number',
+                {
+                    render: 'obj.render_call_number(fm.receive_call_number())',
+                    input: 'if(!obj.multi_org_edit) { c = function(v){ obj.apply("receive_call_number",v); if (typeof post_c == "function") post_c(v); }; x = util.widgets.make_menulist( util.functional.map_list( obj.get_acn_list(), function(obj) { return [ obj.label(), obj.id() ]; }).sort()); x.setAttribute("value",obj.editor_values.receive_call_number); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false); }',
+                    value_key: 'receive_call_number',
+                    dropdown_key: 'fm.receive_call_number() == null ? null : typeof fm.receive_call_number() == "object" ? fm.receive_call_number().id() : fm.receive_call_number()'
+                }
+            ],
+            [
+                'Bind Call Number',
+                {
+                    render: 'obj.render_call_number(fm.bind_call_number())',
+                    input: 'if(!obj.multi_org_edit) { c = function(v){ obj.apply("bind_call_number",v); if (typeof post_c == "function") post_c(v); }; x = util.widgets.make_menulist( util.functional.map_list( obj.get_acn_list(), function(obj) { return [ obj.label(), obj.id() ]; }).sort()); x.setAttribute("value",obj.editor_values.bind_call_number); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false); }',
+                    value_key: 'bind_call_number',
+                    dropdown_key: 'fm.bind_call_number() == null ? null : typeof fm.bind_call_number() == "object" ? fm.bind_call_number().id() : fm.bind_call_number()'
+                }
+            ],
+            [
+                'Receive Unit Template',
+                {
+                    render: 'obj.render_unit_template(fm.receive_unit_template())',
+                    input: 'if(!obj.multi_org_edit) { c = function(v){ obj.apply("receive_unit_template",v); if (typeof post_c == "function") post_c(v); }; x = util.widgets.make_menulist( util.functional.map_list( obj.get_act_list(), function(obj) { return [ obj.name(), obj.id() ]; }).sort()); x.setAttribute("value",obj.editor_values.receive_unit_template); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false); }',
+                    value_key: 'receive_unit_template',
+                    dropdown_key: 'fm.receive_unit_template() == null ? null : typeof fm.receive_unit_template() == "object" ? fm.receive_unit_template().id() : fm.receive_unit_template()'
+                }
+            ],
+            [
+                'Bind Unit Template',
+                {
+                    render: 'obj.render_unit_template(fm.bind_unit_template())',
+                    input: 'if(!obj.multi_org_edit) { c = function(v){ obj.apply("bind_unit_template",v); if (typeof post_c == "function") post_c(v); }; x = util.widgets.make_menulist( util.functional.map_list( obj.get_act_list(), function(obj) { return [ obj.name(), obj.id() ]; }).sort()); x.setAttribute("value",obj.editor_values.bind_unit_template); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false); }',
+                    value_key: 'bind_unit_template',
+                    dropdown_key: 'fm.bind_unit_template() == null ? null : typeof fm.bind_unit_template() == "object" ? fm.bind_unit_template().id() : fm.bind_unit_template()'
+                }
+            ],
+        ],
+
+        };
+    },
+
+    /******************************************************************************************************/
+    /* This loops through all our fieldnames and all the copies, tallying up counts for the different values */
+
+    'summarize' :  serial.editor_base.editor_base_summarize,
+
+    /******************************************************************************************************/
+    /* Display the summarized data and inputs for editing */
+
+    'render' :  serial.editor_base.editor_base_render,
+
+    /******************************************************************************************************/
+    /* This actually draws the change button and input widget for a given field */
+    'render_input' : serial.editor_base.editor_base_render_input,
+
+    /******************************************************************************************************/
+    /* save the distributions */
+
+    'save' : function() {
+        var obj = this;
+        obj.editor_base_save('open-ils.serial.distribution.fleshed.batch.update');
+    },
+
+    /******************************************************************************************************/
+    /* spawn notes interface */
+
+    'notes' : function() {
+        var obj = this;
+        JSAN.use('util.window'); var win = new util.window();
+        win.open(
+            urls.XUL_SERIAL_NOTES, 
+            //+ '?copy_id=' + window.escape(obj.sdists[0].id()),
+            'Distribution Notes','chrome,resizable,modal',
+            { 'object_id' : obj.sdists[0].id(), 'function_type' : 'SDISTN', 'object_type' : 'distribution', 'constructor' : sdistn }
+        );
+    },
+
+    /******************************************************************************************************/
+    'save_attributes' : serial.editor_base.editor_base_save_attributes,
+
+    /******************************************************************************************************/
+    /* This returns a list of sre details appropriate for the distributions being edited */
+
+    'get_sre_details_list' : function() {
+        var obj = this;
+        try {
+            /* we only show this list if dealing with one org_unit, default to first sdist*/
+            var lib_id = typeof obj.sdists[0].holding_lib() == 'object' ? obj.sdists[0].holding_lib().id() : obj.sdists[0].holding_lib();
+            var sre_details_list = obj.sres_ou_map[lib_id];
+            if (sre_details_list == null) {
+                return [];
+            } else {
+                return sre_details_list;
+            }
+        } catch(E) {
+            obj.error.standard_unexpected_error_alert('get_sre_details_list',E);
+            return [];
+        }
+    },
+
+    /******************************************************************************************************/
+    /* This returns a list of acn's appropriate for the distributions being edited */
+
+    'get_acn_list' : function() {
+        var obj = this;
+        try {
+            var lib_id = typeof obj.sdists[0].holding_lib() == 'object' ? obj.sdists[0].holding_lib().id() : obj.sdists[0].holding_lib();
+
+            if (!obj.acn_lists) {
+                obj.acn_lists = {};
+            }
+
+            // return cached version if we have it
+            // TODO: clear cache on holding_lib change? (cannot remember how to reproduce this bug)
+            if (obj.acn_lists[lib_id]) {
+                return obj.acn_lists[lib_id];
+            }
+
+            var acn_list = obj.network.request(
+                'open-ils.pcrud',
+                'open-ils.pcrud.search.acn',
+                [ ses(), {"record" : obj.docid, "owning_lib" : lib_id, "deleted" : 'f' }, {"order_by" : {"acn" : "label"} } ]
+            );
+
+            if (!acn_list) {
+                return [];
+            } else if (!acn_list.length) {
+                acn_list = [acn_list];
+            }
+
+            // build label map
+            obj.acn_label_map = {};
+            for (i = 0; i < acn_list.length; i++) {
+                obj.acn_label_map[acn_list[i].id()] = acn_list[i].label();
+            }
+
+            // cache the list
+            obj.acn_lists[lib_id] = acn_list;
+            return acn_list;
+
+        } catch(E) {
+            obj.error.standard_unexpected_error_alert('get_acn_list',E);
+            return [];
+        }
+    },
+
+    /******************************************************************************************************/
+    /* This returns a list of asset copy templates appropriate for the distributions being edited */
+
+    'get_act_list' : function() {
+        var obj = this;
+        try {
+            /* we only show this list if dealing with one org_unit, default to first sdist*/
+            var lib_id = typeof obj.sdists[0].holding_lib() == 'object' ? obj.sdists[0].holding_lib().id() : obj.sdists[0].holding_lib();
+
+            if (!obj.act_lists) {
+                obj.act_lists = {};
+            }
+
+            // return cached version if we have it
+            if (obj.act_lists[lib_id]) {
+                return obj.act_lists[lib_id];
+            }
+            
+            var act_list = obj.network.request(
+                'open-ils.pcrud',
+                'open-ils.pcrud.search.act',
+                [ ses(), {"owning_lib" : lib_id }, {"order_by" : {"act" : "name"} } ]
+            );
+
+            if (act_list == null) {
+                return [];
+            } else if (!act_list.length) {
+                act_list = [act_list];
+            }
+
+            // build name map
+            obj.act_name_map = {};
+            for (i = 0; i < act_list.length; i++) {
+                obj.act_name_map[act_list[i].id()] = act_list[i].name();
+            }
+
+            // cache the list
+            obj.act_lists[lib_id] = act_list;
+            return act_list;
+        } catch(E) {
+            obj.error.standard_unexpected_error_alert('get_act_list',E);
+            return [];
+        }
+    }
+
+};
+
+dump('exiting serial/sdist_editor.js\n');
diff --git a/Open-ILS/xul/staff_client/server/serial/sdist_editor.xul b/Open-ILS/xul/staff_client/server/serial/sdist_editor.xul
new file mode 100644 (file)
index 0000000..52956ef
--- /dev/null
@@ -0,0 +1,43 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Distribution Editor Overlay -->
+
+<!-- LOCALIZATION -->
+<!DOCTYPE overlay PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<overlay id="serial_sdist_editor_panel_overlay" 
+       xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+       <vbox flex="1" id="serial_sdist_editor_panel" class="my_overflow">
+        <vbox id="brief_display_box"/>
+
+               <hbox flex="1" style="overflow: auto">
+                       <vbox flex="1">
+                               <label value="Distribution" style="font-weight: bold; font-size: large"/>
+                               <vbox id="sdist_editor_left_pane" flex="1"/>
+                       </vbox>
+                       <splitter><grippy /></splitter>
+                       <vbox flex="1">
+                               <vbox id="sdist_editor_right_pane"/>
+                <groupbox>
+                    <caption label="Library Specific Options" />
+                    <description>Note: Changing the 'Holding Lib' will unset all of these values</description>
+                    <vbox id="sdist_editor_lso_pane" flex="1"/>
+                </groupbox>
+                       </vbox>
+               </hbox>
+
+               <hbox id="sdist_editor_nav">
+                       <spacer flex="1"/>
+                       <button id="sdist_notes" label="&staff.serial.sdist_editor.notes;" accesskey="&staff.serial.sdist_editor.notes.accesskey;" oncommand="g.manage_subs.sdist_editor.notes()" />
+                       <button id="sdist_save" label="&staff.serial.sdist_editor.modify;" hidden="true" accesskey="&staff.serial.ssub_editor.modify.accesskey;" oncommand="g.manage_subs.sdist_editor.save()" />
+                       <!--<button id="cancel" label="&staff.cat.copy_editor.cancel.label;" accesskey="&staff.cat.copy_editor.cancel.accesskey;" oncommand="window.close();"/>-->
+               </hbox>
+
+               <spacer/>
+       </vbox>
+
+</overlay>
+
diff --git a/Open-ILS/xul/staff_client/server/serial/select_aou.xul b/Open-ILS/xul/staff_client/server/serial/select_aou.xul
new file mode 100644 (file)
index 0000000..1c2eead
--- /dev/null
@@ -0,0 +1,106 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Select AOU Dialog -->
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- STYLESHEETS -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://open_ils_staff_client/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/cat.css" type="text/css"?>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- LOCALIZATION -->
+<!DOCTYPE window PUBLIC "" ""[
+       <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- OVERLAYS -->
+<?xul-overlay href="/xul/server/OpenILS/util_overlay.xul"?>
+
+<window id="select_aou_win" title="Select Org Unit"
+       onload="try { my_init(); font_helper(); persist_helper(); } catch(E) { alert(E); }" oils_persist="height width"
+       xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+       <!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+       <!-- BEHAVIOR -->
+       <script type="text/javascript">var myPackageDir = 'open_ils_staff_client'; var IAMXUL = true; var g = {};</script>
+       <scripts id="openils_util_scripts"/>
+
+       <script type="text/javascript" src="/xul/server/main/JSAN.js"/>
+       <script>
+       <![CDATA[
+               function $(id) { return document.getElementById(id); }
+               function $c(n) { return document.createElement(n); }
+
+               function my_init() {
+                       try {
+                               netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+                if (typeof JSAN == 'undefined') { throw( $("commonStrings").getString('common.jsan.missing') ); }
+                               JSAN.errorLevel = "die"; // none, warn, or die
+                               JSAN.addRepository('/xul/server/');
+                               JSAN.use('util.error'); g.error = new util.error();
+                               g.error.sdump('D_TRACE','my_init() for serial/select_aou.xul');
+
+                JSAN.use('util.file'); JSAN.use('util.widgets');
+
+                var file; var list_data; var ml;
+
+                file = new util.file('offline_ou_list');
+                if (file._file.exists()) {
+                    list_data = file.get_object(); file.close();
+                    ml = util.widgets.make_menulist( list_data[0], list_data[1] );
+                    ml.setAttribute('id','lib_menu');
+                    document.getElementById('x_lib_menu').appendChild(ml);
+                    ml.addEventListener(
+                        'command',
+                        function(ev) {
+                            //if (document.getElementById('refresh_button')) document.getElementById('refresh_button').focus();
+                            JSAN.use('util.file'); var file = new util.file('mfhd_create_prefs.'+opener.g.data.server_unadorned);
+                            util.widgets.save_attributes(file, { 'lib_menu' : [ 'value' ] });
+                        },
+                        false
+                    );
+                } else {
+                    throw(document.getElementById('catStrings').getString('staff.cat.copy_browser.missing_library') + '\n');  //TODO: different error?
+                }
+
+                file = new util.file('mfhd_create_prefs.'+opener.g.data.server_unadorned);
+                util.widgets.load_attributes(file);
+                ml.value = ml.getAttribute('value');
+            } catch(E) {
+                //TODO: better error
+                g.error.standard_unexpected_error_alert('', E);
+            }
+        }
+
+        g.select_aou = function() {
+            opener.g.data.create_mfhd_aou = $('lib_menu').value;
+            window.close();
+        }
+
+       ]]>
+       </script>
+       
+       <messagecatalog id="catStrings" src="/xul/server/locale/<!--#echo var='locale'-->/cat.properties" />
+
+       <vbox flex="1" style="overflow: auto">
+       <groupbox flex="1">
+<!--TODO: label strings -->
+               <caption label="Select an Org Unit"/>
+               <description id="desc">Please select an Org Unit</description>
+        <hbox id="x_lib_menu"/>
+               <hbox>
+                       <button label="Select"
+                               accesskey="s" oncommand="g.select_aou()"/>
+                       <button label="&staff.cat.record_buckets_quick.cancel.label;"
+                               accesskey="&staff.cat.record_buckets_quick.cancel.accesskey;" oncommand="window.close()"/>
+               </hbox>
+               <hbox>
+               </hbox>
+       </groupbox>
+       </vbox>
+
+</window>
+
diff --git a/Open-ILS/xul/staff_client/server/serial/select_unit.xul b/Open-ILS/xul/staff_client/server/serial/select_unit.xul
new file mode 100644 (file)
index 0000000..9fc93ef
--- /dev/null
@@ -0,0 +1,94 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Select Serial Unit Dialog -->
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- STYLESHEETS -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://open_ils_staff_client/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/cat.css" type="text/css"?>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- LOCALIZATION -->
+<!DOCTYPE window PUBLIC "" ""[
+       <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- OVERLAYS -->
+<?xul-overlay href="/xul/server/OpenILS/util_overlay.xul"?>
+
+<window id="select_serial_unit_win" title="Select Serial Unit"
+       onload="try { my_init(); font_helper(); persist_helper(); } catch(E) { alert(E); }" oils_persist="height width"
+       xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+       <!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+       <!-- BEHAVIOR -->
+       <script type="text/javascript">var myPackageDir = 'open_ils_staff_client'; var IAMXUL = true; var g = {};</script>
+       <scripts id="openils_util_scripts"/>
+
+       <script type="text/javascript" src="/xul/server/main/JSAN.js"/>
+       <script>
+       <![CDATA[
+               function $(id) { return document.getElementById(id); }
+
+               function my_init() {
+                       try {
+                               netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+                if (typeof JSAN == 'undefined') { throw( $("commonStrings").getString('common.jsan.missing') ); }
+                               JSAN.errorLevel = "die"; // none, warn, or die
+                               JSAN.addRepository('/xul/server/');
+                               JSAN.use('util.error'); g.error = new util.error();
+                               g.error.sdump('D_TRACE','my_init() for serial/select_unit.xul');
+
+                JSAN.use('util.widgets');
+
+                var ml;
+                               JSAN.use('util.network'); g.network = new util.network();
+                var robj = g.network.request(
+                    'open-ils.serial',
+                    'open-ils.serial.unit_list.retrieve',
+                    opener.g.serial_items_sdist_ids
+                );
+                if (typeof robj.ilsevent != 'undefined') throw(robj);
+                ml = util.widgets.make_menulist(robj);
+                ml.setAttribute('id','unit_menu');
+                document.getElementById('x_unit_menu').appendChild(ml);
+            } catch(E) {
+                //TODO: better error
+                g.error.standard_unexpected_error_alert('', E);
+            }
+        }
+
+        g.select_unit = function() {
+            var selection = JSON2js($('unit_menu').value);
+            selection.label = $('unit_menu').selectedItem.label;
+            opener.g.serial_items_sunit_select = selection;
+            window.close();
+        }
+
+       ]]>
+       </script>
+       
+       <messagecatalog id="catStrings" src="/xul/server/locale/<!--#echo var='locale'-->/cat.properties" />
+
+       <vbox flex="1" style="overflow: auto">
+       <groupbox flex="1">
+<!--TODO: label strings -->
+               <caption label="Select a Serial Unit"/>
+               <description id="desc">Please select a Serial Unit</description>
+        <hbox id="x_unit_menu"/>
+               <hbox>
+                       <button label="Select"
+                               accesskey="s" oncommand="g.select_unit()"/>
+                       <button label="&staff.cat.record_buckets_quick.cancel.label;"
+                               accesskey="&staff.cat.record_buckets_quick.cancel.accesskey;" oncommand="window.close()"/>
+               </hbox>
+               <hbox>
+               </hbox>
+       </groupbox>
+       </vbox>
+
+</window>
+
diff --git a/Open-ILS/xul/staff_client/server/serial/serctrl_main.xul b/Open-ILS/xul/staff_client/server/serial/serctrl_main.xul
new file mode 100644 (file)
index 0000000..0d9b393
--- /dev/null
@@ -0,0 +1,124 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Serial Control Main -->
+<!--
+vim:noet:sw=4:ts=4:
+-->
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- STYLESHEETS -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://open_ils_staff_client/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/cat.css" type="text/css"?>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- LOCALIZATION -->
+<!DOCTYPE window PUBLIC "" ""[
+<!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- OVERLAYS -->
+<?xul-overlay href="/xul/server/OpenILS/util_overlay.xul"?>
+<?xul-overlay href="/xul/server/serial/manage_items.xul"?>
+<?xul-overlay href="/xul/server/serial/manage_subs.xul"?>
+
+<window id="serial_serctrl_main" 
+       onload="try { my_init(); font_helper(); } catch(E) { alert(E); }"
+       xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+       <!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+       <!-- BEHAVIOR -->
+       <script type="text/javascript">
+               var myPackageDir = 'open_ils_staff_client'; var IAMXUL = true; var g = {};
+       </script>
+       <scripts id="openils_util_scripts"/>
+
+       <script type="text/javascript" src="/xul/server/main/JSAN.js"/>
+
+       <messagecatalog id="catStrings" src="/xul/server/locale/<!--#echo var='locale'-->/cat.properties" />
+       <messagecatalog id="circStrings" src="/xul/server/locale/<!--#echo var='locale'-->/circ.properties" />
+    <messagecatalog id="patronStrings" src="/xul/server/locale/<!--#echo var='locale'-->/patron.properties"/>
+    <messagecatalog id="serialStrings" src="/xul/server/locale/<!--#echo var='locale'-->/serial.properties"/>
+
+       <commandset id="serial_commands">
+               <command id="cmd_add_item"/>
+               <command id="cmd_add_scap"/>
+               <command id="cmd_add_sdist"/>
+               <command id="cmd_add_siss"/>
+               <command id="cmd_add_subscriptions"/>
+               <command id="cmd_broken" />
+               <command id="cmd_clear" />
+               <command id="cmd_edit_items"/>
+        <command id="cmd_edit_sdists"/>
+        <command id="cmd_edit_subscriptions"/>
+               <command id="cmd_edit_sunit"/>
+               <command id="cmd_delete_items"/>
+               <command id="cmd_delete_scap"/>
+               <command id="cmd_delete_sdist"/>
+               <command id="cmd_delete_siss"/>
+        <command id="cmd_delete_ssub"/>
+        <command id="cmd_make_predictions"/>
+        <command id="cmd_mark_library"/>
+        <command id="cmd_mark_subscription"/>
+               <command id="cmd_predict_items"/>
+               <command id="cmd_print_spine_labels"/>
+               <command id="cmd_receive_items"/>
+               <command id="cmd_refresh_list"/>
+               <command id="cmd_replace_barcode"/>
+        <command id="cmd_set_sunit" />
+        <command id="cmd_set_other_sunit" />
+        <command id="cmd_show_all_libs" />
+        <command id="cmd_show_libs_with_distributions" />
+        <command id="cmd_show_my_libs" />
+               <command id="cmd_transfer_items"/>
+        <command id="cmd_transfer_sdists"/>
+        <command id="cmd_transfer_subscription"/>
+               <command id="save_columns" />
+               <command id="sel_clip" />
+               <command id="sel_mark_items_damaged" />
+               <command id="sel_mark_items_missing" />
+        <command id="sel_mark_sdists_damaged" />
+        <command id="sel_mark_sdists_missing" />
+       </commandset>
+
+       <!--<popupset>
+               <popup id="serctrl_main_actions">
+                       <menuitem command="cmd_add_items" label="&staff.cat.copy_browser.actions.cmd_add_items.label;" accesskey="&staff.cat.copy_browser.actions.cmd_add_items.accesskey;"/>
+                       <menuitem command="cmd_edit_items" label="Edit Item Attributes" accesskey="&staff.cat.copy_browser.actions.cmd_edit_items.accesskey;"/>
+                       <menuitem command="cmd_delete_items" label="Delete Item" accesskey="&staff.cat.copy_browser.actions.cmd_delete_items.accesskey;"/>
+                       <menuitem command="sel_clip" label="&staff.cat.copy_browser.actions.sel_clip.label;" accesskey="&staff.cat.copy_browser.actions.sel_clip.accesskey;"/>
+                       <menuseparator/>
+                       <menuitem command="cmd_edit_volumes" label="&staff.cat.copy_browser.actions.cmd_edit_volumes.label;" accesskey="&staff.cat.copy_browser.actions.cmd_edit_volumes.accesskey;"/>
+                       <menuitem command="cmd_mark_volume" label="&staff.cat.copy_browser.actions.cmd_mark_volume.label;" accesskey="&staff.cat.copy_browser.actions.cmd_mark_volume.accesskey;"/>
+                       <menuitem command="cmd_transfer_volume" label="&staff.cat.copy_browser.actions.cmd_transfer_volume.label;" accesskey="&staff.cat.copy_browser.actions.cmd_transfer_volume.accesskey;"/>
+                       <menuitem command="cmd_delete_volumes" label="&staff.cat.copy_browser.actions.cmd_delete_volumes.label;" accesskey=""/>
+                       <menuseparator/>
+                       <menuitem command="save_columns" label="&staff.cat.copy_browser.actions.save_columns.label;"/>
+                       <menuitem command="cmd_refresh_list" label="&staff.cat.copy_browser.actions.cmd_refresh_list.label;" accesskey="&staff.cat.copy_browser.actions.cmd_refresh_list.accesskey;"/>
+               </popup>
+       </popupset> -->
+
+<!--   <groupbox flex="1" class="my_overflow"> -->
+        <tabbox id="serial_tabbox" flex="1" class="my_overflow">
+            <caption label="Serial Control"/>
+            <tabs>
+                <tab label="Items" />
+                <tab label="Units" />
+                <tab label="Distributions" />
+                <tab id="serial_manage_subs_tab" label="Subscriptions" />
+                <tab label="Claims" />
+            </tabs>
+            <tabpanels flex="1">
+                <tabpanel id="serial_manage_items" />
+                <tabpanel id="serial_manage_units"><description>This tab will contain an alternative unit view/editor.</description></tabpanel>
+                <tabpanel id="serial_manage_distributions"><description>This tab will contain a tree of distributions with editors for templates, summaries, and streams.</description></tabpanel>
+                <tabpanel id="serial_manage_subs" />
+                <tabpanel id="serial_manage_claims"><description>This tab will contain a claims interface.</description></tabpanel>
+            </tabpanels>
+        </tabbox>
+<!--   </groupbox> -->
+
+</window>
+
diff --git a/Open-ILS/xul/staff_client/server/serial/siss_editor.js b/Open-ILS/xul/staff_client/server/serial/siss_editor.js
new file mode 100644 (file)
index 0000000..332b9e3
--- /dev/null
@@ -0,0 +1,195 @@
+dump('entering serial/siss_editor.js\n');
+// vim:noet:sw=4:ts=4:
+
+JSAN.use('serial.editor_base');
+
+if (typeof serial == 'undefined') serial = {};
+serial.siss_editor = function (params) {
+    try {
+        netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+        JSAN.use('util.error'); this.error = new util.error();
+        JSAN.use('OpenILS.data'); this.data = new OpenILS.data(); this.data.init({'via':'stash'});
+        JSAN.use('util.network'); this.network = new util.network();
+    } catch(E) {
+        dump('serial/siss_editor: ' + E + '\n');
+    }
+
+    /* This keeps track of what fields have been edited for styling purposes */
+    this.changed = {};
+
+    /* This holds the original values for prepopulating the field editors */
+    this.editor_values = {};
+};
+
+serial.siss_editor.prototype = {
+    // we could do this with non-standard '__proto__' property instead
+    'editor_base_init' : serial.editor_base.editor_base_init,
+    'editor_base_apply' : serial.editor_base.editor_base_apply,
+    'editor_base_save' : serial.editor_base.editor_base_save,
+
+    'fm_type' : 'siss',
+    'fm_type_plural' : 'sisses',
+    'can_have_notes' : true,
+
+    'init' : function (params) {
+        var obj = this;
+        
+        params.retrieve_function = 'FM_SISS_FLESHED_BATCH_RETRIEVE.authoritative';
+
+        obj.editor_base_init(params);
+
+        /* Do it */
+        obj.summarize( obj.sisses );
+        obj.render();
+    },
+
+    /******************************************************************************************************/
+    /* Restore backup copies */
+
+    'reset' : serial.editor_base.editor_base_reset,
+
+    /******************************************************************************************************/
+    /* Apply a value to a specific field on all the copies being edited */
+
+    'apply' : function(field,value) {
+        var obj = this;
+        if (field == 'date_published') {
+            if (value == '') { value = null; }
+        }
+        obj.editor_base_apply(field, value);
+    },
+
+    /******************************************************************************************************/
+    /* Initialize the panes */
+
+    'init_panes' : function () {
+        var obj = this;
+        obj.panes_and_field_names = {
+
+    /* These get shown in the left panel */
+            'siss_editor_left_pane' :
+        [
+            [
+                $('catStrings').getString('staff.cat.copy_editor.field.creation_date.label') + ' ', //adding extra spaces to satisfy summarize uniqueness requirements
+                {
+                    render: 'fm.create_date() == null ? "<Unset>" : util.date.formatted_date( fm.create_date(), "%F");',
+                }
+            ],
+            [
+                $('catStrings').getString('staff.cat.copy_editor.field.creator.label') + ' ',
+                {
+                    render: 'fm.creator().usrname() == null ? "<Unset>" : fm.creator().usrname();',
+                }
+            ],
+            [
+                $('catStrings').getString('staff.cat.copy_editor.field.last_edit_date.label') + ' ',
+                {
+                    render: 'fm.edit_date() == null ? "<Unset>" : util.date.formatted_date( fm.edit_date(), "%F");',
+                }
+            ],
+            [
+                $('catStrings').getString('staff.cat.copy_editor.field.last_editor.label') + ' ',
+                {
+                    render: 'fm.editor().usrname() == null ? "<Unset>" : fm.editor().usrname();',
+                }
+            ],
+        ],
+
+        'siss_editor_middle_pane' :
+        [
+/*rjs7 don't think we need these anymore            [
+                'Holding Type',
+                {
+                    render: 'fm.holding_type();',
+                    input: 'c = function(v){ obj.apply("holding_type",v); if (typeof post_c == "function") post_c(v); }; x = util.widgets.make_menulist( [ ["basic", "basic"], ["index", "index"], ["supplement", "supplement"] ] ); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                }
+            ],
+            [
+                'Holding Link ID',
+                {
+                    render: 'fm.holding_link_id();',
+                    input: 'c = function(v){ obj.apply("holding_link_id",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.holding_link_id); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'holding_link_id'
+                }
+            ],*/
+            [
+                'Holding Code',
+                {
+                    render: 'fm.holding_code();',
+                    input: 'c = function(v){ obj.apply("holding_code",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.holding_code); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'holding_code'
+                }
+            ],
+            [
+                'Caption/Pattern', //TODO: make this a drop-down selector, perhaps?
+                {
+                    render: 'fm.caption_and_pattern();',
+                    input: 'c = function(v){ obj.apply("caption_and_pattern",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.caption_and_pattern); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'caption_and_pattern'
+                }
+            ],
+        ],
+
+        'siss_editor_right_pane' :
+        [
+            [
+                'Date Published',
+                {
+                    render: 'fm.date_published() == null ? "" : util.date.formatted_date( fm.date_published(), "%F");',
+                    input: 'c = function(v){ obj.apply("date_published",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.date_published); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'date_published'
+                }
+            ],
+            [
+                'Issuance Label',
+                {
+                    render: 'fm.label() == null ? "" : fm.label();',
+                    input: 'c = function(v){ obj.apply("label",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.label); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'label'
+                }
+            ],
+        ],
+
+        };
+    },
+
+    /******************************************************************************************************/
+    /* This loops through all our fieldnames and all the copies, tallying up counts for the different values */
+
+    'summarize' : serial.editor_base.editor_base_summarize,
+
+    /******************************************************************************************************/
+    /* Display the summarized data and inputs for editing */
+
+    'render' : serial.editor_base.editor_base_render,
+
+    /******************************************************************************************************/
+    /* This actually draws the change button and input widget for a given field */
+    'render_input' : serial.editor_base.editor_base_render_input,
+
+    /******************************************************************************************************/
+    /* update the issuances */
+
+    'save' : function() {
+        var obj = this;
+        obj.editor_base_save('open-ils.serial.issuance.fleshed.batch.update');
+    },
+
+    /******************************************************************************************************/
+    /* spawn issuance notes interface */
+
+    'notes' : function() {
+        var obj = this;
+        JSAN.use('util.window'); var win = new util.window();
+        win.open(
+            urls.XUL_SERIAL_NOTES, 
+            'Issuance Notes','chrome,resizable,modal',
+            { 'object_id' : obj.sisses[0].id(), 'function_type' : 'SISSN', 'object_type' : 'issuance', 'constructor' : sissn }
+        );
+    },
+
+    /******************************************************************************************************/
+    'save_attributes' : serial.editor_base.editor_base_save_attributes
+};
+
+dump('exiting serial/siss_editor.js\n');
diff --git a/Open-ILS/xul/staff_client/server/serial/siss_editor.xul b/Open-ILS/xul/staff_client/server/serial/siss_editor.xul
new file mode 100644 (file)
index 0000000..f8be3aa
--- /dev/null
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Issuance Editor Overlay -->
+
+<!-- LOCALIZATION -->
+<!DOCTYPE overlay PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<overlay id="serial_siss_editor_panel_overlay" 
+       xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+       <vbox flex="1" id="serial_siss_editor_panel" class="my_overflow">
+        <vbox id="brief_display_box"/>
+
+               <hbox flex="1" style="overflow: auto">
+                       <vbox flex="1">
+                               <label value="Issuance" style="font-weight: bold; font-size: large"/>
+                               <vbox id="siss_editor_left_pane" flex="1"/>
+                       </vbox>
+                       <splitter><grippy /></splitter>
+                       <vbox flex="1">
+                               <vbox id="siss_editor_middle_pane"/>
+                       </vbox>
+                       <splitter><grippy /></splitter>
+                       <vbox flex="1">
+                               <vbox id="siss_editor_right_pane"/>
+                       </vbox>
+               </hbox>
+
+               <hbox id="siss_editor_nav">
+                       <spacer flex="1"/>
+                       <button id="siss_notes" label="Issuance Notes" accesskey="&staff.cat.copy_editor.copy_notes.accesskey;" oncommand="g.manage_subs.siss_editor.notes()" />
+                       <button id="siss_save" label="Modify Issuances" hidden="true" accesskey="&staff.cat.copy_editor.save.accesskey;" oncommand="g.manage_subs.siss_editor.save()" />
+                       <!--<button id="cancel" label="&staff.cat.copy_editor.cancel.label;" accesskey="&staff.cat.copy_editor.cancel.accesskey;" oncommand="window.close();"/>-->
+               </hbox>
+
+               <spacer/>
+       </vbox>
+
+</overlay>
+
diff --git a/Open-ILS/xul/staff_client/server/serial/sitem_editor.js b/Open-ILS/xul/staff_client/server/serial/sitem_editor.js
new file mode 100644 (file)
index 0000000..7742399
--- /dev/null
@@ -0,0 +1,186 @@
+dump('entering serial/sitem_editor.js\n');
+// vim:noet:sw=4:ts=4:
+
+JSAN.addRepository('/xul/server/');
+JSAN.use('serial.editor_base');
+
+if (typeof serial == 'undefined') serial = {};
+serial.sitem_editor = function (params) {
+    try {
+        netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+        JSAN.use('util.error'); this.error = new util.error();
+        JSAN.use('OpenILS.data'); this.data = new OpenILS.data(); this.data.init({'via':'stash'});
+        JSAN.use('util.network'); this.network = new util.network();
+    } catch(E) {
+        dump('serial/sitem_editor: ' + E + '\n');
+    }
+
+    /* This keeps track of what fields have been edited for styling purposes */
+    this.changed = {};
+
+    /* This holds the original values for prepopulating the field editors */
+    this.editor_values = {};
+};
+
+serial.sitem_editor.prototype = {
+    // we could do this with non-standard '__proto__' property instead
+    'editor_base_init' : serial.editor_base.editor_base_init,
+    'editor_base_apply' : serial.editor_base.editor_base_apply,
+    'editor_base_save' : serial.editor_base.editor_base_save,
+
+    'fm_type' : 'sitem',
+    'fm_type_plural' : 'sitems',
+    'can_have_notes' : true,
+
+    'init' : function (params) {
+        var obj = this;
+
+        params.retrieve_function = 'FM_SITEM_FLESHED_BATCH_RETRIEVE.authoritative';
+
+        obj.editor_base_init(params);
+
+        /* Do it */
+        obj.summarize( obj.sitems );
+        obj.render();
+    },
+
+    /******************************************************************************************************/
+    /* Restore backup copies */
+
+    'reset' :  serial.editor_base.editor_base_reset,
+
+    /******************************************************************************************************/
+    /* Apply a value to a specific field on all the copies being edited */
+
+    'apply' : function(field,value) {
+        var obj = this;
+        JSAN.use('util.date');
+        if (field == 'date_expected') {
+            if (value == '') {
+                alert("Date Expected cannot be unset.");
+                return false;
+            } else if (!util.date.check('YYYY-MM-DD',value)) {
+                alert("Invalid Date");
+                return false;
+            }
+        } else if (field == 'date_received') { // manually unset not allowed
+            if (value == '') {
+                alert("Date Received cannot be manually unset; use 'Reset to Expected' instead.");
+                return false;
+            } else if (!util.date.check('YYYY-MM-DD',value)) {
+                alert("Invalid Date");
+                return false;
+            }
+        }
+        obj.editor_base_apply(field, value);
+        return true;
+    },
+
+    /******************************************************************************************************/
+
+    'init_panes' : function () {
+        var obj = this;
+        obj.panes_and_field_names = {
+
+        /* These get shown in the left panel */
+        'sitem_editor_left_pane' :
+        [
+            [
+                'ID',
+                { 
+                    render: '"#" + fm.id();', 
+
+                }
+            ],
+            [
+                'Status',
+                { 
+                    render: 'fm.status();',
+                    value_key: 'label'
+                }
+            ]
+        ],
+        /* These get shown in the middle panel */
+        'sitem_editor_middle_pane' :
+        [
+            [
+                'Distribution',
+                {
+                    render: 'fm.stream().distribution().label() == null ? "" : fm.stream().distribution().label();',
+
+                }
+            ],
+            [
+                'Shelving Unit ID',
+                {
+                    render: 'fm.unit() == null ? "" : "#" + fm.unit().id();',
+                }
+            ],
+        ],
+
+        /* These get shown in the right panel */
+        'sitem_editor_right_pane' :
+        [
+            [
+                'Date Expected',
+                {
+                    render: 'fm.date_expected() == null ? "" : util.date.formatted_date( fm.date_expected(), "%F");',
+                    input: 'c = function(v){ var applied = obj.apply("date_expected",v); if (typeof post_c == "function") post_c(v, !applied);}; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.date_expected); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'date_expected'
+                }
+            ],
+            [
+                'Date Received',
+                {
+                    render: 'fm.date_received() == null ? "" : util.date.formatted_date( fm.date_received(), "%F");',
+                    input: 'if (obj.editor_values.date_received) { c = function(v){ var applied = obj.apply("date_received",v); if (typeof post_c == "function") post_c(v, !applied);}; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.date_received); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false); } else { alert("Cannot edit Date Received for unreceived items."); block = false; }',
+                    value_key: 'date_received'
+                }
+            ],
+        ],
+
+        };
+    },
+
+    /******************************************************************************************************/
+    /* This loops through all our fieldnames and all the copies, tallying up counts for the different values */
+
+    'summarize' :  serial.editor_base.editor_base_summarize,
+
+    /******************************************************************************************************/
+    /* Display the summarized data and inputs for editing */
+
+    'render' :  serial.editor_base.editor_base_render,
+
+    /******************************************************************************************************/
+    /* This actually draws the change button and input widget for a given field */
+    'render_input' : serial.editor_base.editor_base_render_input,
+
+    /******************************************************************************************************/
+    /* save the items */
+
+    'save' : function() {
+        var obj = this;
+        obj.editor_base_save('open-ils.serial.item.fleshed.batch.update');
+    },
+
+    /******************************************************************************************************/
+    /* spawn notes interface */
+
+    'notes' : function() {
+        var obj = this;
+        JSAN.use('util.window'); var win = new util.window();
+        win.open(
+            urls.XUL_SERIAL_NOTES, 
+            //+ '?copy_id=' + window.escape(obj.sitems[0].id()),
+            'Item Notes','chrome,resizable,modal',
+            { 'object_id' : obj.sitems[0].id(), 'function_type' : 'SIN', 'object_type' : 'item', 'constructor' : sin }
+        );
+    },
+
+    /******************************************************************************************************/
+    'save_attributes' : serial.editor_base.editor_base_save_attributes
+
+};
+
+dump('exiting serial/sitem.js\n');
diff --git a/Open-ILS/xul/staff_client/server/serial/sitem_editor.xul b/Open-ILS/xul/staff_client/server/serial/sitem_editor.xul
new file mode 100644 (file)
index 0000000..c4ae7b3
--- /dev/null
@@ -0,0 +1,96 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Serial Item Editor -->
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- STYLESHEETS -->
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://open_ils_staff_client/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="/xul/server/skin/global.css" type="text/css"?>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- LOCALIZATION -->
+<!DOCTYPE window PUBLIC "" ""[
+       <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+<!-- OVERLAYS -->
+<?xul-overlay href="/xul/server/OpenILS/util_overlay.xul"?>
+
+<window id="serial_item_editor_win" 
+       onload="try { my_init(); font_helper(); persist_helper(); } catch(E) { alert(E); }"
+       width="800" height="390" oils_persist="width height"
+       title="Item Editor"
+       xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+       <!-- ///////////////////////////////////////////////////////////////////////////////////////////////////////////// -->
+       <!-- BEHAVIOR -->
+    <script type="text/javascript">
+               var myPackageDir = 'open_ils_staff_client'; var IAMXUL = true; var g = {};
+       </script>
+    <scripts id="openils_util_scripts"/>
+
+       <script type="text/javascript" src="/xul/server/main/JSAN.js"/>
+       <script type="text/javascript" src="sitem_editor.js"/>
+    <script>
+        <![CDATA[
+            function my_init() {
+                try {
+                    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+                    if (typeof JSAN == 'undefined') { 
+                        throw( document.getElementById("commonStrings").getString('common.jsan.missing') );
+                    }
+                    JSAN.errorLevel = "die"; // none, warn, or die
+                    JSAN.addRepository('/xul/server/');
+                    JSAN.use('util.error'); g.error = new util.error();
+                    g.error.sdump('D_TRACE','my_init() for serial/sitem_editor.xul');
+                    JSAN.use('serial.sitem_editor'); g.sitem_editor = new serial.sitem_editor();
+
+                    g.sitem_editor.init({"do_edit" : true, "handle_update" : true});
+
+                } catch(E) {
+                    var err_msg = document.getElementById("commonStrings").getFormattedString('common.exception', ['serial/sitem_editor.xul', E]);
+                    try { g.error.sdump('D_ERROR',err_msg); } catch(E) { dump(err_msg); }
+                    alert(err_msg);
+                }
+            }
+
+        ]]>
+    </script>
+
+       <messagecatalog id="catStrings" src="/xul/server/locale/<!--#echo var='locale'-->/cat.properties" />
+       <messagecatalog id="serialStrings" src="/xul/server/locale/<!--#echo var='locale'-->/serial.properties" />
+
+    <vbox id="brief_display_box"/>
+
+       <groupbox flex="1" class="my_overflow">
+               <hbox flex="1" style="overflow: auto">
+                       <vbox flex="1">
+                               <label value="Item" style="font-weight: bold; font-size: large"/>
+                               <vbox id="sitem_editor_left_pane" flex="1"/>
+                       </vbox>
+                       <splitter><grippy /></splitter>
+                       <vbox flex="1">
+                               <label value=" " style="font-weight: bold; font-size: large"/>
+                               <vbox id="sitem_editor_middle_pane" flex="1"/>
+                       </vbox>
+                       <splitter><grippy /></splitter>
+                       <vbox flex="1">
+                               <button style="font-weight: bold; font-size: normal" label="Item Dates" accesskey="1" oncommand="document.getElementById('sitem_editor_right_pane').firstChild.firstChild.focus();"/>
+                               <vbox id="sitem_editor_right_pane" flex="1"/>
+                       </vbox>
+               </hbox>
+
+               <hbox id="nav">
+                       <spacer flex="1"/>
+                       <button id="sitem_notes" label="&staff.serial.sitem_editor.notes;" accesskey="&staff.serial.sitem_editor.notes.accesskey;" oncommand="g.sitem_editor.notes();"/>
+                       <button id="sitem_save" label="&staff.serial.sitem_editor.modify;" hidden="true" accesskey="&staff.serial.sitem_editor.modify.accesskey;" oncommand="g.sitem_editor.save();"/>
+                       <button id="sitem_cancel" label="&staff.cat.copy_editor.cancel.label;" accesskey="&staff.cat.copy_editor.cancel.accesskey;" oncommand="window.close();"/>
+               </hbox>
+
+               <spacer/>
+       </groupbox>
+
+</window>
+
diff --git a/Open-ILS/xul/staff_client/server/serial/ssub_editor.js b/Open-ILS/xul/staff_client/server/serial/ssub_editor.js
new file mode 100644 (file)
index 0000000..c807f72
--- /dev/null
@@ -0,0 +1,193 @@
+dump('entering serial/ssub_editor.js\n');
+// vim:noet:sw=4:ts=4:
+
+JSAN.use('serial.editor_base');
+
+if (typeof serial == 'undefined') serial = {};
+serial.ssub_editor = function (params) {
+    try {
+        netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+        JSAN.use('util.error'); this.error = new util.error();
+    } catch(E) {
+        dump('serial/ssub_editor: ' + E + '\n');
+    }
+
+    /* This keeps track of what fields have been edited for styling purposes */
+    this.changed = {};
+
+    /* This holds the original values for prepopulating the field editors */
+    this.editor_values = {};
+
+};
+
+serial.ssub_editor.prototype = {
+    // we could do this with non-standard '__proto__' property instead
+    'editor_base_init' : serial.editor_base.editor_base_init,
+    'editor_base_apply' : serial.editor_base.editor_base_apply,
+    'editor_base_save' : serial.editor_base.editor_base_save,
+
+    'fm_type' : 'ssub',
+    'fm_type_plural' : 'ssubs',
+    'can_have_notes' : true,
+
+    'init' : function (params) {
+        var obj = this;
+
+        params.retrieve_function = 'FM_SSUB_FLESHED_BATCH_RETRIEVE.authoritative';
+
+        obj.editor_base_init(params);
+
+        /* Do it */
+        obj.summarize( obj.ssubs );
+        obj.render();
+    },
+
+    /******************************************************************************************************/
+    /* Restore backup copies */
+
+    'reset' : serial.editor_base.editor_base_reset,
+
+    /******************************************************************************************************/
+    /* Apply a value to a specific field on all the copies being edited */
+
+    'apply' : function(field, value) {
+        var obj = this;
+
+        if (field == 'start_date' || field == 'end_date') {
+            if (value == '') { value = null; }
+        }
+
+        obj.editor_base_apply(field, value);
+    },
+
+
+    /******************************************************************************************************/
+    /* These need data from the middle layer to render */
+
+    /*
+    function init_panes0() {
+    obj.special_exception = {};
+    obj.special_exception[$('catStrings').getString('staff.cat.copy_editor.field.owning_library.label')] = function(label,value) {
+            JSAN.use('util.widgets');
+            if (value>0) { // an existing call number
+                obj.network.simple_request(
+                    'FM_ACN_RETRIEVE.authoritative',
+                    [ value ],
+                    function(req) {
+                        var cn = '??? id = ' + value;
+                        try {
+                            cn = req.getResultObject();
+                        } catch(E) {
+                            obj.error.sdump('D_ERROR','callnumber retrieve: ' + E);
+                        }
+                        util.widgets.set_text(label,obj.data.hash.aou[ cn.owning_lib() ].shortname() + ' : ' + cn.label());
+                    }
+                );
+            } else { // a yet to be created call number
+                if (obj.callnumbers) {
+                    util.widgets.set_text(label,obj.data.hash.aou[ obj.callnumbers[value].owning_lib ].shortname() + ' : ' + obj.callnumbers[value].label);
+                }
+            }
+        };
+    },
+    */
+
+    /******************************************************************************************************/
+    /* These get show in the left panel */
+
+    'init_panes' : function () {
+        var obj = this;
+        obj.panes_and_field_names = {
+
+        'left_pane' :
+        [
+            [
+                'ID',
+                { 
+                    render: '"ID : " + fm.id();', 
+                    //input: 'c = function(v){ obj.apply("distribution",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+
+                }
+            ],
+            [
+                'Owning Lib',
+                {
+                    render: 'typeof fm.owning_lib() == "object" ? fm.owning_lib().shortname() : obj.data.hash.aou[ fm.owning_lib() ].shortname()',
+                    input: 'c = function(v){ obj.apply("owning_lib",v); if (typeof post_c == "function") post_c(v); }; x = util.widgets.make_menulist( util.functional.map_list( obj.data.list.aou, function(myobj) { var sname = myobj.shortname(); for (i = sname.length; i < 20; i++) sname += " "; return [ myobj.name() ? sname + " " + myobj.name() : myobj.shortname(), myobj.id(), ( ! get_bool( obj.data.hash.aout[ myobj.ou_type() ].can_have_vols() ) ), ( obj.data.hash.aout[ myobj.ou_type() ].depth() * 2), ]; }), obj.data.list.au[0].ws_ou()); x.setAttribute("value",obj.editor_values.owning_lib); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'owning_lib',
+                    dropdown_key: 'fm.owning_lib() == null ? null : typeof fm.owning_lib() == "object" ? fm.owning_lib().id() : fm.owning_lib()',
+                }
+            ],
+        ],
+
+            'right_pane' :
+        [
+            [
+                'Start Date',
+                { 
+                    render: 'fm.start_date() == null ? "" : util.date.formatted_date( fm.start_date(), "%F");',
+                    input: 'c = function(v){ obj.apply("start_date",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.start_date); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'start_date'
+                }
+            ],
+            [
+                'End Date',
+                {
+                    render: 'fm.end_date() == null ? "" : util.date.formatted_date( fm.end_date(), "%F");',
+                    input: 'c = function(v){ obj.apply("end_date",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.end_date); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'end_date'
+                }
+            ],
+            [
+                'Date Offset',
+                { 
+                    render: 'fm.expected_date_offset() == null ? "" : fm.expected_date_offset();',
+                    input: 'c = function(v){ obj.apply("expected_date_offset",v); if (typeof post_c == "function") post_c(v); }; x = document.createElement("textbox"); x.setAttribute("value",obj.editor_values.expected_date_offset); x.addEventListener("apply",function(f){ return function(ev) { f(ev.target.value); } }(c), false);',
+                    value_key: 'expected_date_offset'
+                }
+            ],
+        ],
+
+        };
+    },
+
+    /******************************************************************************************************/
+    /* This loops through all our fieldnames and all the copies, tallying up counts for the different values */
+
+    'summarize' : serial.editor_base.editor_base_summarize,
+
+    /******************************************************************************************************/
+    /* Display the summarized data and inputs for editing */
+
+    'render' : serial.editor_base.editor_base_render,
+
+    /******************************************************************************************************/
+    /* This actually draws the change button and input widget for a given field */
+    'render_input' : serial.editor_base.editor_base_render_input,
+
+    /******************************************************************************************************/
+    /* save the subscriptions */
+
+    'save' : function() {
+        var obj = this;
+        obj.editor_base_save('open-ils.serial.subscription.fleshed.batch.update');
+    },
+
+    /******************************************************************************************************/
+    /* spawn notes interface */
+
+    'notes' : function() {
+        var obj = this;
+        JSAN.use('util.window'); var win = new util.window();
+        win.open(
+            urls.XUL_SERIAL_NOTES, 
+            'Subscription Notes','chrome,resizable,modal',
+            { 'object_id' : obj.ssubs[0].id(), 'function_type' : 'SSUBN', 'object_type' : 'subscription', 'constructor' : ssubn }
+        );
+    },
+
+    /******************************************************************************************************/
+    'save_attributes' : serial.editor_base.editor_base_save_attributes
+};
+
+dump('exiting serial/ssub_editor.js\n');
diff --git a/Open-ILS/xul/staff_client/server/serial/ssub_editor.xul b/Open-ILS/xul/staff_client/server/serial/ssub_editor.xul
new file mode 100644 (file)
index 0000000..b2ba9aa
--- /dev/null
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+<!-- Application: Evergreen Staff Client -->
+<!-- Screen: Subscription Editor Overlay -->
+
+<!-- LOCALIZATION -->
+<!DOCTYPE overlay PUBLIC "" ""[
+    <!--#include virtual="/opac/locale/${locale}/lang.dtd"-->
+]>
+
+<overlay id="serial_ssub_editor_panel_overlay" 
+       xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+       <vbox flex="1" id="serial_ssub_editor_panel" class="my_overflow">
+        <vbox id="brief_display_box"/>
+
+               <hbox flex="1" style="overflow: auto">
+                       <vbox flex="1">
+                               <label value="Subscription" style="font-weight: bold; font-size: large"/>
+                               <vbox id="left_pane" flex="1"/>
+                       </vbox>
+                       <splitter><grippy /></splitter>
+                       <vbox flex="1">
+                               <label value=" " style="font-weight: bold; font-size: large"/>
+                               <vbox id="right_pane" flex="1"/>
+                       </vbox>
+               </hbox>
+
+               <hbox id="nav">
+                       <spacer flex="1"/>
+                       <button id="ssub_notes" label="&staff.serial.ssub_editor.notes;" accesskey="&staff.serial.ssub_editor.notes.accesskey;" oncommand="g.manage_subs.ssub_editor.notes()" />
+                       <button id="ssub_save" label="&staff.serial.ssub_editor.modify;" hidden="true" accesskey="&staff.serial.ssub_editor.modify.accesskey;" oncommand="g.manage_subs.ssub_editor.save()" />
+                       <!--<button id="cancel" label="&staff.cat.copy_editor.cancel.label;" accesskey="&staff.cat.copy_editor.cancel.accesskey;" oncommand="window.close();"/>-->
+               </hbox>
+
+               <spacer/>
+       </vbox>
+
+</overlay>
+