Initial commit of the Serial.pm application. It is mostly basic, still in rough...
authordbwells <dbwells@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Thu, 8 Jul 2010 15:16:51 +0000 (15:16 +0000)
committerdbwells <dbwells@dcc99617-32d9-48b4-a31d-7c20da2025e4>
Thu, 8 Jul 2010 15:16:51 +0000 (15:16 +0000)
git-svn-id: svn://svn.open-ils.org/ILS/branches/seials-integration@16882 dcc99617-32d9-48b4-a31d-7c20da2025e4

Open-ILS/src/perlmods/OpenILS/Application/Serial.pm [new file with mode: 0644]

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..3c0a6d5
--- /dev/null
@@ -0,0 +1,1182 @@
+#!/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::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] );
+
+
+# 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 => 'issuances',
+                 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_item( $editor, $override, $item);
+        } elsif( $item->isnew ) {
+            # TODO: reconsider this
+            # if the item has a new issuance, create the issuance first
+            if ($item->issuance->isnew) {
+                fleshed_issuance_alter($self, $conn, $auth, [$item->issuance]);
+            }
+            _cleanse_dates($item, ['date_expected','date_received']);
+            $evt = _create_item( $editor, $item );
+        } else {
+            _cleanse_dates($item, ['date_expected','date_received']);
+            $evt = _update_item( $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_item {
+    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_item {
+    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_item {
+    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_issuance( $editor, $override, $issuance);
+        } elsif( $issuance->isnew ) {
+            _cleanse_dates($issuance, ['date_published']);
+            $evt = _create_issuance( $editor, $issuance );
+        } else {
+            _cleanse_dates($issuance, ['date_published']);
+            $evt = _update_issuance( $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_issuance {
+    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_issuance {
+    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_issuance {
+    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;
+}
+
+
+##########################################################################
+# 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) = @_;
+
+#    my $evt;
+#    my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
+#    return $evt if ( $evt = OpenILS::Application::Cat::AssetCommon->org_cannot_have_vols($editor, $org) );
+
+    $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;
+}
+
+
+##########################################################################
+# predict and receive methods
+#
+__PACKAGE__->register_method(
+    method    => 'generate_predictions',
+    api_name  => 'open-ils.serial.generate_predictions',
+    api_level => 1,
+    argc      => 1,
+    signature => {
+        desc     => 'Receives an sre (serial record entry) id and returns an array ref of predicted issuances',
+        'params' => [ {
+                 name => 'sre_id',
+                 desc => 'Serial Record Entry ID',
+                 type => 'integer'
+            }
+        ],
+        'return' => {
+            desc => 'Returns predicted issuances',
+            type => 'array'
+        }
+    }
+);
+
+sub generate_predictions {
+    my ($self, $conn, $authtoken, $args) = @_;
+
+    my $editor = OpenILS::Utils::CStoreEditor->new();
+    if (!exists($args->{sre_id})) { # lookup by sdist_id instead
+        my $sdist = $editor->retrieve_serial_distribution([$args->{sdist_id}]);
+        $args->{sre_id} = $sdist->record_entry;
+    }
+    #return $args->{sre_id};
+    my $sre = $editor->retrieve_serial_record_entry([$args->{sre_id}]);
+
+    #return $sre->marc;
+
+    #convert from marc_xml to marc
+    my $marc = MARC::Record->new_from_xml($sre->marc);
+
+    #turn into MFHD record object
+    my $mfhd = MFHD->new($marc);
+
+    my @predictions;
+    # TODO: consider support for predicting supplements/indexes (854/855)
+    my $tag = '853';
+    my @active_captions = $mfhd->active_captions($tag);
+    foreach my $caption (@active_captions) {
+        my $options = {
+                'caption' => $caption,
+                'num_to_predict' => $args->{num_to_predict},
+                'last_rec_date' => $args->{last_rec_date}
+                };
+        if ($args->{from_last_received}) {
+            my $last_received = $editor->search_serial_issuance([
+                {   'holding_type' => $MFHD_NAMES_BY_TAG{$tag},
+                    'holding_link_id' => $caption->link_id,
+                    'distribution' => $args->{sdist_id}},
+                {limit => 1, order_by => { siss => "date_expected DESC" }}]
+                );
+            if ($last_received->[0]) {
+                $options->{last_rec_date} = $last_received->[0]->date_expected;
+                $options->{predict_from} = _revive_holding($mfhd, $last_received->[0]->holding_code);
+            }
+        }
+        push( @predictions, _generate_issuance_values($mfhd, $options) );
+    }
+
+    my @issuances;
+    foreach my $prediction (@predictions) {
+        my $issuance = new Fieldmapper::serial::issuance;
+        $issuance->isnew(1);
+        $issuance->holding_link_id($prediction->[0]);
+        $issuance->label($prediction->[1]);
+        $issuance->date_published($prediction->[2]);
+        $issuance->date_expected($prediction->[3]);
+        $issuance->holding_code(OpenSRF::Utils::JSON->perl2JSON($prediction->[4]));
+        $issuance->holding_type($prediction->[5]);
+        push (@issuances, $issuance);
+    }
+
+    return \@issuances;
+}
+
+#
+# _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 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).
+#
+sub _generate_issuance_values {
+    my ($mfhd, $options) = @_;
+    my $caption = $options->{caption};
+    my $num_to_predict = $options->{num_to_predict};
+    my $last_rec_date = $options->{last_rec_date};   # expected or actual, according to preference
+    my $predict_from = $options->{predict_from};   # optional issuance to predict from
+
+    # TODO: add support for predicting serials with no chronology by passing in
+    # a last_pub_date option?
+
+    my $strp = new DateTime::Format::Strptime(pattern => '%F');
+
+    my $receival_date = $strp->parse_datetime($last_rec_date);
+
+    my $htag    = $caption->tag;
+    $htag =~ s/^85/86/;
+    my $link_id = $caption->link_id;
+    if(!$predict_from) {
+        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;
+    }
+
+    my $pub_date  = $strp->parse_datetime($predict_from->chron_to_date);
+    my $date_diff = $receival_date - $pub_date;
+
+    $predict_from->notes('public',  []);
+# add a note marker for system use
+    $predict_from->notes('private', ['AUTOGEN']);
+
+    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);
+        my $arrival_date = $pub_date + $date_diff;
+        push(
+                @issuance_values,
+                [
+                    $link_id,
+                    $prediction->format,
+                    $pub_date->strftime('%F'),
+                    $arrival_date->strftime('%F'),
+                    [$htag,$prediction->indicator(1),$prediction->indicator(2),$prediction->subfields_list],
+                    $MFHD_NAMES_BY_TAG{$caption->tag}
+                ]
+            );
+    }
+
+    return @issuance_values;
+}
+
+sub _revive_holding {
+    my $mfhd = shift;
+    my $holding_code = shift;
+
+    # build MARC::Field
+    my $holding_parts = OpenSRF::Utils::JSON->JSON2perl($holding_code);
+    my $issuance_holding = new MARC::Field(@$holding_parts);
+    # fetch matching captions
+    my $captag = $issuance_holding->tag;
+    $captag =~ s/^86/85/;
+    my $captions_ref = $mfhd->captions($captag, 'hashref');
+    # build MFHD::Holding
+    my $link_subfield = $issuance_holding->subfield('8');
+    my ($link_id, $seqno) = split(/\./, $link_subfield);
+    return new MFHD::Holding($seqno, $issuance_holding, $captions_ref->{$link_id});
+}
+
+__PACKAGE__->register_method(
+    method    => 'receive_issuances',
+    api_name  => 'open-ils.serial.receive_issuances',
+    api_level => 1,
+    argc      => 1,
+    signature => {
+        desc     => 'Marks an issuance as received, updates the shelving unit (creating a new shelving unit if needed), and updates the underlying MFHD record',
+        'params' => [ {
+                 name => 'issuances',
+                 desc => 'array of Issuance objects',
+                 type => 'array'
+            }
+        ],
+        'return' => {
+            desc => 'Returns number of received issuances',
+            type => 'int'
+        }
+    }
+);
+
+sub receive_issuances {
+    my ($self, $conn, $auth, $issuances) = @_;
+
+    my $last_distribution;
+    my $last_mfhd;
+    my %sres_to_save;
+    my %mfhds_to_save;
+    my( $reqr, $evt ) = $U->checkses($auth);
+    return $evt if $evt;
+    my $editor = new_editor(requestor => $reqr, xact => 1);
+    foreach my $issuance (@$issuances) {
+        # unflesh shelving unit if fleshed
+        $issuance->shelving_unit( $issuance->shelving_unit->id ) if ref($issuance->shelving_unit);
+        $issuance->distribution( $issuance->distribution->id ) if ref($issuance->distribution);
+
+        $issuance->copies_received($issuance->copies_received + 1);
+        $issuance->copies_expected($issuance->copies_expected - 1);
+        $issuance->date_received('now');
+
+        # create shelving unit if needed
+        if ($issuance->shelving_unit == -1) { # create by "volume" (first issuance division)
+        #TODO
+        } elsif ($issuance->shelving_unit == -2) { # create by "issue" (second issuance division)
+        #TODO
+        }
+
+        my $mfhd;
+        my $sre;
+        if ($issuance->distribution == $last_distribution) {
+            # use cached record
+            $mfhd = $last_mfhd;
+        } else { # get MFHD record
+            my $sdist = $editor->retrieve_serial_distribution([$issuance->distribution]);
+            $sre = $editor->retrieve_serial_record_entry([$sdist->record_entry]);
+
+            #convert from marc_xml to marc
+            my $marc = MARC::Record->new_from_xml($sre->marc);
+
+            #turn into MFHD record object
+            $mfhd = MFHD->new($marc);
+            $sres_to_save{$sre->id} = $sre;
+            $mfhds_to_save{$sre->id} = $mfhd;
+        }
+
+#        # build MARC::Field
+#        my $holding_parts = OpenSRF::Utils::JSON->JSON2perl($issuance->holding_code);
+#        my $issuance_holding = new MARC::Field(@$holding_parts);
+#        # fetch matching captions
+#        my $captag = $issuance_holding->tag;
+#        $captag =~ s/^86/85/;
+#        my $captions_ref = $mfhd->captions($captag, 'hashref');
+#        # build MFHD::Holding
+#        my $link_subfield = $issuance_holding->subfield('8');
+#        my ($link_id, $seqno) = split(/\./, $link_subfield);
+#        $issuance_holding = new MFHD::Holding($seqno, $issuance_holding, $captions_ref->{$link_id});
+        my $issuance_holding = _revive_holding($mfhd, $issuance->holding_code);
+        
+        # get all current holdings for this linked caption
+#        my @curr_holdings = $mfhd->holdings($issuance_holding->tag, $link_id);
+        my @curr_holdings = $mfhd->holdings($issuance_holding->tag, $issuance_holding->caption->link_id);
+        # short-circuit logic : if holding is the next one, increment the last current holding
+        my $next_holding_values = $curr_holdings[-1]->next;
+        if ($next_holding_values and $issuance_holding->matches($next_holding_values)) {
+            $curr_holdings[-1]->extend;
+        } else { # not the next expected, do full replacement
+            $mfhd->append_fields($issuance_holding);
+#            my @updated_holdings = $mfhd->get_compressed_holdings($captions_ref->{$link_id});
+            my @updated_holdings = $mfhd->get_compressed_holdings($issuance_holding->caption);
+            # set reference point to top of current holdings
+            my $marker_field = MARC::Field->new(500, '', '','a' => 'Temporary Marker'); 
+            $mfhd->insert_fields_before($curr_holdings[0], $marker_field);
+            foreach my $holding (@curr_holdings) {
+                $mfhd->delete_field($holding);
+            }
+            $mfhd->delete_field($issuance_holding);
+            $mfhd->insert_fields_before($marker_field, @updated_holdings);
+            # delete reference point
+            $mfhd->delete_field($marker_field);
+        }   
+
+        $last_distribution = $issuance->distribution;
+        $last_mfhd = $mfhd;
+        _update_issuance($editor, undef, $issuance);
+    }
+
+    foreach my $sre_id (keys %sres_to_save) {
+        #TODO: update '005' to current date
+        my $sre = $sres_to_save{$sre_id};
+        (my $xml = $mfhds_to_save{$sre_id}->as_xml_record()) =~ s/\n//sog;
+        $xml =~ s/^<\?xml.+\?\s*>//go;
+        $sre->marc($xml);
+        $sre->ischanged(1);
+        $editor->update_serial_record_entry($sre);
+        #return ($sre->record);
+    }
+
+    #return OpenSRF::Utils::JSON->perl2JSON($last_mfhd);
+
+    $editor->commit;
+    return scalar @$issuances;
+}
+
+
+##########################################################################
+# 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
+    /
+);
+
+# 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
+    /
+);
+
+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.
+        /
+);
+
+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 $note = $e->retrieve_serial_item_note([
+        $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);
+#    }
+
+    my $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));
+    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"
+);
+
+# user_session may be null/undef
+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  ];
+
+#              for my $dist (@$dists) {
+#                      if( $c->status == OILS_COPY_STATUS_CHECKED_OUT ) {
+#                              $c->circulations(
+#                                      $e->search_action_circulation(
+#                                              [
+#                                                      { target_copy => $c->id },
+#                                                      {
+#                                                              order_by => { circ => 'xact_start desc' },
+#                                                              limit => 1
+#                                                      }
+#                                              ]
+#                                      )
+#                              )
+#                      }
+#              }
+
+               $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);
+
+               my $scaps = $e->search_serial_caption_and_pattern(
+                       { subscription => $sub->id }); # TODO: filter on !deleted?
+
+               $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);
+    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 / ]}
+        });
+}
+
+1;