Make FF a child of Evergreen; experiment
authorBill Erickson <berick@esilibrary.com>
Fri, 30 Aug 2013 20:21:05 +0000 (16:21 -0400)
committerBill Erickson <berick@esilibrary.com>
Fri, 25 Oct 2013 15:09:43 +0000 (11:09 -0400)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
44 files changed:
Open-ILS/src/perlmods/lib/FulfILLment/AT/Reactor/BibRefresh.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/AT/Reactor/ItemLoad.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/AT/Reactor/ItemRefresh.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/Application/LAICore.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Aleph.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Evergreen.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Evergreen/FulfILLment_EGAPP.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Horizon.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/III.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/III/2009B_1_2.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/III/2011_1_3.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Koha.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Polaris.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Symphony.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/Util/NCIP.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/Util/SIP2Client.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/Util/Z3950.pm [new file with mode: 0644]
Open-ILS/src/perlmods/lib/FulfILLment/WWW/FastImport.pm [new file with mode: 0755]
Open-ILS/web/staff/js/circ_tracker.js [new file with mode: 0644]
Open-ILS/web/staff/js/hold_tracker.js [new file with mode: 0644]
Open-ILS/web/staff/js/record_mgmt.js [new file with mode: 0644]
Open-ILS/web/staff/php/barcode/barcode.php [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/c128aobject.php [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/c128bobject.php [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/c128cobject.php [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/c39object.php [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/debug.php [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/download.php [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/download.png [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/home.php [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/home.png [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/i25object.php [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/image.php [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/image.png [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/index.php [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/linux.gif [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/php_logo.gif [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/sample.php [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/sample.png [new file with mode: 0755]
Open-ILS/web/staff/php/barcode/spain.png [new file with mode: 0755]
Open-ILS/web/staff/xml/circ_tracker.xml [new file with mode: 0644]
Open-ILS/web/staff/xml/hold_tracker.xml [new file with mode: 0644]
Open-ILS/web/staff/xml/record_mgmt.xml [new file with mode: 0644]

diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/AT/Reactor/BibRefresh.pm b/Open-ILS/src/perlmods/lib/FulfILLment/AT/Reactor/BibRefresh.pm
new file mode 100644 (file)
index 0000000..20eb10d
--- /dev/null
@@ -0,0 +1,57 @@
+package FulfILLment::AT::Reactor::BibRefresh;
+use base 'OpenILS::Application::Trigger::Reactor';
+use OpenSRF::Utils::Logger qw($logger);
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+
+use strict; use warnings;
+use Error qw/:try/;
+use OpenSRF::Utils::SettingsClient;
+
+my $U = 'OpenILS::Application::AppUtils';
+my $FF = OpenSRF::AppSession->create('fulfillment.laicore');
+
+sub handler {
+    my $self = shift;
+    my $env = shift;
+
+    update_bib($_) for @{$env->{target}};
+
+    return 1;
+}
+
+sub update_bib {
+    my $self = shift;
+    my $b = shift || $self;
+    my $e = new_editor();
+
+    my $owner = $b->owner;
+    my ($error, $new_bibs);
+
+    try {
+        $new_bibs = $FF->request( 'fulfillment.laicore.record_by_id', $owner, $b->remote_id)->gather(1);
+    } otherwise {
+        $error = 1;
+    };
+
+    unless ($error) {
+        if (@$new_bibs) {
+
+            my $bib = shift @$new_bibs;
+            (my $id = $bib->{id}) =~ s#^/resources/##;
+            $b->ischanged(1);
+            $b->cache_time('edit_time');
+            $b->marc($bib->{content});
+            $b->remote_id($id); # just in case
+
+            $e->xact_begin;
+            $e->update_biblio_record_entry($b) or return $e->die_event;
+            return $e->xact_commit;
+        }
+
+        return undef;
+    }
+
+    return 0;
+}
+
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/AT/Reactor/ItemLoad.pm b/Open-ILS/src/perlmods/lib/FulfILLment/AT/Reactor/ItemLoad.pm
new file mode 100644 (file)
index 0000000..b22bc94
--- /dev/null
@@ -0,0 +1,119 @@
+package FulfILLment::AT::Reactor::ItemLoad;
+use base 'OpenILS::Application::Trigger::Reactor';
+use OpenSRF::Utils::Logger qw($logger);
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+
+use strict; use warnings;
+use Error qw/:try/;
+use OpenSRF::Utils::SettingsClient;
+
+my $U = 'OpenILS::Application::AppUtils';
+
+=comment
+$item_template = {
+
+         'isbn_issn_code' => '',
+         'call_number' => '',
+         'edit_date' => '',
+         'create_date' => '',
+         'fingerprint' => '',
+         'barcode' => '',
+         'holdable' => 't',
+         'call_number' => '',
+         'agency_id' => '',
+         'error' => 0,
+         'error_message' => ''
+};
+=cut
+
+sub ByBib {
+    my $self = shift;
+    my $env = shift;
+
+    my $owner = $env->{target}->owner;
+    my $remote_bibid = $env->{target}->remote_id;
+    my $bibid = $env->{target}->id;
+
+    my $FF = OpenSRF::AppSession->create('fulfillment.laicore');
+
+    my ($error, $new_items);
+    try {
+        $new_items = $FF->request( 'fulfillment.laicore.items_by_record', $owner, $remote_bibid)->gather(1);
+    } otherwise {
+        $error = 1;
+    };
+
+    if ($error or !$new_items or !@$new_items) {
+        $FF->disconnect;
+        return 0;
+    }
+
+    my $e = new_editor();
+    for my $remote_cp (@$new_items) {
+        try {
+            $e->xact_begin;
+    
+            $$remote_cp{call_number} ||= 'UNKNOWN';
+    
+            $logger->info("Remote copy data: " . join(', ', map { "$_ => $$remote_cp{$_}" } keys %$remote_cp));
+    
+            my $existing_cp = $e->search_asset_copy(
+                { source_lib => $owner, barcode => $$remote_cp{barcode} }
+            )->[0];
+    
+            if (!$existing_cp) {
+                $existing_cp = Fieldmapper::asset::copy->new;
+                $existing_cp->isnew(1);
+                $existing_cp->creator(1);
+                $existing_cp->editor(1);
+                $existing_cp->loan_duration(2);
+                $existing_cp->fine_level(2);
+                $existing_cp->source_lib($owner);
+                $existing_cp->circ_lib($owner);
+                $existing_cp->barcode($$remote_cp{barcode});
+            }
+    
+            $existing_cp->ischanged( 1 );
+            $existing_cp->remote_id( $remote_cp->{bib_id} );
+            $existing_cp->holdable( defined($remote_cp->{holdable}) ? $remote_cp->{holdable} : 1 );
+            my $due = $remote_cp->{due_date} || ''; # avoid warnings
+            $existing_cp->status( $due =~ /^\d+-\d+-\d+$/ ? 1 : 0 );
+    
+    
+            my $existing_cn = $e->search_asset_call_number(
+                { record => $bibid, owning_lib => $owner, label => $$remote_cp{call_number} }
+            )->[0];
+    
+            if (!$existing_cn) {
+                $existing_cn = Fieldmapper::asset::call_number->new;
+                $existing_cn->isnew(1);
+                $existing_cn->creator(1);
+                $existing_cn->editor(1);
+                $existing_cn->label($$remote_cp{call_number});
+                $existing_cn->owning_lib($owner);
+                $existing_cn->record($bibid);
+    
+                $existing_cn = $e->create_asset_call_number( $existing_cn );
+            }
+    
+            $existing_cp->call_number( $existing_cn->id );
+    
+            if ($existing_cp->isnew) {
+                $e->create_asset_copy( $existing_cp );
+            } else {
+                $e->update_asset_copy( $existing_cp );
+            }
+    
+            $e->xact_commit;
+        } otherwise {
+            $e->xact_rollback;
+        };
+    }
+    $e->disconnect;
+    $FF->disconnect;
+
+    return 1;
+}
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/AT/Reactor/ItemRefresh.pm b/Open-ILS/src/perlmods/lib/FulfILLment/AT/Reactor/ItemRefresh.pm
new file mode 100644 (file)
index 0000000..95ef382
--- /dev/null
@@ -0,0 +1,86 @@
+package FulfILLment::AT::Reactor::ItemRefresh;
+use base 'OpenILS::Application::Trigger::Reactor';
+use OpenSRF::Utils::Logger qw($logger);
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+
+use strict; use warnings;
+use Error qw/:try/;
+use OpenSRF::Utils::SettingsClient;
+
+my $U = 'OpenILS::Application::AppUtils';
+
+
+sub ByItem {
+    my $self = shift;
+    my $env = shift;
+
+    update_item($_) for @{$env->{target}};
+
+    return 1;
+}
+
+sub ByBib {
+    my $self = shift;
+    my $env = shift;
+
+    my $e = new_editor();
+    for my $cn (@{ $e->search_asset_call_number( { record => $env->{target}->id } ) }) {
+        update_item($_) for @{ $e->search_asset_copy( { call_number => $cn->id } ) };
+    }
+
+    return 1;
+}
+
+sub update_item {
+    my $self = shift;
+    my $i = shift;
+
+    $i = $self if (!$i);
+
+    my $owner = $i->source_lib;
+    my $e = new_editor();
+    my $FF = OpenSRF::AppSession->create('fulfillment.laicore');
+
+    my ($error, $new_items);
+    try {
+        $new_items = $FF->request( 'fulfillment.laicore.item_by_barcode', $owner, $i->barcode)->gather(1);
+    } otherwise {
+        $error = 1;
+    };
+
+    unless ($error) {
+        if (@$new_items) {
+            my $item = shift @$new_items;
+
+            $i->ischanged(1);
+            $i->cache_time('now');
+            $i->remote_id($item->{bib_id});
+            $i->holdable($item->{holdable});
+            $i->status(
+                $item->{due_date} =~ /^\d+-\d+-\d+$/ ? 1 : 0
+            );
+
+            my $bib = $e->search_biblio_record_entry(
+                {remote_id => $item->{bib_id}, owner => $owner}
+            )->[0];
+
+            if ($bib) {
+                my $cn = $e->search_asset_call_number(
+                    {label => $item->{call_number}, record => $bib->id}
+                )->[0];
+                $i->call_number($cn->id) if ($cn);
+            }
+
+            $e->xact_begin;
+            $e->update_asset_copy($i) or return $e->die_event;
+            return $e->xact_commit;
+        }
+
+        return undef;
+    }
+
+    return 0;
+}
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/Application/LAICore.pm b/Open-ILS/src/perlmods/lib/FulfILLment/Application/LAICore.pm
new file mode 100644 (file)
index 0000000..fc3edfe
--- /dev/null
@@ -0,0 +1,375 @@
+package FulfILLment::Application::LAICore;
+use strict; use warnings;
+use OpenILS::Application;
+use base qw/OpenILS::Application/;
+use OpenSRF::AppSession;
+use OpenSRF::EX qw(:try);
+use OpenSRF::Utils::SettingsClient;
+use OpenSRF::Utils::Logger qw($logger);
+use OpenSRF::Utils::JSON;
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+use FulfILLment::LAIConnector;
+use FulfILLment::AT::Reactor::ItemLoad;
+
+sub killme_wrapper {
+
+    my $self = shift;
+    my $client = shift;
+    my $method = $self->{real_method};
+    die "killme_wrapper called without method\n" unless $method;
+
+    $logger->info("FF executing LAICore::$method()");
+
+    my @final = ();
+    eval {
+      local $SIG{ALRM} = sub { die 'killme' };
+      alarm($self->{killme});
+      my $this = bless($self,$self->{package});
+      @final = $this->$method($client,@_);
+      alarm(0);
+    };
+    alarm(0);
+
+    if ($@) {
+        $logger->error("Error executing $method : $@");
+        return undef;
+    }
+
+    $client->respond($_) for (@final);
+    return undef;
+}
+
+# TODO : If/when API calls return virtual Fieldmapper 
+# objects (or arrays thereof), a wrapper to hash-ify 
+# all outbound responses may be in order.
+
+sub register_method {
+    my $class = shift;
+    my %args = @_;
+
+    $class = ref($class) || $class;
+    $args{package} = $class;
+
+    if (exists($args{killme})) {
+        $args{real_method} = $args{method};
+        $args{method} = 'killme_wrapper';
+        return $class->SUPER::register_method( %args );
+    }
+
+    $class->SUPER::register_method( %args );
+}
+
+
+__PACKAGE__->register_method(
+    method    => "get_connector_info",
+    api_name  => "fulfillment.laicore.connector_info.retrieve",
+    signature => { params => [ {desc => 'Org Unit ID', type => 'number'} ] },
+    argc      => 1,
+    api_level => 1
+);
+
+sub get_connector_info {
+    my ($self, $client, $ou) = @_;
+    return undef if ($ou !~ /^\d+$/);
+    return FulfILLment::LAIConnector->load($ou);
+}
+
+__PACKAGE__->register_method(
+    method   => "items_by_barcode",
+    api_name => "fulfillment.laicore.item_by_barcode",
+    killme   => 120
+);
+__PACKAGE__->register_method(
+    method   => "items_by_barcode",
+    api_name => "fulfillment.laicore.item_by_barcode.batch",
+    killme   => 120
+);
+
+sub items_by_barcode {
+    my ($self, $client, $ou, $ids) = @_;
+
+    my $connector = FulfILLment::LAIConnector->load($ou) or return;
+
+    return $connector->get_item_batch($ids) 
+        if $self->api_name =~ /batch/;
+
+    return $connector->get_item($ids);
+}
+
+__PACKAGE__->register_method(
+    method   => "items_by_record",
+    api_name => "fulfillment.laicore.items_by_record",
+    killme   => 120
+);
+__PACKAGE__->register_method(
+    method   => "items_by_record",
+    api_name => "fulfillment.laicore.items_by_record.batch",
+    killme   => 120
+);
+sub items_by_record {
+    my ($self, $client, $ou, $ids) = @_;
+
+    my $connector = FulfILLment::LAIConnector->load($ou) or return;
+
+    return $connector->get_items_by_record_batch($ids) 
+        if $self->api_name =~ /batch/;
+
+    return $connector->get_items_by_record($ids);
+}
+
+__PACKAGE__->register_method(
+    method   => "records_by_item",
+    api_name => "fulfillment.laicore.record_by_item",
+    killme   => 120
+);
+__PACKAGE__->register_method(
+    method   => "items_by_record",
+    api_name => "fulfillment.laicore.record_by_item.batch",
+    killme   => 120
+);
+sub records_by_item {
+    my ($self, $client, $ou, $ids) = @_;
+
+    my $connector = FulfILLment::LAIConnector->load($ou) or return;
+
+    return $connector->get_record_by_item_batch($ids) 
+        if $self->api_name =~ /batch/;
+
+    return $connector->get_record_by_item($ids);
+}
+
+__PACKAGE__->register_method(
+    method   => "get_holds",
+    api_name => "fulfillment.laicore.holds_by_item",
+    killme   => 120
+);
+__PACKAGE__->register_method(
+    method   => "get_holds",
+    api_name => "fulfillment.laicore.holds_by_record",
+    killme   => 120
+);
+# target is copy_barcode or record_id
+sub get_holds {
+    my ($self, $client, $ou, $target, $user_barcode) = @_;
+
+    my $connector = FulfILLment::LAIConnector->load($ou) or return;
+
+    return $connector->get_item_holds($target, $user_barcode)
+        if $self->api_name =~ /by_item/;
+
+    return $connector->get_record_holds($target, $user_barcode);
+}
+
+__PACKAGE__->register_method(
+    method   => "lender_holds",
+    api_name => "fulfillment.laicore.hold.lender.place",
+    killme   => 120
+);
+__PACKAGE__->register_method(
+    method   => "lender_holds",
+    api_name => "fulfillment.laicore.hold.lender.delete_earliest",
+    killme   => 120
+);
+
+sub lender_holds {
+    my ($self, $client, $ou, $copy_barcode) = @_;
+
+    my $connector = FulfILLment::LAIConnector->load($ou) or return;
+    my $e = new_editor();
+
+    # for lending library holds, we use the configured hold user
+    my $user_barcode = $connector->{'user.hold'} || $connector->{'user'};
+
+    if (!$user_barcode) {
+        $logger->error(
+            "FF no hold recipient defined for ou=$ou copy=$copy_barcode");
+        return;
+    }
+
+    # TODO: proxy user pickup lib setting?
+    return $connector->place_lender_hold($copy_barcode, $user_barcode)
+        if $self->api_name =~ /place/;
+
+    return $connector->delete_lender_hold($copy_barcode, $user_barcode);
+}
+
+__PACKAGE__->register_method(
+    method   => "create_borrower_copy",
+    api_name => "fulfillment.laicore.item.create_for_borrower",
+    killme   => 120
+);
+sub create_borrower_copy {
+    my ($self, $client, $ou, $src_copy_id) = @_;
+
+    my $connector = FulfILLment::LAIConnector->load($ou) or return;
+    my $e = new_editor();
+
+    my $src_copy = $e->retrieve_asset_copy([
+        $src_copy_id,
+        {   flesh => 3, 
+            flesh_fields => {
+                acp => ['call_number'], 
+                acn => ['record'],
+                bre => ['simple_record']
+            }
+        }
+    ]);
+
+    my $circ_lib = $e->retrieve_actor_org_unit($ou)->shortname;
+
+    return $connector->create_borrower_copy($src_copy, $circ_lib);
+}
+
+
+__PACKAGE__->register_method(
+    method   => "borrower_holds",
+    api_name => "fulfillment.laicore.hold.borrower.place",
+    killme   => 120
+);
+__PACKAGE__->register_method(
+    method   => "borrower_holds",
+    api_name => "fulfillment.laicore.hold.borrower.delete_earliest",
+    killme   => 120
+);
+# ---------------------------------------------------------------------------
+# Borrower Library Holds:
+#   Create a hold against the temporary copy for the borrowing user
+#   at the borrowing library.
+# ---------------------------------------------------------------------------
+sub borrower_holds {
+    my ($self, $client, $ou, $copy_barcode, $user_barcode) = @_;
+
+    # TODO: should be a pickup_lib here based on the pickup_lib
+    # of the FF hold
+
+    my $connector = FulfILLment::LAIConnector->load($ou) or return;
+    my $e = new_editor();
+    my $pickup_lib = $e->retrieve_actor_org_unit($ou)->shortname;
+
+    return $connector->place_borrower_hold(
+        $copy_barcode, $user_barcode, $pickup_lib)
+        if $self->api_name =~ /place/;
+
+    return $connector->deletel_borrower_hold($copy_barcode, $user_barcode);
+}
+
+
+__PACKAGE__->register_method(
+    method   => "circulation",
+    api_name => "fulfillment.laicore.circ.retrieve",
+    killme   => 120
+);
+__PACKAGE__->register_method(
+    method   => "circulation",
+    api_name => "fulfillment.laicore.circ.lender.checkout",
+    killme   => 120
+);
+__PACKAGE__->register_method(
+    method   => "circulation",
+    api_name => "fulfillment.laicore.circ.lender.checkin",
+    killme   => 120
+);
+__PACKAGE__->register_method(
+    method   => "circulation",
+    api_name => "fulfillment.laicore.circ.borrower.checkout",
+    killme   => 120
+);
+__PACKAGE__->register_method(
+    method   => "circulation",
+    api_name => "fulfillment.laicore.circ.borrower.checkin",
+    killme   => 120
+);
+sub circulation {
+    my ($self, $client, $ou, $item_ident, $user_barcode) = @_;
+
+    my $connector = FulfILLment::LAIConnector->load($ou) or return;
+
+    if ($self->api_name =~ /lender/) {
+        # the circulation on the lender side is always checked
+        # out to the circ proxy user.
+
+        $user_barcode = $connector->{'user.circ'} || $connector->{'user'};
+        if (!$user_barcode) {
+            $logger->error("FF proxy circ user defined for $ou");
+            return;
+        }
+    }
+
+    if ($self->api_name =~ /checkout/) {
+
+        return $connector->checkout_lender($item_ident, $user_barcode)
+            if $self->api_name =~ /lender/;
+
+        return $connector->checkout_borrower($item_ident, $user_barcode);
+    } 
+    
+    if ($self->api_name =~ /checkin/) {
+
+        return $connector->checkin_lender($item_ident, $user_barcode)
+            if $self->api_name =~ /lender/;
+
+        return $connector->checkin_borrower($item_ident, $user_barcode);
+    }
+
+    return $connector->get_circulation($item_ident, $user_barcode);
+}
+
+__PACKAGE__->register_method(
+    method   => "records_by_id",
+    api_name => "fulfillment.laicore.record_by_id",
+    killme   => 120
+);
+__PACKAGE__->register_method(
+    method   => "records_by_id",
+    api_name => "fulfillment.laicore.record_by_id.batch",
+    killme   => 120
+);
+sub records_by_id {
+    my ($self, $client, $ou, $ids) = @_;
+
+    my $connector = FulfILLment::LAIConnector->load($ou) or return;
+
+    return $connector->get_record_by_id_batch($ids)
+        if $self->api_name =~ /batch/;
+
+    return $connector->get_record_by_id($ids);
+}
+
+__PACKAGE__->register_method(
+    method   => "lookup_user",
+    api_name => "fulfillment.laicore.lookup_user",
+    killme   => 120
+);
+
+sub lookup_user {
+    my ($self, $client, $ou, $user_barcode, $user_pass) = @_;
+    my $connector = FulfILLment::LAIConnector->load($ou) or return;
+    return $connector->get_user($user_barcode, $user_pass);
+}
+
+__PACKAGE__->register_method(
+    method   => "import_items_by_record",
+    api_name => "fulfillment.laicore.import_items_by_record",
+    killme   => 120,
+    signature => q/
+        Import items from the remote site via remote record ID.
+        Returns true on success, false on failure.
+    /
+);
+sub import_items_by_record {
+    my ($self, $client, $ou, $record_id) = @_;
+    FulfILLment::LAIConnector->load($ou) or return;
+    my $e = new_editor();
+
+    # record_id param refers to the remote_id
+    my $rec = $e->search_biblio_record_entry(
+        {owner => $ou, remote_id => $record_id}
+    )->[0] or return;
+
+    return FulfILLment::AT::Reactor::ItemLoad->ByBib({target => $rec});
+}
+
+
+1;
+
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector.pm b/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector.pm
new file mode 100644 (file)
index 0000000..095be8b
--- /dev/null
@@ -0,0 +1,611 @@
+package FulfILLment::LAIConnector;
+use strict; use warnings;
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenILS::Application::AppUtils;
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use FulfILLment::Util::Z3950;
+use FulfILLment::Util::SIP2Client;
+use FulfILLment::Util::NCIP;
+my $U = 'OpenILS::Application::AppUtils';
+
+# Determines the correct connector to load for the provided org
+# unit and returns a ref to a new instance of the connector.
+# This is the main sub called by external modules.
+sub load {
+    my ($class, $org_id) = @_;
+
+    # collect all of the FF org unit settings for this org.
+    my $settings = new_editor()->search_config_org_unit_setting_type(
+        {name => {'like' => 'ff.remote.connector%'}}
+    );
+
+    my $cdata = { 
+        map {
+            $_->name => $U->ou_ancestor_setting($org_id, $_->name)
+        } @$settings
+    };
+
+    my %args = (org_id => $org_id, extra => {});
+
+    for my $key (keys %$cdata) {
+        my $setting = $cdata->{$key};
+        my $value = $setting->{value};
+        (my $newkey = $key) =~ s/^ff\.remote\.connector\.//;
+        if ($newkey =~ /^extra\./) {
+            $newkey =~ s/^extra\.//;
+            $args{extra}{$newkey} = $value;
+        } else {
+            $args{$newkey} = $value;
+        }
+    }
+
+    if (!$args{type}) {
+        $logger->error("No ILS type specifed for org unit $org_id");
+        return undef;
+    }
+
+    if ($args{disabled}) {
+        $logger->info("FF connector for ".$args{type}." disabled");
+        return;
+    }
+
+    return $class->_load_connector($args{type}, $args{version}, \%args);
+}
+
+# Returns a new LAIConnector for the specified type
+# and (optional) version.  All additional args are
+# passed through to the connector constructor.
+sub _load_connector {
+    my ($class, $type, $version, $args) = @_;
+
+    my $module = "FulfILLment::LAIConnector::$type";
+    $module .= "::$version" if $version;
+
+    $logger->info("FF loading ILS module org=$$args{org_id} $module");
+
+    # note: do not wrap in eval {}.  $module->use 
+    # uses eval internally and nested evals clobber $@
+    $module->use;
+
+    if ($@) {
+        $logger->error("Unable to load $module : $@");
+        return undef;
+    }
+
+    my $connector;
+    eval { $connector = $module->new($args) };
+
+    if ($@) {
+        $logger->error("Unable to create $module object : $@");
+        return undef;
+    }
+
+    if (!$connector->init) {
+        $logger->error("Error initializing connector $module");
+        return undef;
+    }
+
+    return $connector;
+}
+
+sub new {
+    my ($class, $args) = @_;
+    $args ||= {};
+    return bless($args, $class);
+}
+
+sub z39_client {
+    my $self = shift;
+
+    if (!$self->{z39_client}) {
+
+        my $host = $self->{extra}{'z3950.host'} || $self->{host};
+        my $port = $self->{extra}{'z3950.port'};
+        my $database = $self->{extra}{'z3950.database'};
+
+        unless ($host and $port and $database) {
+            $logger->info("FF Z39 not configured for $self, skipping...");
+            return;
+        }
+
+        $self->{z39_client} =
+            FulfILLment::Util::Z3950->new(
+                $host, $port, $database, 
+                $self->{extra}{'z3950.username'},
+                $self->{extra}{'z3950.password'}
+        );
+    }
+
+    return $self->{z39_client};
+}
+
+sub sip_client {
+    my $self = shift;
+
+    if (!$self->{sip_client}) {
+
+        my $host = $self->{extra}{'sip2.host'} || $self->{host};
+        my $port = $self->{extra}{'sip2.port'};
+
+        unless ($host and $port) {
+            $logger->info("FF SIP not configured for $self, skipping...");
+            return;
+        }
+
+        $self->{sip_client} =
+            FulfILLment::Util::SIP2Client->new(
+                $host, 
+                $self->{extra}{'sip2.username'},
+                $self->{extra}{'sip2.password'},
+                $port, 
+                $self->{extra}{'sip2.protocol'}, # undef == SOCK_STREAM
+                $self->{extra}{'sip2.institution'}
+        );
+    }
+
+    return $self->{sip_client};
+}
+
+sub ncip_client { 
+    my $self = shift;
+
+    if (!$self->{ncip_client}) {
+        my $host = $self->{extra}{'ncip.host'} || $self->{host};
+        my $port = $self->{extra}{'ncip.port'};
+
+        unless ($host and $port) {
+            $logger->info("FF NCIP not configured for $self, skipping...");
+            return;
+        }
+
+        $self->{ncip_client} = FulfILLment::Util::NCIP->new(
+            protocol => $self->{extra}->{'ncip.protocol'},
+            host => $host,
+            port => $port,
+            path => $self->{extra}->{'ncip.path'}, 
+            template_paths => ['/openils/var/ncip/v1'], # TODO
+            ils_agency_name => $self->{extra}->{'ncip.ils_agency.name'},
+            ils_agency_uri => $self->{extra}->{'ncip.ils_agency.uri'},
+            ff_agency_name => 'FulfILLment',
+            ff_agency_uri => 'http://fulfillment-ill.org/ncip/schemes/agency.scm'
+        );
+    }
+
+    return $self->{ncip_client};
+}
+
+
+# override with connector-specific initialization as needed
+# return true on success, false on failure
+sub init {
+    my $self = shift;
+    return 1;
+}
+
+# commonly accessed data
+
+# retursn the connector type (ff.remote.connector.type)
+sub type {
+    my $self = shift;
+    return $self->{type};
+}
+# returns the connector ILS version string (ff.remote.connector.version)
+sub version {
+    my $self = shift;
+    return $self->{version};
+}
+# returns the actor.org_unit.id for our current context org unit
+sub org_id {
+    my $self = shift;
+    return $self->{org_id};
+}
+# returns the actor.org_unit.shortname value for our current context org unit
+sub org_code {
+    my $self = shift;
+    return $self->{org_code} ?  $self->{org_code} :
+        $self->{org_code} = 
+            new_editor()->retrieve_actor_org_unit($self->org_id)->shortname;
+}
+
+
+# ----------------------------------------------------------------------------
+# Below are methods responsible for communicating with remote ILSes.  In some
+# cases, default implementations are provided.  This is only done when the 
+# implementation could reasonably by used by multiple connectors and only when
+# using SIP2 or Z39.50 as the communication layer.
+# 
+# Connectors should override each method as needed.
+# ----------------------------------------------------------------------------
+
+# returns one item
+# Default implementation uses SIP2
+sub get_item {
+    my ($self, $copy_barcode) = @_;
+
+    my $item = 
+        $self->sip_client->item_information_request($copy_barcode)
+        or return;
+
+    $item->{barcode} = $item->{item_identifier};
+    $item->{status} = $item->{circulation_status};
+    $item->{location_code} = $item->{permanent_location};
+
+    return $item;
+}
+
+# returns a list of items
+sub get_item_batch {
+    my ($self, $item_idents) = @_;
+    return [map {$self->get_item($_)} @$item_idents];
+}
+
+# returns a list of items
+sub get_items_by_record {
+    my ($self, $record_id) = @_;
+    return [];
+}
+
+# returns a list of items
+sub get_items_by_record_batch {
+    my ($self, $record_ids) = @_;
+    return [map {$self->get_items_by_record($_)} @$record_ids];
+}
+
+# returns one record
+sub get_record_by_id {
+    my ($self, $rec_id) = @_;
+    return;
+}
+
+# returns one record
+sub get_record_by_id_batch {
+    my ($self, $rec_ids) = @_;
+    return [];
+}
+
+# returns one record
+sub get_record_by_item {
+    my ($self, $item_ident) = @_;
+    return [];
+}
+
+# returns a list of records
+sub get_record_by_item_batch {
+    my ($self, $item_idents) = @_;
+    return [];
+}
+
+# returns 1 user.
+# Default implementation uses SIP2
+sub get_user {
+    my ($self, $user_barcode, $user_pass) = @_;
+
+    my $user = $self->sip_client->lookup_user({
+        patron_id => $user_barcode,
+        patron_pass => $user_pass
+    });
+
+    return unless $user;
+
+    $user->{user_barcode} = $user->{patron_identifier};
+    $user->{loaned_items} = $user->{charged_items};
+    $user->{loaned_items_count} = $user->{charged_items_count};
+    $user->{loaned_items_limit} = $user->{charged_items_limit};
+    $user->{lang_pref} = $user->{language};
+    $user->{phone} = $user->{home_phone_number};
+
+    $user->{billing_address} = $user->{home_address};
+    $user->{mailing_address} = $user->{home_address};
+
+    return $user;
+}
+
+# returns a list of holds
+sub get_item_holds {
+    my ($self, $item_ident) = @_;
+    return [];
+}
+
+# TODO: docs
+sub place_borrower_hold {
+    my ($self, $item_barcode, $user_barcode, $pickup_lib) = @_;
+}
+
+# TODO: docs
+sub delete_borrower_hold {
+    my ($self, $item_barcode, $user_barcode) = @_;
+}
+
+# TODO: docs
+sub place_lender_hold {
+    my ($self, $item_barcode, $user_barcode, $pickup_lib) = @_;
+}
+
+# TODO: docs
+sub delete_lender_hold {
+    my ($self, $item_barcode, $user_barcode) = @_;
+}
+
+
+# ---------------------------------------------------------------------------
+# Provide a default hold placement via SIP
+# ---------------------------------------------------------------------------
+sub place_hold_via_sip {
+    my $self = shift;
+    my $bib_id = shift || '';
+    my $copy_barcode = shift || '';
+    my $user_barcode = shift || '';
+    my $pickup_lib = shift || '';
+    my $expire_date = shift;
+    my $hold_type = shift;
+
+    if (!$hold_type) {
+        # if no hold type is provided, assume passing 
+        # a barcode implies a copy-level hold
+
+        # 2 == bib hold
+        # 3 == copy hold
+        $hold_type = $copy_barcode ? 3 : 2;
+    }
+
+    if (!$expire_date) {
+        $expire_date =  # interval should be a setting?
+            DateTime->now->add({months => 6})->strftime("%Y%m%d    000000");
+    }
+
+    if (!$pickup_lib) {
+        # use the home org unit of the requesting user 
+        # as the pickup lib if none is provided
+        my $user = $self->flesh_user($user_barcode);
+        $pickup_lib = $user->home_ou->shortname if $user;
+    }
+
+    $logger->warn("FF has no pickup lib for $user_barcode") if !$pickup_lib;
+
+    $logger->info("FF placing hold copy=$copy_barcode; ".
+        "pickup_lib=$pickup_lib; bib=$bib_id; ".
+        "user=$user_barcode; expire=$expire_date");
+
+    my $resp = $self->sip_client->place_hold($user_barcode, undef, 
+        $expire_date, $pickup_lib, $hold_type, $copy_barcode, $bib_id);
+
+    return undef unless $resp;
+
+    my $blob = $self->translate_sip_hold($resp);
+    $blob->{bibID} = $bib_id;
+    $blob->{hold_type} = $bib_id ? 'T' : 'C';
+
+    return undef if $blob->{error};
+    return $blob;
+}
+
+sub translate_sip_hold {
+    my ($self, $sip_msg) = @_;
+
+    my $fields = $sip_msg->{fields};
+    my $fixed_fields = $sip_msg->{fixed_fields};
+
+    # TODO: verify returned format is sane
+
+    return {
+        error => !$fixed_fields->[0],
+        error_message => $fields->{AF},
+        success_message => $fields->{AF},
+        expire_time => $fields->{BW},
+        expires => $fields->{BW},
+        placed_on => $fixed_fields->[2],
+        request_time => $fixed_fields->[2],
+        status => $fixed_fields->[1] eq 'Y' ? 'Available' : 'Pending',
+        title => $fields->{AJ},
+        barcode => $fields->{AB},
+        itemid => $fields->{AB},
+        pickup_lib => $fields->{BS}
+    };
+}
+
+# given a copy barcode, this will return the copy whose source lib
+# matches my org unit fleshed with its call number and bib record
+sub flesh_copy {
+    my ($self, $copy_barcode) = @_;
+
+    # find the FF copy so we can get the copy's record_id
+    my $copy = new_editor()->search_asset_copy([
+        {   barcode => $copy_barcode, source_lib => $self->org_id},
+        {   flesh => 2,
+            flesh_fields => {
+                acp => ['call_number'],
+                acn => ['record']
+            }
+        }
+    ])->[0];
+
+    return $copy ? $copy : undef;
+}
+
+
+# given a user barcode, this will return the use whose home lib
+# is at or below my org unit, fleshed with home_ou
+sub flesh_user {
+    my ($self, $user_barcode) = @_;
+    
+    my $cards = new_editor()->search_actor_card([
+        {   barcode => $user_barcode,
+            org => $U->get_org_descendants($self->org_id)
+        }, {
+            flesh => 2,
+            flesh_fields => {ac => ['usr'], au => ['home_ou']}
+        }
+    ]);
+
+    return @$cards ? $cards->[0]->usr : undef;
+}
+
+
+
+
+# returns one hold
+# pickup_lib is the library code (org_unit.shortname)
+sub place_item_hold {
+    my ($self, $item_ident, $user_barcode, $pickup_lib) = @_;
+    return;
+}
+
+# returns one hold
+# pickup_lib is the library code (org_unit.shortname)
+sub place_record_hold {
+    my ($self, $rec_id, $user_barcode, $pickup_lib) = @_;
+    return;
+}
+
+# returns one hold
+# pickup_lib is the library code (org_unit.shortname)
+sub delete_item_hold {
+    my ($self, $item_ident, $user_barcode, $pickup_lib) = @_;
+    return;
+}
+
+# returns one hold
+# pickup_lib is the library code (org_unit.shortname)
+sub delete_record_hold {
+    my ($self, $rec_id, $user_barcode, $pickup_lib) = @_;
+    return;
+}
+
+# returns a list of holds
+sub get_record_holds {
+    my ($self, $rec_id) = @_;
+    return [];
+}
+
+# ---------------------------------------------------------------------------
+# Allow connectors to provide lender vs. borrower checkout and checkin 
+# handling.  Call the stock checkout/checkin methods by default.
+# ---------------------------------------------------------------------------
+sub checkout_lender {
+    my $self = shift;
+    return $self->checkout(@_);
+}
+sub checkout_borrower {
+    my $self = shift;
+    return $self->checkout(@_);
+}
+sub checkin_lender {
+    my $self = shift;
+    return $self->checkin(@_);
+}
+sub checkin_borrower {
+    my $self = shift;
+    return $self->checkin(@_);
+}
+
+# ---------------------------------------------------------------------------
+# Provide default checkout and checkin routines via SIP.
+# Override with connector-specific behavior as needed.
+# ---------------------------------------------------------------------------
+sub checkout {
+    my ($self, $item_barcode, $user_barcode) = @_;
+    return unless $self->sip_client;
+    my $resp = $self->sip_client->checkout($user_barcode, undef, $item_barcode);
+    return $self->sip_client->sip_msg_to_circ($resp, 'checkout');
+}
+
+sub checkin {
+    my ($self, $item_barcode, $user_barcode) = @_;
+    return unless $self->sip_client;
+    my $resp = $self->sip_client->checkin($user_barcode, undef, $item_barcode);
+    return $self->sip_client->sip_msg_to_circ($resp);
+}
+
+
+# ---------------------------------------------------------------------------
+
+# returns one circulation
+sub get_circulation {
+    my ($self, $item_ident, $user_barcode) = @_;
+    return;
+}
+
+# ---------------------------------------------------------------------------
+# Reference copy is the asset.copy hold target for the lender hold,
+# fleshed with ->call_number->record.  The borrower copy is a temporary / 
+# dummy copy created at the borrowing library for the purposes of 
+# hold placement and circulation at the borrowing library.
+# ---------------------------------------------------------------------------
+sub create_borrower_copy {
+    my ($self, $reference_copy, $circ_lib_code) = @_;
+}
+
+# --------------------------------------------------
+# -- these method have no corresponding API calls --
+# -- their purpose is unclear                     --
+
+sub item_get_page {
+    # TODO: params?
+    return [];
+}
+sub item_get_range {
+    # TODO: params?
+    return [];
+}
+sub resource_get_page {
+    # TODO: params
+    return [];
+}
+sub resource_get_range {
+    # TODO: params
+    return [];
+}
+sub resource_get_actor_relation {
+    # TODO: params
+    return [];
+}
+sub resource_get_total_pages {
+    # TODO: params
+    return [];
+}
+sub resource_get_on_date {
+    # TODO: params
+    return [];
+}
+sub resource_get_after_date {
+    # TODO: params
+    return [];
+}
+sub resource_get_before_date {
+    # TODO: params
+    return [];
+}
+sub actor_get_range {
+    # TODO: params
+    return [];
+}
+sub actor_get_page {
+    # TODO: params
+    return [];
+}
+sub actor_get_total_pages {
+    # TODO: params
+    return [];
+}
+sub actor_list_holds {
+    my ($self, $user_barcode) = @_;
+    return [];
+}
+sub get_results_per_page {
+    # TODO: params
+    return [];
+}
+sub get_host_ills {
+    # TODO: params
+    return;
+}
+sub item_get_total_pages {
+    # TODO: params?
+    return [];
+}
+sub item_get_all {
+    # TODO: params?
+    return [];
+}
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Aleph.pm b/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Aleph.pm
new file mode 100644 (file)
index 0000000..fba7816
--- /dev/null
@@ -0,0 +1,6 @@
+package FulfILLment::LAIConnector::Aleph;
+use base FulfILLment::LAIConnector;
+use strict; use warnings;
+use OpenSRF::Utils::Logger qw/$logger/;
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Evergreen.pm b/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Evergreen.pm
new file mode 100644 (file)
index 0000000..4abbca9
--- /dev/null
@@ -0,0 +1,396 @@
+package FulfILLment::LAIConnector::Evergreen;
+use base FulfILLment::LAIConnector;
+use strict; use warnings;
+use OpenSRF::Utils::Logger qw/$logger/;
+use Digest::MD5 qw(md5_hex);
+use LWP::UserAgent;
+use HTTP::Request;
+use JSON::XS;
+use Data::Dumper;
+
+my $json = JSON::XS->new;
+$json->allow_nonref(1);
+
+my $ua = LWP::UserAgent->new;
+$ua->agent("FulfILLment/1.0");
+
+sub gateway {
+    my ($self, $service, $method, @args) = @_;
+
+    my $url = sprintf(
+        'https://%s/osrf-gateway-v1?service=%s&method=%s',
+        $self->{host}, $service, $method
+    );
+    $url .= '&param=' . $json->encode($_) for (@args);
+
+    $logger->debug("FF Evergreen gateway request => $url");
+
+    my $req = HTTP::Request->new('GET' => $url);
+    my $res = $ua->request($req);
+
+    if (!$res->is_success) {
+        $logger->error(
+            "FF Evergreen gateway request error [HTTP ".$res->code."] $url");
+        return undef;
+    }
+
+    my $value = decode_json($res->content);
+    return $$value{payload} if $$value{status} == 200;
+}
+
+# --------------------------------------------------------------------
+# Login 
+# TODO: support authtoken re-use (and graceful recovery) for 
+# faster batches of actions
+# Always assumes barcode-based login.
+# --------------------------------------------------------------------
+sub login {
+    my $self = shift;
+    my $username = shift || $self->{user};
+    my $password = shift || $self->{passwd};
+    my $type = shift;
+
+    my $json = $self->gateway(
+        'open-ils.fulfillment',
+        'fulfillment.connector.login',
+        undef, $password, $type, $username
+    );
+
+    my $auth = $$json[0];
+    $logger->info("EG: login failed for $username") unless $auth;
+    return $self->{authtoken} = $auth;   
+}
+
+sub get_user {
+    my $self = shift;
+    my $user_barcode = shift;
+    my $user_password = shift;
+
+    my $resp = $self->gateway(
+        'open-ils.fulfillment',
+        'fulfillment.connector.verify_user_by_barcode',
+        $user_barcode, $user_password
+    );
+
+    # TODO: we always assume barcode logins in FF, but it would be
+    # nice if the EG connector could safey fall-through to username
+    # logins.  Care must be taken to prevent multiple accounts, one 
+    # for the barcode and one for the username.
+
+    unless ($resp and $resp->[0]) {
+        $logger->info("EG: unable to verify user $user_barcode");
+        return [];
+    }
+
+    my $data = $resp->[0];
+
+    $logger->info("Evergreen retreived user " . Dumper($data));
+
+    $data->{surname} = $data->{family_name};
+    $data->{user_id} = $data->{id};
+    $data->{given_name} = $data->{first_given_name};
+    $data->{exp_date} = $data->{expire_date};
+    $data->{user_barcode} = ref($data->{card}) ? 
+        $data->{card}->{barcode} : $user_barcode;
+
+    return $data;
+}
+
+sub get_items_by_record {
+    my ($self, $record_id) = @_;
+
+    my $auth = $self->login or return [];
+
+    my $resp = $self->gateway(
+        'open-ils.fulfillment', 
+        'fulfillment.connector.copy_tree',
+        $auth, $record_id
+    );
+    
+    my $cns = $resp->[0];
+    my @copies = map {@{$_->{copies}}} @$cns;
+    $_->{bib_id} = $_->{record_id} = $record_id for @copies;
+    return \@copies;
+}
+
+sub get_record_by_id {
+    my ($self, $record_id) = @_;
+
+    my $url = sprintf(
+        "http://%s/opac/extras/supercat/retrieve/marcxml/record/%s",
+        $self->{host}, $record_id
+    );
+
+    $logger->info("FF EG get_record_by_id() => $url");
+
+    my $req = HTTP::Request->new("GET" => $url);
+    my $res = $ua->request($req);
+
+    if (!$res->is_success) {
+        $logger->error(
+            "FF Evergreen gateway request error [HTTP ".$res->code."] $url");
+        return undef;
+    }
+
+    return {
+        marc => $res->content,
+        error => 0,
+        id => $record_id
+    };
+}
+
+sub get_item {
+    my ($self, $barcode) = @_;
+
+    my $auth = $self->login;
+
+    # TODO add fields as needed
+    my %fields;
+    for my $field (qw/
+        id circ_lib barcode location status holdable circulate/) {
+        $fields{$field} = {path => $field, display => 1};
+    }
+
+    $fields{call_number} = {path => 'call_number.label', display => 1};
+    $fields{record_id} = {path => 'call_number.record', display => 1};
+
+    my $resp = $self->gateway(
+        'open-ils.fielder',
+        'open-ils.fielder.flattened_search',
+        $auth, 'acp', \%fields, {barcode => $barcode}
+    );
+
+    my $copy = $resp->[0];
+    $copy->{bib_id} = $copy->{record_id};
+
+    return $resp->[0];
+}
+
+sub get_record_holds {
+    my ($self, $record_id) = @_;
+    my $auth = $self->login;
+    
+    # TODO: xmlrpc is dead
+    #my $resp = $self->request(
+    #'open-ils.circ',
+    #'open-ils.circ.holds.retrieve_all_from_title',
+    #$key,
+    #$bibID,
+    #)->value;
+}
+
+sub place_lender_hold {
+    my $self = shift;
+    return $self->place_borrower_hold(@_);
+}
+
+sub place_borrower_hold {
+    my ($self, $copy_barcode, $user_barcode, $pickup_lib) = @_;
+
+    my $auth = $self->login or return;
+
+    my $resp = $self->gateway(
+        'open-ils.fulfillment',
+        'fulfillment.connector.create_hold',
+        $auth, $copy_barcode, $user_barcode
+    );
+
+
+    $logger->debug("FF Evergreen item hold for copy=$copy_barcode ".
+        "user=$user_barcode resulted in ".Dumper($resp));
+
+    # NOTE: fulfillment.connector.create_hold only returns 
+    # the hold ID and not the hold object.  is that enough?
+    return $$resp[0] if $resp and @$resp;
+    return;
+}
+
+sub delete_lender_hold {
+    my $self = shift;
+    return $self->delete_borrower_hold(@_);
+}
+
+sub delete_borrower_hold {
+    my ($self, $copy_barcode, $user_barcode) = @_;
+
+    my $auth = $self->login or return;
+
+    my $resp = $self->gateway(
+        'open-ils.fulfillment',
+        'fulfillment.connector.cancel_oldest_hold',
+        $auth, $copy_barcode
+    );
+
+    # NOTE: fulfillment.connector.cancel_oldest_hold only 
+    # returns success or failure.  is that enough?
+    return $resp and @$resp and $$resp[0];
+}
+
+sub create_borrower_copy {
+    my ($self, $ref_copy, $ou_code) = @_;
+
+    my $auth = $self->login or return;
+    
+    my $resp = $self->gateway(
+        'open-ils.fulfillment',
+        'fulfillment.connector.create_borrower_copy',
+        $auth, $ou_code, $ref_copy->barcode, {
+            title => $ref_copy->call_number->record->simple_record->title,
+            author => $ref_copy->call_number->record->simple_record->author,
+            isbn => $ref_copy->call_number->record->simple_record->isbn,
+        }
+    );
+
+    my $copy = $resp->[0] if $resp;
+
+    unless ($copy) {
+        $logger->error(
+            "FF unable to create borrower copy for ".$ref_copy->barcode);
+        return;
+    }
+
+    $logger->debug("FF created borrower copy " . Dumper($copy));
+
+    return $copy;
+}
+
+# borrower checkout uses a precat copy
+sub checkout_borrower {
+    my ($self, $copy_barcode, $user_barcode) = @_;
+
+    my $args = {
+        copy_barcode => $copy_barcode, 
+        patron_barcode => $user_barcode,
+        request_precat => 1
+    };
+
+    return $self->_perform_checkout($args);
+}
+
+# to date, lender checkout requires no special handling
+sub checkout_lender {
+    my ($self, $copy_barcode, $user_barcode) = @_;
+
+    my $args = {
+        copy_barcode => $copy_barcode, 
+        patron_barcode => $user_barcode
+    };
+
+    return $self->_perform_checkout($args);
+}
+
+# ---------------------------------------------------------------------------
+# attempts a checkout.  
+# if the checkout fails with a COPY_IN_TRANSIT event, abort the transit and
+# attempt the checkout again.
+# ---------------------------------------------------------------------------
+sub _perform_checkout {
+    my ($self, $args) = @_;
+    my $auth = $self->login or return;
+    my $copy_barcode = $args->{copy_barcode};
+
+    my $resp = $self->_send_checkout($auth, $args) or return;
+
+    if ($resp->{textcode} eq 'COPY_IN_TRANSIT') {
+        # checkout of in-transit copy attempted.  We really want this
+        # copy, so let's abort the transit, then try again.
+
+        $logger->info("FF EG attempting to abort ".
+            "transit on $copy_barcode for checkout");
+
+        my $resp2 = $self->gateway(
+            'open-ils.circ',
+            'open-ils.circ.transit.abort',
+            $auth, {barcode => $copy_barcode}
+        );
+
+        if ($resp2 and $resp2->[0] eq '1') {
+            $logger->info(
+                "FF EG successfully aborted transit for $copy_barcode");
+
+            # re-do the checkout
+            $resp = $self->_send_checkout($auth, $args);
+
+        } else {
+            $logger->warn("FF EG unable to abort transit on checkout");
+            return;
+        }
+
+    } 
+
+    return $resp;
+}
+
+sub _send_checkout {
+    my ($self, $auth, $args) = @_;
+
+    my $resp = $self->gateway(
+        'open-ils.circ',
+        'open-ils.circ.checkout.full.override',
+        $auth, $args
+    );
+
+    if ($resp) {
+        # gateway returns an array
+        if ($resp = $resp->[0]) {
+            # circ may return an array of events; use the first.
+            $resp = $resp->[0] if ref($resp) eq 'ARRAY';
+            $logger->info("FF EG checkout returned event ".$resp->{textcode});
+            return $resp;
+        }
+    }
+
+    $logger->error("FF EG checkout failed to return a response");
+    return;
+}
+
+
+sub checkin {
+    my ($self, $copy_barcode, $user_barcode) = @_;
+    my $auth = $self->login or return;
+
+    # we want to check the item in at the 
+    # correct location or it will go into transit.
+    my $fields = {
+        id => {path => 'id', display => 1},
+        shortname => {path => 'shortname', display => 1}
+    };
+
+    my $resp = $self->gateway(
+        'open-ils.fielder',
+        'open-ils.fielder.flattened_search',
+        $auth, 'aou', $fields, {shortname => $self->org_code}
+    );
+
+    my $checkin_args = {copy_barcode => $copy_barcode};
+
+    if ($resp and $resp->[0]) {
+        $logger->debug("FF EG found org ".Dumper($resp));
+        $checkin_args->{circ_lib} = $resp->[0]->{id};
+    } else {
+        $logger->warn("FF EG unable to locate org unit ".
+            $self->org_code.", passing no circ_lib on checkin");
+    }
+
+    $resp = $self->gateway(
+        'open-ils.circ',
+        'open-ils.circ.checkin.override',
+        $auth, $checkin_args
+    );
+
+    if ($resp) {
+        # gateway returns an array
+        if ($resp = $resp->[0]) {
+            # circ may return an array of events; use the first.
+            $resp = $resp->[0] if ref($resp) eq 'ARRAY';
+            $logger->info("FF EG checkin returned event ".$resp->{textcode});
+            return $resp;
+        }
+    }
+
+    $logger->error("FF EG checkin failed to return a response");
+    return;
+}
+
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Evergreen/FulfILLment_EGAPP.pm b/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Evergreen/FulfILLment_EGAPP.pm
new file mode 100644 (file)
index 0000000..25d5242
--- /dev/null
@@ -0,0 +1,489 @@
+#
+#===============================================================================
+#
+#         FILE: FF-evergreen-app.pm
+#
+#  DESCRIPTION: 
+#
+#        FILES: ---
+#         BUGS: ---
+#        NOTES: ---
+#       AUTHOR: Michael Davadrian Smith (), msmith@esilibrary.com, Mike Rylander, miker@esilibrary.com
+#      COMPANY: Equinox Software
+#      VERSION: 1.0
+#      CREATED: 01/29/2013 03:27:04 PM
+#     REVISION: ---
+#===============================================================================
+
+use strict;
+use warnings;
+
+package OpenILS::Application::FulfILLment_EGAPP;
+
+use OpenILS::Application;
+use base qw/OpenILS::Application/;
+
+use OpenSRF::EX qw(:try);
+use OpenSRF::Utils::SettingsClient;
+use OpenSRF::Utils::Logger qw($logger);
+
+use OpenILS::Utils::Fieldmapper;
+use OpenILS::Const qw/:const/;
+use OpenILS::Application::AppUtils;
+my $U = 'OpenILS::Application::AppUtils';
+
+use OpenILS::Utils::CStoreEditor q/:funcs/;
+use Digest::MD5 qw(md5_hex);
+
+sub login {
+    my( $self, $client, $username, $password, $type, $barcode ) = @_;
+
+    $type |= "staff";
+
+    my $seed = $U->simplereq(
+        'open-ils.auth',
+        'open-ils.auth.authenticate.init',
+        $username || $barcode
+    );
+
+    $logger->info("No auth seed. Couldn't talk to the auth server") unless $seed;
+
+    my $args = { 
+        username => $username, 
+        agent => 'fulfillment',
+        password => md5_hex($seed . md5_hex($password)),
+        type => $type 
+    };
+
+    if ($barcode) {
+        delete $args->{username};
+        $args->{barcode} = $barcode;
+    }
+
+    my $response = $U->simplereq(
+        'open-ils.auth',
+        'open-ils.auth.authenticate.complete',
+        $args
+    );
+
+    $logger->info("No auth response returned on login.") unless $response;
+
+    my $authtime = $response->{payload}->{authtime};
+    my $authtoken = $response->{payload}->{authtoken};
+
+    my $ident = $username || $barcode;
+    $logger->info("Login failed for user $ident!") unless $authtoken;
+
+    return $authtoken || '';
+}   
+
+__PACKAGE__->register_method(
+    method => "login",
+    api_name => "fulfillment.connector.login",
+    signature => {
+            desc => "Authenticate the requesting user",
+            params => [
+                { name => 'username', type => 'string' },
+                { name => 'passwd', type => 'string' },
+                { name => 'type', type => 'string' }
+            ],
+            'return' => {
+                desc => 'Returns an authentication token on success, or an empty string on failure'
+            }
+    }
+);
+
+sub lookup_user {
+    my ($self, $client, $authtoken, $keys, $value) = @_;
+
+    $keys = [$keys] if (!ref($keys));
+
+    ($authtoken) = $self
+        ->method_lookup( 'fulfillment.connector.login' )
+        ->run(@$authtoken)
+            if (ref $authtoken);
+
+    my $e = new_editor(authtoken => $authtoken);
+
+    return undef unless $e->checkauth;
+
+    for my $k ( @$keys ) {
+        my $users = $e->search_actor_user([
+            { $k => $value },
+            {flesh => 1, flesh_fields => {au => ['card']}}
+        ]);
+
+        if ($users->[0]) {
+
+            # user's are allowed to retrieve their own accounts
+            # regardless of proxy permissions
+            return recursive_hash($users->[0]) 
+                if $users->[0]->id eq $e->requestor->id;
+
+            # all other user retrievals require proxy user permissions
+            return undef unless $e->allowed('fulfillment.proxy_user');
+            return recursive_hash($users->[0]);
+        }
+    }
+
+    return undef;
+}
+
+__PACKAGE__->register_method(
+    method => "lookup_user",
+    api_name => "fulfillment.connector.lookup_user",
+    signature => {
+            desc => "Retrieve a user hash",
+            params => [
+                { name => 'authtoken', type => 'string', desc => 'Either a valid auth token OR an arrayref containing a username and password to log in as' },
+                { name => 'keys', type => 'string', 'One or more fields against which to attempt matching the retrieval value, such as "id" or "usrname"' },
+                { name => 'lookup_value', type => 'string' }
+            ],
+            'return' => {
+                desc => 'Returns a user hash on success, or nothing on failure'
+            }
+    }
+);
+
+sub verify_user_by_barcode {
+    my ($self, $client, $user_barcode, $user_password) = @_;
+
+    my ($authtoken) = $self
+        ->method_lookup( 'fulfillment.connector.login' )
+        ->run(undef, $user_password, 'temp', $user_barcode);
+
+    return undef unless $authtoken;
+
+    my $user = $U->simplereq(
+        'open-ils.auth',
+        'open-ils.auth.session.retrieve', 
+        $authtoken
+    );
+
+    return recursive_hash($user);
+}
+
+__PACKAGE__->register_method(
+    method => "verify_user_by_barcode",
+    api_name => "fulfillment.connector.verify_user_by_barcode",
+    signature => {
+            desc => q/Given a user barcode and password, returns the 
+                user hash if the barcode+password combo is valid/,
+            params => [
+                {name => 'barcode', type => 'string', desc => 'User barcode'},
+                {name => 'password', type => 'string', desc => 'User password'}
+            ],
+            'return' => {
+                desc => 'Returns a user hash on success, or nothing on failure'
+            }
+    }
+);
+
+
+sub lookup_holds {
+    my ($self, $client, $authtoken, $uid) = @_;
+
+    ($authtoken) = $self
+        ->method_lookup( 'fulfillment.connector.login' )
+        ->run(@$authtoken)
+            if (ref $authtoken);
+
+    my $e = new_editor(authtoken => $authtoken);
+
+    return undef unless $e->checkauth;
+    return undef unless $e->allowed('fulfillment.proxy_user');
+
+    $uid ||= $e->requestor;
+
+    my $holds = $e->search_action_hold_request([
+        { usr => $uid, capture_time => undef, cancel_time => undef },
+        { order_by => { ahr => 'request_time'  } }
+    ]);
+
+    return recursive_hash($holds);
+}
+
+__PACKAGE__->register_method(
+    method => "lookup_holds",
+    api_name => "fulfillment.connector.lookup_holds",
+    signature => {
+            desc => "Retrieve a list of open holds for a user",
+            params => [
+                { name => 'authtoken', type => 'string', desc => 'Either a valid auth token OR an arrayref containing a username and password to log in as' }
+            ],
+            'return' => {
+                desc => 'Returns an array of hold hashes on success, or nothing on failure'
+            }
+    }
+);
+
+sub copy_detail {
+    my ($self, $client, $authtoken, $barcode ) = @_;
+
+    ($authtoken) = $self
+        ->method_lookup( 'fulfillment.connector.login' )
+        ->run(@$authtoken)
+            if (ref $authtoken);
+
+    my $e = new_editor(authtoken => $authtoken);
+    return undef unless $e->checkauth;
+    return undef unless $e->allowed('fulfillment.proxy_user');
+
+    my $tree =  $U->simplereq('open-ils.circ', 'open-ils.circ.copy_details.retrieve.barcode', $authtoken, $barcode);
+
+    return recursive_hash($tree);
+}
+
+__PACKAGE__->register_method(
+    method => "copy_detail",
+    api_name => "fulfillment.connector.copy_detail",
+    signature => {
+            desc => "Fetch a copy tree by bib id, optionally org-scoped",
+            params => [
+                { name => 'authtoken', type => 'string', desc => 'Either a valid auth token OR an arrayref containing a username and password to log in as' },
+                { name => 'barcode', type => 'string', desc => 'Copy barcode' },
+            ],
+            'return' => {
+                desc => 'Returns a fleshed copy on success, or nothing on failure'
+            }
+    }
+);
+
+sub copy_tree {
+    my ($self, $client, $authtoken, $bib, @orgs ) = @_;
+
+    ($authtoken) = $self
+        ->method_lookup( 'fulfillment.connector.login' )
+        ->run(@$authtoken)
+            if (ref $authtoken);
+
+    my $e = new_editor(authtoken => $authtoken);
+    return undef unless $e->checkauth;
+    return undef unless $e->allowed('fulfillment.proxy_user');
+
+    my $tree;
+    if (@orgs) {
+        $tree =  $U->simplereq('open-ils.cat', 'open-ils.cat.asset.copy_tree.retrieve', $authtoken, $bib, @orgs);
+    } else {
+        $tree =  $U->simplereq('open-ils.cat', 'open-ils.cat.asset.copy_tree.global.retrieve', $authtoken, $bib);
+    }
+
+    return recursive_hash($tree);
+}
+
+__PACKAGE__->register_method(
+    method => "copy_tree",
+    api_name => "fulfillment.connector.copy_tree",
+    signature => {
+            desc => "Fetch a copy tree by bib id, optionally org-scoped",
+            params => [
+                { name => 'authtoken', type => 'string', desc => 'Either a valid auth token OR an arrayref containing a username and password to log in as' },
+                { name => 'bib_id', type => 'string', desc => 'Bib id to fetch copies from' },
+                { name => 'org', type => 'string', desc => 'Org id for copy scoping; repeatable' },
+            ],
+            'return' => {
+                desc => 'Returns a CN-CP tree on success, or nothing on failure'
+            }
+    }
+);
+
+sub recursive_hash {
+    my $obj = shift;
+
+    if (ref($obj)) {
+        if (ref($obj) =~ /Fieldmapper/) {
+            $obj = $obj->to_bare_hash;
+            $$obj{$_} = recursive_hash($$obj{$_}) for (keys %$obj);
+        } elsif (ref($obj) =~ /ARRAY/) {
+            $obj = [ map { recursive_hash($_) } @$obj ];
+        } else {
+            $$obj{$_} = recursive_hash($$obj{$_}) for (keys %$obj);
+        }
+    }
+
+    return $obj;
+}
+
+
+sub create_hold {
+    my ($self, $client, $authtoken, $copy_bc, $patron_bc) = @_;
+
+    ($authtoken) = $self
+        ->method_lookup( 'fulfillment.connector.login' )
+        ->run(@$authtoken)
+            if (ref $authtoken);
+
+    my $e = new_editor(authtoken => $authtoken);
+    return undef unless $e->checkauth;
+    return undef unless $e->allowed('fulfillment.proxy_user');
+
+    my $patron = $e->requestor->id;
+
+    if ($patron_bc) {
+        my $p = $e->search_actor_card({barcode => $patron_bc})->[0];
+        $patron = $p->id if $p;
+    }
+
+    my $copy = $e->search_asset_copy({barcode => $copy_bc, deleted => 'f'})->[0];
+    return undef unless ($copy);
+
+    my $hold = new Fieldmapper::action::hold_request;
+    $hold->usr($patron);
+    $hold->target($copy->id);
+    $hold->hold_type('C');
+    $hold->pickup_lib($copy->circ_lib);
+
+    my $resp =  $U->simplereq('open-ils.circ', 'open-ils.circ.holds.create.override', $authtoken, $hold);
+
+    return undef if (ref $resp);
+    return $resp;
+}
+
+__PACKAGE__->register_method(
+    method => "create_hold",
+    api_name => "fulfillment.connector.create_hold",
+    signature => {
+            desc => "Create a new hold",
+            params => [
+                { name => 'authtoken', type => 'string', desc => 'Either a valid auth token OR an arrayref containing a username and password to log in as' },
+                { name => 'copy_bc', type => 'string', desc => 'Copy barcode on which to place a hold' },
+                { name => 'patron_bc', type => 'string', desc => 'Patron barcode as which to place a hold, if different from calling user' },
+            ],
+            'return' => {
+                desc => 'Returns a hold id on success, or nothing on failure'
+            }
+    }
+);
+
+sub cancel_proxy_hold {
+    my ($self, $client, $authtoken, $copy_bc) = @_;
+
+    ($authtoken) = $self
+        ->method_lookup( 'fulfillment.connector.login' )
+        ->run(@$authtoken)
+            if (ref $authtoken);
+
+    my $e = new_editor(authtoken => $authtoken);
+    return undef unless $e->checkauth;
+    return undef unless $e->allowed('fulfillment.proxy_user');
+
+    my ($holds) = $self
+        ->method_lookup( 'fulfillment.connector.lookup_holds' )
+        ->run($authtoken);
+
+    my $copy = $e->search_asset_copy({barcode => $copy_bc, deleted => 'f'})->[0];
+    return undef unless ($copy);
+
+    $holds = [ grep { $_->{target} == $copy->id } @$holds ];
+
+    my $resp =  $U->simplereq('open-ils.circ', 'open-ils.circ.hold.cancel', $authtoken, $holds->[-1]->{id}) if (@$holds);
+
+    return undef if (ref $resp);
+    return $resp;
+}
+
+__PACKAGE__->register_method(
+    method => "cancel_proxy_hold",
+    api_name => "fulfillment.connector.cancel_oldest_hold",
+    signature => {
+            desc => "Retrieve a list of open holds for a user",
+            params => [
+                { name => 'authtoken', type => 'string', desc => 'Either a valid auth token OR an arrayref containing a username and password to log in as' },
+                { name => 'copy_bc', type => 'string', desc => 'Copy barcode against which to cancel the oldest hold' },
+            ],
+            'return' => {
+                desc => 'Returns 1 on success, or nothing on failure'
+            }
+    }
+);
+
+
+__PACKAGE__->register_method(
+    method => "create_borrower_copy",
+    api_name => "fulfillment.connector.create_borrower_copy",
+    signature => {
+        desc => "Creates a pre-cat copy for borrower holds/circs",
+        params => [
+            {   name => 'authtoken', 
+                type => 'string', 
+                desc => q/Either a valid auth token OR an arrayref 
+                    containing a username and password to log in as/ 
+            },
+            {   name => 'ou_code', 
+                type => 'string', 
+                desc => 'org_unit shortname to use as the copy circ lib' 
+            },
+            {   name => 'barcode',
+                type => 'string',
+                desc => q/copy barcode.  Note, if a barcode collision 
+                    occurs the barcode of the final copy may be different/,
+            },
+            {   name => 'args', 
+                type => 'hash', 
+                desc => 'Hash of optional extra information: title, author, isbn'
+            }
+        ],
+        'return' => {
+            desc => 'Copy object (hash) or nothing on failure'
+        }
+    }
+);
+
+sub create_borrower_copy {
+    my ($self, $client, $auth, $ou_code, $barcode, $args) = @_;
+    $args ||= {};
+
+    ($auth) = $self
+        ->method_lookup('fulfillment.connector.login')
+        ->run(@$auth) if ref $auth;
+
+    my $e = new_editor(authtoken => $auth, xact => 1);
+
+    my $e_copy = $e->search_asset_copy(
+        {deleted => 'f', barcode => $barcode});
+
+    # copy with the requested barcode already exists.
+    # Add a prefix to the barcode. 
+    # TODO: make the prefix a setting
+    # TODO: maybe all such copies should be given a prefix for consistency
+    $barcode = "FF$barcode" if @$e_copy;
+
+    my $circ_lib = $e->search_actor_org_unit({shortname => $ou_code});
+    if (!@$circ_lib) {
+        $logger->error("Unable to locate org unit '$ou_code'");
+        $e->rollback;
+        return;
+    }
+
+    my $copy = Fieldmapper::asset::copy->new;
+    $copy->barcode($barcode);
+    $copy->circ_lib($circ_lib->[0]->id);
+    $copy->creator($e->requestor->id);
+    $copy->editor($e->requestor->id);
+
+    $copy->call_number(OILS_PRECAT_CALL_NUMBER); 
+    $copy->loan_duration(OILS_PRECAT_COPY_LOAN_DURATION);
+    $copy->fine_level(OILS_PRECAT_COPY_FINE_LEVEL);
+
+    # if the caller provided any additional metadata on the
+    # item we're creating, capture it in the dummy fields
+    $copy->dummy_title($args->{title} || "");
+    $copy->dummy_author($args->{author} || "");
+    $copy->dummy_isbn($args->{isbn} || "");
+
+    unless ($e->create_asset_copy($copy)) {
+        $logger->error("error creating FF precat copy");
+        $e->rollback;
+        return;
+    }
+
+    # fetch from DB to ensure updated values (dates, etc.)
+    $copy = $e->retrieve_asset_copy($copy->id);
+    $e->commit;
+    return recursive_hash($copy);
+}
+
+
+
+1;
+
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Horizon.pm b/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Horizon.pm
new file mode 100644 (file)
index 0000000..85f167f
--- /dev/null
@@ -0,0 +1,318 @@
+package FulfILLment::LAIConnector::Horizon;
+use base FulfILLment::LAIConnector;
+use strict; use warnings;
+use OpenSRF::Utils::Logger qw/$logger/;
+use OpenILS::Utils::Fieldmapper;
+use FulfILLment::Util::Z3950;
+use FulfILLment::Util::SIP2Client;
+use XML::LibXML;
+use Data::Dumper;
+use Encode;
+use Unicode::Normalize;
+use DateTime;
+use MARC::Record;
+use MARC::File::XML ( BinaryEncoding => 'utf8' );
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+use OpenILS::Application::AppUtils;
+my $U = 'OpenILS::Application::AppUtils';
+
+sub get_item {
+    my ($self, $item_ident) = @_;
+
+    my $sip_item = $self->sip_client->item_information_request($item_ident);
+    return unless $sip_item;
+
+    my $item = $sip_item;
+    $item->{barcode} = $sip_item->{item_identifier};
+    $item->{status} = $sip_item->{circulation_status};
+    $item->{location_code} = $sip_item->{permanent_location};
+
+    return $item;
+}
+
+sub get_item_batch {
+    my ($self, $item_barcodes) = @_;
+    return [map {$self->get_item($_)} @$item_barcodes];
+}
+
+sub get_record_by_id {
+    my ($self, $record_id) = @_;
+
+    my $bib = {}; 
+    my $attr =  $self->{args}{extra}{'z3950.search_attr'};
+    my $xml = $self->z39_client->get_record_by_id($record_id, $attr);
+
+    # TODO: clean this up
+    if ($xml =~ /something went wrong/) {
+          $bib->{'error'} = 1;
+          $bib->{'error_message'} = $xml;
+    } else {
+         $bib->{'marc'} = $xml;
+         $bib->{'id'} = $record_id;
+    }   
+
+    return $bib;
+}
+
+
+
+=comment Format for holdings via Z39.50
+
+<holdings>
+ <holding>
+  <localLocation>LIB NAME</localLocation>
+  <shelvingLocation>Juvenile Fiction</shelvingLocation>
+  <callNumber>J ROWLING</callNumber>
+  <circulations>
+   <circulation>
+    <availableNow value="1"/>
+    <restrictions>LIB NAME</restrictions>
+    <itemId>1234567890</itemId>
+    <renewable value="1"/>
+    <onHold value="0"/>
+    <temporaryLocation>Checked In</temporaryLocation>
+   </circulation>
+  </circulations>
+ </holding>
+ ...
+=cut
+
+sub get_items_by_record {
+    my ($self, $record_id) = @_;
+
+    my $attr =  $self->{args}{extra}{'z3950.search_attr'};
+    my $xml = $self->z39_client->get_record_by_id($record_id, $attr, undef, 'opac', 1);
+
+    # entityize()
+    $xml = decode_utf8($xml);
+    NFC($xml);
+    $xml =~ s/&(?!\S+;)/&amp;/gso;
+    $xml =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
+
+    # strip control chars, etc.
+    # silence 'uninitialized value in substitution iterator'
+    no warnings;
+    $xml =~ s/([\x{0000}-\x{001F}])//sgoe; 
+    use warnings;
+
+    my $doc = XML::LibXML->new->parse_string($xml);
+
+    my %map = (
+        circ_lib => 'localLocation',
+        location => 'shelvingLocation',
+        call_number => 'callNumber',
+        barcode => 'itemId',
+        status => 'temporaryLocation'
+    );
+
+    my @copies;
+    for my $node ($doc->findnodes('//holding')) {
+        my $copy = {};
+
+        for my $key (keys %map) {
+            my $fnode = $node->getElementsByTagName($map{$key})->[0];
+            $copy->{$key} = $fnode->textContent if $fnode;
+        }
+
+       push(@copies, $copy);
+    }
+
+    return \@copies;
+}
+
+sub get_user {
+    my ($self, $user_barcode, $user_pass) = @_;
+
+    # fetch the user using the default implementation
+    my $user = $self->SUPER::get_user($user_barcode, $user_pass);
+    return unless $user;
+
+    # munge the names...
+    # personal_name is delivered => SURNAME, GIVEN NAME
+    my @names = split(',', $user->{personal_name} || $user->{full_name});
+    $user->{full_name} = $user->{personal_name};
+    $user->{given_name} = $names[1];
+    $user->{surname} = $names[0];
+
+    return $user;
+}
+
+# copy hold
+sub place_borrower_hold {
+    my ($self, $item_barcode, $user_barcode, $pickup_lib) = @_;
+    return $self->place_hold_via_sip(
+        undef, $item_barcode, $user_barcode, $pickup_lib);
+}
+
+# bib hold
+sub place_lender_hold {
+    my ($self, $item_barcode, $user_barcode, $pickup_lib) = @_;
+
+    my $copy = $self->flesh_copy($item_barcode) or return;
+
+    return $self->place_hold_via_sip(
+        $copy->call_number->record->id,
+        undef, # item_barcode
+        $user_barcode, 
+        $pickup_lib
+    );
+}
+
+sub delete_borrower_hold {
+    my ($self, $item_barcode, $user_barcode) = @_;
+
+    my $resp = $self->sip_client->delete_hold(
+        $user_barcode, 
+        undef, undef, undef, undef, 
+        $item_barcode
+    );
+
+    return $resp ? $self->translate_sip_hold($resp) : undef;
+}
+
+sub delete_lender_hold {
+    my ($self, $item_barcode, $user_barcode) = @_;
+    my $copy = $self->flesh_copy($item_barcode) or return;
+    
+    my $resp = $self->sip_client->delete_hold(
+        $user_barcode, 
+        undef, undef, undef, undef, undef,
+        $copy->call_number->record->id
+    );
+
+    return $resp ? $self->translate_sip_hold($resp) : undef;
+}
+
+sub delete_item_hold {
+    my ($self, $item_ident, $user_barcode) = @_;
+
+    my $resp = $self->sip_client->delete_hold(
+        $user_barcode, undef, undef, undef, undef, $item_ident);
+
+    return unless $resp;
+    return $self->translate_sip_hold($resp);
+}
+
+sub delete_record_hold {
+    my ($self, $record_id, $user_barcode) = @_;
+
+    my $resp = $self->sip_client->delete_hold(
+        $user_barcode, undef, undef, undef, undef, undef, $record_id);
+
+    return [] unless $resp;
+
+    my $blob = $self->translate_sip_hold($resp);
+    $blob->{bibID} = $record_id;
+    return $blob;
+}
+
+
+# ---------------------------------------------------------
+# BELOW NEEDS RE-TESTING
+# ---------------------------------------------------------
+
+sub get_user_holds {
+    my $self = shift;
+    my $user_barcode = shift;
+    my $user_pass = shift;
+    my @holds;
+
+    # TODO: requiring user_pass may be problematic...  
+
+    # Horizon provides available holds and unavailable holds in
+    # separate lists, which requires multiple patron info requests.
+    # FF does not differentiate, though, so collect them all 
+    # into one patron holds list.
+
+    # available holds
+    my $user = $self->sip_client->lookup_user({
+        patron_id => $user_barcode,
+        patron_pass => $user_pass
+    });
+
+    # TODO: fix the Available/Pending statuses?
+
+    if ($user) {
+        $logger->debug("User hold items = @{$user->{hold_items}}");
+        push(@holds, translate_patron_info_hold($_, 'Available')) 
+            for @{$user->{hold_items}};
+    }
+
+    # unavailable holds
+    $user = $self->sip_client->lookup_user({
+        patron_id => $user_barcode,
+        patron_pass => $user_pass,
+        enable_summary_pos => 5
+    });
+
+    if ($user) {
+        $logger->debug("User pending hold items = @{$user->{hold_items}}");
+        push(@holds, translate_patron_info_hold($_, 'Pending')) 
+            for @{$user->{hold_items}};
+    }
+
+    return \@holds;
+}
+
+sub translate_patron_info_hold {
+    my ($txt, $status) = @_;
+
+    # Horizon SIP2 patron info hold format
+    # |CDSOFFTESTB12 CENT 06/05/13 $0.00 b SO FF Test Book 1|
+    # |CDSOFFTESTB22 CENT 06/05/13 $0.00 b SO FF Test Book 2|
+
+    my ($barcode, undef, $xact_start, undef, undef, $title) = split(' ', $txt);
+
+    return {
+        placed_on => $xact_start,
+        status => $status,
+        title => $title,
+        barcode => $barcode,
+        itemid => $barcode
+    };
+}
+
+sub create_borrower_copy {
+    my ($self, $ref_copy, $ou_code) = @_;
+
+    return unless $self->ncip_client;
+
+    my $simple_rec = $ref_copy->call_number->record->simple_record;
+
+    my ($doc, @errs) = $self->ncip_client->request(
+        'CreateItem',
+        item => {
+            barcode => $ref_copy->barcode,
+            call_number => $ref_copy->call_number->label,
+            title => $simple_rec->title,
+            author => $simple_rec->author,
+            owning_lib => $ou_code
+        }
+    );
+
+
+    @errs = ('See Error Log') unless @errs or $doc;
+
+    if (@errs) {
+        $logger->error(
+            "FF unable to create borrower copy ".
+                $ref_copy->barcode." : @errs");
+        return;
+    }
+
+    my $barcode = $doc->findnodes(
+        '//CreateItemResponse/UniqueItemId/ItemIdentifierValue'
+    )->string_value;
+
+    if (!$barcode) {
+        $logger->error("FF unable to create borrower copy : ".$doc->toString);
+        return;
+    }
+
+    $logger->info("FF created borrower copy $barcode");
+
+    return {barcode => $barcode};
+}
+
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/III.pm b/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/III.pm
new file mode 100644 (file)
index 0000000..a26b0e0
--- /dev/null
@@ -0,0 +1,144 @@
+package FulfILLment::LAIConnector::III;
+use base FulfILLment::LAIConnector;
+use strict; use warnings;
+use Net::Telnet;
+use Net::OpenSSH;
+use OpenSRF::Utils::Logger qw/$logger/;
+
+# connect to SSH / Millenium terminal app
+sub ssh_connect {
+    my $self = shift;
+
+    my $host = $self->{'ssh.host'} || $self->{'host.item'} || $self->{host};
+    my $user = $self->{'ssh.user'} || $self->{'user.item'} || $self->{user};
+    my $pass = $self->{'ssh.passwd'} || $self->{'passwd.item'} || $self->{passwd};
+
+    # creating a pty spawns a child process make sure 
+    # we're not picking up any existing sigchld handlers
+    $SIG{CHLD} = 'DEFAULT';
+
+    $logger->info("FF III SSH connecting to $user\@$host");
+
+    my $ssh = Net::OpenSSH->new(
+        $host, user => $user, password => $pass);
+
+    if ($ssh->error) {
+        $logger->error("FF III SSH connect error " . $ssh->error);
+        return;
+    }
+
+    my ($fh, $pid) = $ssh->open2pty();
+    my $term = Net::Telnet->new(Fhopen => $fh);
+
+    # keep these around for later cleanup and to ensure $ssh stays in scope
+    $self->{ssh_parent} = $ssh;
+    $self->{ssh_pty} = $fh;
+    $self->{ssh_pid} = $pid;
+    $self->{ssh_term} = $term;
+
+    return 1 if $term->waitfor(-match => '/SEARCH/', -errmode => "return");
+
+    $logger->error("FF III never received SSH menu prompt");
+    $self->ssh_disconnect;
+    return;
+}
+
+sub ssh_disconnect {
+    my $self = shift;
+    return unless $self->{ssh_parent};
+
+    $logger->debug("FF III SSH disconnecting");
+
+    $self->{ssh_pty}->close if $self->{ssh_pty};
+    $self->{ssh_term}->close if $self->{ssh_term};
+    $self->{ssh_term} = undef;
+    $self->{ssh_parent} = undef;
+
+    # required to avoid <defunct> SSH processes
+    if (my $pid = $self->{ssh_pid}) { # assignment
+        $logger->debug("FF III SSH waiting on child $pid");
+        waitpid($pid, 0);
+    }
+}
+
+# send command to SSH term and wait for a response
+sub send_wait {
+    my ($self, $send, $wait, $timeout) = @_;
+
+    if ($send) {
+        $logger->debug("FF III sending SSH command '$send'");
+        $self->{ssh_term}->print($send);
+    }
+
+    my @response;
+
+    if ($wait) {
+        $logger->debug("FF III SSH waiting for '$wait'...");
+
+        @response = $self->{ssh_term}->waitfor(
+            -match => "/$wait/", 
+            -errmode => 'return',
+            -timeout => $timeout || 10
+        );
+
+        if (@response) {
+            my $txt = join('', @response);
+            $txt =~ s/[[:cntrl:]]//mg;
+            $logger->debug("FF III SSH wait received text: $txt");
+            warn "==\n$send ==> \n$txt\n";
+
+        } else {
+            $logger->warn(
+                "FF III SSH timed out waiting for '$wait' :".
+                $self->{ssh_term}->errmsg);
+        }
+    }
+
+    return @response;
+}
+
+
+sub get_user {
+    my ($self, $user_barcode, $user_pass) = @_;
+
+    return $self->SUPER::get_user($user_barcode, $user_pass)
+        if $self->sip_client;
+
+    # no SIP, use SSH instead..
+    $self->ssh_connect or return;
+
+    my $user;
+    eval { $user = $self->get_user_guts($user_barcode, $user_pass) };
+    $logger->error("FF III error getting user $user_barcode : $@");
+
+    $self->ssh_disconnect;
+    return $user;
+} 
+
+sub get_items_by_record {
+    my ($self, $record_id) = @_;
+    $self->ssh_connect or return;
+
+    my @items;
+    eval { @items = $self->get_items_by_record_guts($record_id) };
+    $logger->error("FF III get_items_by_record() died : $@") if $@;
+
+    $self->ssh_disconnect;
+    return @items ? \@items : [];
+}
+
+# TODO: test with 2011
+sub get_record_by_id {
+    my ($self, $rec_id) = @_;
+
+    if ($self->z39_client) {
+        $logger->info("FF III fetching record from Z39: $rec_id");
+        chop($rec_id); # z39 does not want the final checksum char.
+        return { marc => $self->z39_client->get_record_by_id($rec_id) };
+    } else {
+        $logger->info("FF III fetching record from SSH: $rec_id");
+        return $self->get_record_by_id_ssh($rec_id);
+    }
+}
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/III/2009B_1_2.pm b/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/III/2009B_1_2.pm
new file mode 100644 (file)
index 0000000..671d596
--- /dev/null
@@ -0,0 +1,1154 @@
+package FulfILLment::LAIConnector::III::2009B_1_2;
+use base FulfILLment::LAIConnector::III;
+use strict; use warnings;
+use OpenSRF::Utils::Logger qw/$logger/;
+
+use MARC::Record;
+use MARC::Batch;
+use MARC::File::XML (BinaryEncoding => 'utf8');
+use LWP::UserAgent;
+use HTTP::Request;
+use WWW::Mechanize;
+use XML::Simple;
+use Sip;
+use FulfILLment::Util::Z3950;
+use FulfILLment::Util::SIP2Client;
+use Data::Dumper;
+
+my $ua = LWP::UserAgent->new;
+$ua->agent("FulfILLment/1.0");
+
+sub get_items_by_record_guts {
+    my ($self, $record_id) = @_;
+    my @items;
+
+    $self->send_wait('S', 'prominently') or return;
+    $self->send_wait('B', 'RECORD NO') or return;
+    $self->send_wait('R', 'Type Record') or return;
+    $self->send_wait($record_id, 'Record SUMMARY') or return;
+
+    my ($prematch, $match) = $self->send_wait('S', 
+        'To see a particular|Copy Type:\w+') or return;
+
+    if ($match =~ /Copy Type/) {
+        # single-copy bib jumps right to copy details.
+
+        # we need the Copy Type from the match as well, 
+        # so push it back into the main text
+        $prematch .= $match;
+
+        # TODO: see why parse_item_screen() works fine for
+        # single copies in 2011_1_3 but not here.  there's
+        # an opportunity for more consolidation w/i the superclass
+        my @barcodes = ($prematch =~ /BARCODE\s+(\d+)/g);
+        my @location_code = ($prematch =~ /LOCATION:\s(\w+)\s+/);
+        $prematch =~ s/\e\[\d+(?>(;\d+)*)[mh]//gi;
+        $prematch =~ s/\e\[k//gi;
+        $prematch =~ s/[[:cntrl:]]//g;
+        my @fstring = ($prematch =~ /TITLE(.*)IMPRINT/);
+        my @fingerprint = split(/IMPRINT|\s{2,}/,$fstring[0]);
+        my $item_fields = $self->parse_item_screen($prematch, $barcodes[0]);
+
+        unless ($item_fields and $barcodes[0]) {
+            $logger->warn(
+                "FF III unable to parse single-item screen for $record_id");
+            return;
+        }
+
+        my $item = {
+            fingerprint => $fingerprint[1],
+            call_number => $item_fields->{call_number},
+            due_date => $item_fields->{due_date},
+            barcode => $barcodes[0],
+            holdable => "t",
+            location_code => $location_code[0],
+            error => 0,
+            error_message => '',
+            item_id => $item_fields->{item_id},
+        };
+
+        push(@items, $item);
+
+    } else {
+        # multi-copy bib
+
+        my @item_indexes = ($prematch =~ /ITEM\s+(\d+)\s>/g);
+
+        for my $index (@item_indexes) {
+            my @response = $self->send_wait($index, 'Record SUMMARY') or last;
+            my $screen = $response[0];
+
+            my @barcodes = ($screen =~ /BARCODE\s+(\d+)/g);
+
+            $logger->debug("FF III item screen contains ".
+                length($screen)." characters");
+
+            my $item;
+            $logger->debug("FF III parsing item screen for entry $index");
+            $item = $self->parse_item_screen($screen, $barcodes[0]);
+
+            unless ($item and $item->{barcode}) {
+                $logger->warn(
+                    "FF III unable to parse item screen for $record_id");
+                last;
+            }
+
+            push(@items, $item);
+
+            # return to the summary screen
+            $self->send_wait('S', 'To see a particular') or last;
+        }
+    }
+
+    return @items;
+}
+
+sub get_user_guts {
+    my ($self, $user_barcode, $user_pass) = @_;
+
+    $self->send_wait('S', 'prominently') or return;
+    $self->send_wait('P', 'RECORDS') or return;
+    $self->send_wait('B', 'BARCODE') or return;
+
+    my ($txt, $match) = $self->send_wait(
+        $user_barcode, 
+        'Record|BARCODE not found'
+    ) or return;
+
+    if ($match =~ /BARCODE not found/) {
+        $logger->info("FF III user '$user_barcode' not found");
+        return;
+    }
+
+    $txt =~ s/\[\d+;\d+(;\d+)?[Hm]//g;
+    $txt =~ s/^\d\d\d\s+.+//g;
+    $txt =~ s/\x1b//g;
+    $txt =~ s/\[0m//g;
+    $txt =~ s/\[0xF\]//g;
+    $txt =~ s/[[:cntrl:]]//g;
+
+    my $user = {
+        exp_date => qr/EXP DATE:\s(\d+\-\d+\-\d+)/,
+        user_id => qr/PIN\s+([A-Za-z0-9]+)INPUT/,
+        notice_pref => qr/NOTICE PREF:\s(.*)TOT/,
+        lang_pref => qr/LANG PREF:\s+(\w+)/,
+        mblock => qr/MBLOCK:\s+([A-Za-z0-9\-])\s+/,
+        patron_agency => qr/PAT AGENCY:\s+(\d+)\s+/,
+        overdue_items_count => qr/HLODUES:\s+(\d+)/,
+        notes => qr/PMESSAGE:\s+([A-Za-z0-9\-])/,
+        home_ou => qr/HOME LIBR:\s+(\w+)/,
+        total_renewals  => qr/TOT RENWAL:\s+(\d+)/,
+        email_address => qr/EMAIL ADDR\s+([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,3})/,
+        claims_returned_count => qr/CL RTRND:\s(\d+)/,
+        loaned_items_count => qr/CUR CHKOUT:\s(\d+)/,
+        money_owed => qr/MONEY OWED:\s(\$\d+\.\d+)/,
+        overdue_penalty => qr/OD PENALTY:\s(\d+)/,
+        institution_code => qr/HOME LIBR:\s+(\w+)/,
+        block_until => qr/BLK UNTIL:\s(\d+\-\d+\-\d+)/,
+        full_name => qr/PATRN NAME\s+(.*)ADDRESS/,
+        street_address => qr/ADDRESS\s+(.*)SSN/,
+        ssn => qr/SSN #\s+(\d+)P BARCODE/,
+        photocopy_count => qr/PIUSE:\s+(\d+)/,
+        patron_code1 => qr/PCODE1:\s+(\d+)/,
+        patron_code2 => qr/PCODE2:\s+(\d+)/,
+        patron_code3 => qr/PCODE3:\s+(\d+)/,
+        patron_code4 => qr/PCODE4:\s+(\d+)/,
+        notes => qr/PMESSAGE:\s+([A-Za-z0-9\-])/,
+        ill_request => qr/ILL REQUES:\s+(\d+)/,
+        last_circ_date => qr/CIRCACTIV:\s(\d+\-\d+\-\d+)/,
+        patron_type => qr/P TYPE:\s(\d+)/,
+        census => qr/CENSUS:\s(\d+)\s+/,
+        total_checkouts => qr/TOT CHKOUT:\s(\d+)/,
+        checkouts => qr/CUR CHKOUT:\s+(\d+)/,
+        phone => qr/TELEPHONE\s+(\d\d\d\-\d\d\d\-\d\d\d\d)/,
+        input_by => qr/INPUT BY\s+([A-Za-z\/]+)/,
+        birth_date => qr/BIRTH DAT:(\d\d\-\d\d-\d\d)/,
+        cur_item_A => qr/CUR ITEMA:\s(\d+)/,
+        cur_item_B => qr/CUR ITEMB:\s(\d+)/,
+        cur_item_C => qr/CUR ITEMC:\s(\d+)/,
+        cur_item_D => qr/CUR ITEMD:\s(\d+)/,
+        error => 0,
+    };
+
+    for my $key (keys %$user) {
+        my ($val) = ($txt =~ $user->{$key});
+        $user->{$key} = $val;
+    }
+
+    my %prefs = (
+        z => 'email',
+        t => 'phone',
+        p => 'secondary phone'
+    );
+
+    $user->{notice_pref} = $prefs{$user->{notice_pref}};
+    $user->{barcode} = $user_barcode;
+    
+    return $user;
+}
+
+
+sub get_item {
+    my ($self, $item_barcode) = @_;
+
+    $self->ssh_connect or return;
+
+    $self->send_wait('S', 'prominently') or return;
+    $self->send_wait('I', 'SEARCHING RECORDS') or return;
+    $self->send_wait('B', 'BARCODE') or return; # NOTE: 2011 uses 'D'
+    my ($screen) = $self->send_wait($item_barcode, 'NEW Search') or return;
+
+    $self->ssh_disconnect;
+
+    if ($screen =~ /not found|PATRON Information/g) {
+        $logger->info("FF III unable to locate item $item_barcode");
+        return;
+    }
+
+    my $item = $self->parse_item_screen($screen, $item_barcode);
+
+    $logger->error("FF III error parsing item screen for $item_barcode")
+        unless $item;
+
+    return $item;
+}
+
+# ---------------------------------------------------------------------------
+# Everything below here needs modification and testing
+# ---------------------------------------------------------------------------
+
+sub get_item_fields{
+    my $self=$_[0];
+    my $itemData=$_[1];
+    #print Dumper $itemData;
+    $itemData =~ s/\e\[\d+(?>(;\d+)*)[mh]//gi;
+    $itemData =~ s/\e\[k//gi;
+    $itemData =~ s/[[:cntrl:]]//g;
+    $itemData =~ s/VOLUME/VOLUME:/g;
+    $itemData =~ s/CALL #/CALL #:/g;
+    $itemData =~ s/R > Browse Nearby EntriesI > Show similar ITEMSN >//g;
+    $itemData =~ s/R > RETURN to BrowsingZ > Show Items Nearby on ShelfF > FORWARD browseI > Show similar ITEMSN >//g;
+    $itemData =~ s/U > Show BIBLIOGRAPHIC RecordZ > Show Items Nearby on Shelf//g;
+    $itemData =~ s/\+ > ADDITIONAL options1-2,N,A,Z,I,U,T,E,\+\).*ITEM Information//g;
+    $itemData =~ s/I > Show similar ITEMSA > ANOTHER Search by RECORD #//g;
+    $itemData =~ s/U > Show similar BIBLIOGRAPHIC RecordS > Record SUMMARY//g;
+    $itemData =~ s/U > Show BIBLIOGRAPHIC RecordS > Record SUMMARY//g;
+    $itemData =~ s/T > Display MARC RecordE > Mark item for//g;
+    $itemData =~ s/U > Show BIBLIOGRAPHIC Record//g;
+    $itemData =~ s/NEW Search//g;
+    $itemData =~ s/N,A,S,Z,I,U,T,E\)//g;
+    $itemData =~ s/N >//g;
+   
+    if(my @l = ($itemData =~ m/(\d+)BARCODE/g)){
+        $itemData =~ s/VOLUME:/VOLUME: $l[0]/g;
+    }
+   
+    
+    $itemData =~ s/(\d+)BARCODE/BARCODE:/g;
+    #$itemData =~ s/1BARCODE/BARCODE/g;
+    my @fields = grep {defined and not /^\s*$/} split /(\s{2,})|(-  -)/, $itemData;
+    my $i=0;
+    my @newfields = [];
+   
+   foreach (@fields){
+     #$_.=':' if $_ eq 'VOLUME';
+     $_.=':' if $_ eq 'BARCODE';
+     
+     if((/^[ \-]+$/ or $newfields[$#newfields] eq 'BARCODE:') and @newfields){
+         $newfields[$#newfields] .=$_;
+     }else{
+         push @newfields, $_;
+     }
+       
+     $i++;
+    }
+  
+   return \@newfields;
+}
+
+
+
+
+
+
+
+sub get_item_call_and_bib_number{
+    my $self = $_[0];
+    my $item_data = $_[1];
+    #print Dumper $item_data;
+    $item_data =~ s/\e\[\d+(?>(;\d+)*)[mh]//gi;
+    $item_data =~ s/\e\[k//gi;
+    $item_data =~ s/[[:cntrl:]]//g;
+    
+    #get Call Number
+    my @c = ($item_data =~ /BIBLIOGRAPHIC Information\s+CALL #\s+(.*?)AUTHOR/);
+    
+    if(not $c[0]){
+        @c = ($item_data =~ /BIBLIOGRAPHIC Information\s+CALL #\s(.*?)TITLE/);
+    }
+
+    #get Barcode
+    my @b = ($item_data =~ /BARCODE:.*(B.*)\s+BIBLIOGRAPHIC Information/);
+    
+    if($b[0]){
+        $b[0] =~ s/\s//g;
+    }
+  
+    #get title
+    my @f = split("TITLE",$item_data) ;
+    my @fingerprint = ($f[0] =~ /\s+(.*)\s{5,}/); 
+   
+    my @out = ($b[0],$c[0],$fingerprint[0]);
+    #print Dumper @out; 
+    return \@out;
+
+}
+
+
+
+         
+
+sub parse_item_screen{
+    my $self = $_[0];
+    my $screen = $_[1];
+    my $barcode = $_[2];
+    my $jhash={};
+    my @nvp;
+    my $label;
+    my $value;
+    my @screen_split;
+    my $rs = $screen;
+    my @record_number = ($rs =~ /(I\d.*)\s+ITEM Information/g);
+    my $e = "ESC[7;2H";
+    $e = ord($e);
+    
+    $record_number[0] =~ s/\s+//g;
+    push @screen_split, split(/ITEM Information/,$screen);
+    #print Dumper @screen_split;
+    my $fields = $self->get_item_fields($screen_split[1]); 
+    my $call_and_bib = $self->get_item_call_and_bib_number($screen_split[0]);
+
+    unless ($call_and_bib->[1]) {
+        # in multi-copy records, the call number is embedded in the 
+        # copy data and has no predictable terminating string.
+        # capture it all and chop it off at the first occurence 
+        # of 2 consecutive spaces
+        my $scr = $screen_split[1];
+        $scr =~ s/[[:cntrl:]]//mg;
+        my @c = ($scr =~ /CALL #\s+(.*)/);
+        $c[0] =~ s/(.*?)\s{2}.*/$1/mg if $c[0];
+        $call_and_bib->[1] = $c[0] || 'UNKNOWN';
+    }
+
+    # remove pesky trailing junk
+    $call_and_bib->[1] =~ s/^\s+//g;
+    $call_and_bib->[1] =~ s/\s+$//g;
+
+    $logger->warn("FF III unble to find callnumber")
+        unless $call_and_bib->[1];
+
+    #print Dumper $fields;
+    
+    foreach(@$fields){
+        @nvp=split(":",$_);
+        $label=$nvp[0];
+        $value=$nvp[1];
+        if($label eq "DUE DATE"){
+            $label="due_date";
+        }elsif($label eq "BARCODE"){
+            $label="barcode";
+        }elsif($label eq "STATUS"){
+            $label="holdable";
+        }elsif($label eq "LOCATION"){
+            $label="location";
+        }
+        
+        if($label eq "holdable" and $value eq " -"){
+            $value="t";
+        }elsif($label eq "holdable" and $value eq "e"){
+            $value="t";
+        }elsif($label eq "holdable" ){
+            $value="f";
+        }
+        
+        $jhash->{$label}=$value;
+    }
+
+        #print Dumper $call_and_bib;
+        if($jhash->{due_date}){
+            if($jhash->{'due_date'} eq "-  -"){
+                $jhash->{'due_date'} = '';
+            }
+        }
+        $jhash->{'call_number'} = $call_and_bib->[1];
+        $jhash->{'bib_id'}=$call_and_bib->[0];
+        $jhash->{'item_id'} = $record_number[0]; 
+        $jhash->{'barcode'} = $barcode;
+        $jhash->{'error_message'} = '';  
+        $jhash->{'fingerprint'} = '';
+        return $jhash; 
+}
+
+    
+
+sub get_item_by_call_number{
+    my $self = $_[0];
+    my $ssh = $self->initialize;
+    my $item_id = $_[1];
+    my $call_number_type = $_[2];
+    my @out;
+    my @entries; #If there are multiple entries for an item, the entries are stored here.
+    #print "preparing to search the catalog\n";
+    $ssh->print("S");
+    $ssh->waitfor(-match => '/prominently\?/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+    #print "ok\n";
+    #print "selecting option to search for items\n";
+    $ssh->print("I");
+    $ssh->waitfor(-match => '/SEARCHING RECORDS/',
+                  -errmode => "return")or die "search failed;", $ssh->lastline;
+    #print "ok\n";
+    #select attribute to search by, i.e title, barcode etc...
+    $ssh->print("C");
+    $ssh->waitfor(-match => '/CALL NUMBER SEARCHES/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+    
+    if(lc($call_number_type) eq "dewey"){
+        
+        $ssh->print("D");
+        $ssh->waitfor(-match => '/DEWEY CALL NO :/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+    
+    }elsif(lc($call_number_type) eq "lc"){
+        $ssh->print("C");
+        $ssh->waitfor(-match => '/LC CALL NO :/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+    
+    }elsif(lc($call_number_type) eq "local"){
+        $ssh->print("L");
+        $ssh->waitfor(-match => '/LOCAL CALL NO :/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+    }
+    
+    $ssh->print($item_id); 
+    push @out,$ssh->waitfor(-match => '/NEW Search/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+    #print "Done.\n";
+    
+    if(my @num_entries =  ($out[0] =~ /(\d+)\sentries found/)){
+        #my $i = 0;
+        #$i++;
+        $ssh->print(1);
+        push @out,$ssh->waitfor(-match => '/NEW Search/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+        #print "Done.\n";
+    }
+
+    $self->ssh_disconnect;
+    my @items;
+    push @items,$self->parse_item_screen($out[2]);
+    return \@items;
+}
+
+
+#Under construction
+
+sub get_item_by_bib_number_z3950{
+    my $self = $_[0];
+    my $bibID = $_[1];    
+    my $marc = $self->get_bib_records_by_record_number_z3950($bibID);
+    my $batch = MARC::Batch->new('XML', $marc ); 
+    while (my $m = $batch->next ){
+        print $m->subfield(650,"a"),"\n";
+
+    }
+    #my $record = $batch->next();
+    #print Dumper $record; 
+
+}
+
+
+
+
+
+# Method: get_bib_records_by_record_num
+# Params:
+#   idList => list of record numbers
+
+#BIBLIOGRAPHIC RECORDS
+#Notes: Section contains methods to retrieve and parse bibliographic records
+#==================================================================================
+
+
+sub get_bib_records_by_record_number{
+    my $self = $_[0];
+    my $ssh = $self->initialize;
+    my $id = $_[1];
+    my $json=JSON::XS->new();
+    my $count=0;
+    my @out;
+    my @screen;
+    my @marc;
+    my @bib = ();
+    my $jhash={};
+    
+    eval{ 
+    if(ref($ssh) eq "ARRAY"){
+        return $ssh;
+    }
+
+    #select SEARCH the catalog
+    #print "getBibRecords\n";
+    #print "preparing to search the catalog\n";
+    $ssh->print("S");
+    
+    $ssh->waitfor(-match => '/prominently\?/',
+                  -errmode => "return") or die "Search failed. Could not retrieve ", $id;
+
+    #print "ok\n";
+    #print "selecting option to search for bibliographic records\n";
+    #select item record option
+    $ssh->print("B");
+    $ssh->waitfor(-match => '/SEARCHING RECORDS/',
+                  -errmode => "return") or die "Search failed. Could not retrieve ", $id;
+    
+    #print "ok\n";
+    #print "selecting option to search by record number\n";
+    #select attribute to search by, i.e title, barcode etc...
+    $ssh->print("R"); #search by record number
+    #print "ok\n";
+    #print "searching for id=$id\n";
+    #search for id
+    $ssh->print($id);
+    my $first = 1;
+    #print "searching more of the document\n";
+    my @bid;
+    
+    my $get_more = sub {
+                        my @temp_screen = (); 
+                        my @lines;
+                        my $last = 0;
+                        my $get_more = shift;
+                        
+                        if($first == 1){
+                            $ssh->print("T");
+                            $ssh->waitfor(-match => '/BIBLIOGRAPHIC Information/',
+                                -errmode => "return") or die "Search failed. Could not retrieve ", $id;
+                                #-errmode => "return") or die "search failed;", $ssh->lastline;
+                            $ssh->print("M");
+                            #$first = 0;
+                        }    
+                        
+                        $ssh->print("M");
+                        $ssh->print("T");
+                        #delete the following two lines
+                        
+                        push @temp_screen ,$ssh->waitfor(-match => '/Regular Display/',
+                                -errmode => "return") or die "Search failed. Could not retrieve ", $id;
+                                #-errmode => "return") or die "search failed;", $ssh->lastline;
+                        #print Dumper $temp_screen[0];
+                        
+                        @bid = ($temp_screen[0] =~ /([Bb].*)\s+BIBLIOGRAPHIC Information/);
+                        if($temp_screen[0] =~ /COUNTRY:/g and $first == 0 ){
+                            #print Dumper $temp_screen[0];
+                            #print "Reached end of record... I think\n";
+                            return 1; 
+                        }
+                        push @lines,split(/\[\d+;\d+(;\d+)?[Hm]/,$temp_screen[0]);
+                        
+                        foreach  (@lines){   
+                              if($_){ 
+                                $_ =~ s/^(\s+)//g;
+                                if($_ =~/^\d\d\d\s+.+/g){
+                                   $_ =~ s/\x1b//g;
+                                   $_ =~ s/\[0m//g;
+                                   $_ =~ s/\[0xF\]//g;
+                                   $_ =~ s/[[:cntrl:]]//g;
+                                   push @marc,$_;
+                                   #print Dumper $_;
+                                }
+                             }
+                        }
+                        #print Dumper @lines;
+
+                        $first = 0;
+                        #print Dumper @marc; 
+                        $ssh->print("M");
+                        $ssh->print("T");
+                        @temp_screen = (); 
+                        $get_more->($get_more);
+                   };
+
+
+    $get_more->($get_more);
+
+    my @id = ($bid[0]) ? ($bid[0] =~ /([Bb][0-9]+)\s+/) : ""  ;
+    #print Dumper @marc; 
+     
+    #print "bid = ".$id[0]."\n";
+    #print "ok\n"; 
+    #push @out, $self->parse_bib_records( $self->grab_bib_screen($ssh,$id), $id );
+    #print "logging out\n";
+    $ssh->print("N");
+    $ssh->print("Q");
+    $ssh->print("Q");
+    $ssh->print("Q");
+    $ssh->print("X");
+    $ssh->waitfor(-match => '/closed/',
+                  #-errmode => "return")or die "log out failed;", $ssh->lastline;
+                  -errmode => "return") or die "Search failed. Could not retrieve ", $id;
+    
+    #print "logged out\n";
+    my $rec =  breaker2marc(\@marc);
+    $rec->insert_fields_ordered(
+        MARC::Field->new(
+            '907',
+            ' ',
+            ' ',
+            'a' => $id[0]
+        )
+    ) if (!$rec->subfield( '907' => 'a' ));
+
+    my $x =  $rec->as_xml_record;
+    $x =~ s/^<\?.+?\?>.//sm;
+    #my $jhash={};
+    $jhash->{'marc'}=$x;
+    #$bid =~ s/\[/$bid/g;
+    $jhash->{'id'}=$id[0];
+    $jhash->{'format'}="marcxml";
+    $jhash->{error} = 0;
+    $jhash->{error_message} = '';
+    #print "xml = $x\n";
+    #my @bib = ();
+    push @bib,$jhash;
+    #warn Dumper \@bib; 
+    return \@bib;
+
+    1;
+    }or do {
+        $jhash->{error} = 1;
+        $jhash->{error_message} = $@;
+        push @bib,$jhash;
+        return \@bib;
+    }
+
+}
+
+
+
+
+sub get_range_of_records{
+    my $self = $_[0];
+    my $list = $_[1];
+    my $dir = `pwd`;
+    my $file = "/openils/lib/perl5/FulfILLment/WWW/LAIConnector/conf/III_2009B_1_2/marc/marc_dump.mrc";
+    open FILE, ">$file" or die &!;
+   
+    foreach my $r (@$list){
+        my $record = $self->get_bib_records_by_record_number($r);
+
+        print FILE $record->[0]->{marc};   
+
+    }
+    
+    close FILE;
+    my @out;
+    my $jhash;
+    push @out,$jhash;
+    return \@out;
+
+}
+
+
+
+
+
+
+
+sub breaker2marc {
+    my $lines = shift;
+    my $delim = quotemeta(shift() || '|');
+    my $rec = new MARC::Record;
+    for my $line (@$lines) {
+
+        chomp($line);
+
+        if ($line =~ /^=?(\d{3})\s{2}(.)(.)\s(.+)$/) {
+
+            my ($tag, $i1, $i2, $rest) = ($1, $2, $3, $4);
+            if ($tag < 10) {
+                $rec->insert_fields_ordered( MARC::Field->new( $tag => $rest ) );
+
+            } else {
+
+                my @subfield_data = split $delim, $rest;
+                if ($subfield_data[0]) {
+                    $subfield_data[0] = 'a' . $subfield_data[0];
+                } else {
+                    shift @subfield_data;
+                }
+
+                my @subfields;
+                for my $sfd (@subfield_data) {
+                    if ($sfd =~ /^(.)(.+)$/) {
+                        push @subfields, $1, $2;
+                    }
+                }
+
+                $rec->insert_fields_ordered(
+                    MARC::Field->new(
+                        $tag,
+                        $i1,
+                        $i2,
+                        @subfields
+                    )
+                ) if @subfields;
+            }
+        }
+    }
+
+    return $rec;
+}
+
+
+#END BIBLIOGRAPHIC RECORDS
+#=====================================================================================
+
+
+
+
+
+#Places hold on a III server through the web interface
+
+
+
+sub placeHold{
+    #use WWW::Curl::Easy;
+    my $self = shift;
+    my $host = $self->{host};
+    my $port = $self->{port};
+    my $name = $self->{login};
+    my $user_barcode = $self->{password};
+    my $bib_id = shift;
+    
+    if(length($bib_id) > 8){
+        chop($bib_id);
+    }
+
+    my $patron_sys_num = shift;
+    my $url = "http://$host/search~SO?/.$bib_id/.$bib_id/1%2C1%2C1%CB/request~$bib_id?name=$name&code=$user_barcode";
+    my @out;
+    my $response_body;
+    my $mech = WWW::Mechanize->new();
+    my $error_msg;
+    my $response;
+    my @radioInputs;
+    my $content;
+    my $hold = {}; 
+
+    eval{
+        $mech->post($url);
+        $mech->form_name('patform');
+        $mech->field('name',$name);
+        $mech->field('code',$user_barcode);
+        $response = $mech->submit(); 
+        $mech->submit();
+        $content = $mech->content;
+    };
+  
+    if($@){
+        $hold->{error} = 1;
+        $hold->{error_message} = "check username and password , error : $@";
+        push @out,$hold;
+        return \@out;
+    }
+    
+    my @title = ($content =~ /<p>Requesting <strong>(.+)<\/strong><br \/><p>/);
+    my @err_response_msg = ($content =~ /<font color="red" size="\+2">(.+)<\/font>/g);
+    my @success_response_msg = ($content =~ /Your request for .* was successful./g);
+    my @delivered_to = ($content =~ /Your request will be delivered to .* when it is available./);
+
+    if($content =~ /No Such Record/){
+        $hold->{error} = 1;
+        $hold->{error_message} = "A hold could not be placed on $bib_id, no such record\n";
+        push @out,$hold;
+        return \@out;
+    }
+    
+        
+    if($content =~ /Request denied/){
+        $err_response_msg[0] =~ s/<strong>//;
+        $err_response_msg[0] =~ s/<\/strong>//;
+        $hold->{error} = 1;
+        $hold->{error_message} = $err_response_msg[0];
+        $hold->{title} = $title[0];
+        push @out,$hold;
+        return \@out;
+    }elsif($content =~ /Your request for .* was successful./g){
+        $success_response_msg[0] =~ s/<strong>//;
+        $success_response_msg[0] =~ s/<\/strong>//;
+        $hold->{error} = 0;
+        $hold->{success_message} = $success_response_msg[0];
+        $hold->{title} = $title[0];
+        push @out,$hold;
+        return \@out;
+    }
+
+    @title = ($content =~ /class="bibInfoLabel">Title<\/td>\n<td class="bibInfoData">\n<strong>(.*)<\/strong>/g);
+    
+    if($title[0]){
+       $hold->{error} = 0;
+       $hold->{success_message} = "Your request for $title[0] was successful.";
+       $hold->{title} = $title[0]; 
+       push @out,$hold;
+       return \@out;
+    }
+}
+
+
+
+
+sub parse_hold_response{
+    my $self = $_[0];
+    my $txt = $_[1];
+    my @resp = split("END SEARCH WIDGET -->",$txt);
+    my $success = ($resp[1] =~ /denied/) ? "false" : "true"; 
+    if(!defined($resp[1])){$success = "false"}
+    my $msg = $resp[1];
+    $msg =~ s/<p>//g;
+    $msg =~ s/<\/p>//g;
+    $msg =~ s/<br \/>//g;
+    $msg =~ s/&nbsp;//g;
+    $msg =~ s/<!-End the bottom logo table-->//g;
+    $msg =~ s/<\/body>//g;
+    $msg =~ s/<\/html>//g;
+    $msg =~ s/<\/strong\>//g;
+    $msg =~ s/<strong>//g;
+    $msg =~ s/\./\. /g;
+    $msg =~ s/\n//g;
+    $msg =~ s/<font color="red"\s+size="\+2">/\. /g;
+    $msg =~ s/<\/font>//g;
+    my $data = {};
+    $data->{'success'} = $success;
+    $data->{'message'} = $msg;
+    my $out = []; 
+    $out->[0] = $data;
+    return $out; 
+}
+
+
+
+sub list_holds{
+    my $self = $_[0];
+    my $base_url = $self->{host};
+    $base_url = "https://".$base_url;
+    my $port = $self->{port};
+    my $username = $_[1];
+    my $user_barcode = $_[2];
+    my $patron_sys_num = $_[3];
+    my $action = $_[4] || "list_holds";
+    my $response_body;
+    my $request_parameters; 
+    $request_parameters = "name=$username&code=$user_barcode"; 
+    my $get_string = "/patroninfo~SO/$patron_sys_num/holds";
+    my $url = $base_url.$get_string."?".$request_parameters;
+    my $out;
+    my $mech = WWW::Mechanize->new();
+    $mech->post($url);
+  
+   
+    if($mech->success){
+        $out = $self->parse_hold_list($mech->content,$action);
+    }else{
+        $out = [{error => 1, error_message => 'There was a problem with the request' }];
+    }
+
+    if( not defined($out->[0]) ){
+        $out = [{error => 0, error_message => 'No holds were found for this user' }];
+    }
+   
+    #print Dumper $out;
+    return $out;
+
+}
+
+
+
+sub list_holds_by_bib{
+    my $self = shift;
+    my $host = shift;
+    my $port = shift;
+    my $login = shift;
+    my $user_barcode = shift;
+    my $bibID = shift;
+    my $patron_sys_num = shift;
+    my $holds =  $self->list_holds($host,$port,$login,$user_barcode,$patron_sys_num);
+    my @out;
+
+    foreach(@$holds){
+        if($_->{'bibid'} eq $bibID){
+            push @out,$_;
+        }
+    }
+    
+    return \@out;
+}
+
+
+
+sub list_holds_by_item{
+    my $self = shift;
+    my $host = shift;
+    my $port = shift;
+    my $login = shift;
+    my $user_barcode = shift;
+    my $itemID = shift;
+    my $patron_sys_num = shift;
+    my $holds =  $self->list_holds($host,$port,$login,$user_barcode,$patron_sys_num);
+    my @out;
+
+    foreach(@$holds){
+        if($_->{'itemid'} eq $itemID){
+            push @out,$_;
+        }
+    }
+    
+    return \@out;
+}
+
+
+
+
+
+
+
+sub delete_hold{
+    
+    my $self = shift;
+    my $host = $self->{host};
+    my $port = $self->{port};
+    my $userid = $self->{login};
+    $userid =~ s/\s/%20/g;
+    my $user_barcode = $self->{password};
+    my $search_id = shift;
+    my $id_type = shift;
+    my $patron_sys_num = shift;
+    my $response_body;
+    my $holds =  $self->list_holds($userid,$user_barcode,$patron_sys_num);
+    my $itemid;
+    my $linkid;
+    my $num_holds_before = @$holds;
+    my $num_holds_after;
+    
+    if($id_type eq "bib"){ 
+    
+        if(length($search_id) > 8){
+            chop($search_id);
+        }
+        
+        foreach(@$holds){
+            #print "bibid = $_->{bibid}  search_id = $search_id\n";
+            if($_->{'bibid'} eq $search_id){
+                #print "item id = ".$_->{'itemid'}."\n";
+                $itemid = $_->{'itemid'};
+                $linkid = $_->{'linkid'};
+            }
+        }
+    }elsif($id_type eq "item"){
+        foreach(@$holds){
+            if($_->{'itemid'} eq $search_id){
+                #print "item id = ".$_->{'itemid'}."\n";
+                $itemid = $_->{'itemid'};
+                $linkid = $_->{'linkid'};
+            }
+        }
+    }
+   
+    $itemid = (defined($itemid)) ? $itemid : "";
+    $linkid = (defined($linkid)) ? $linkid : "";
+    my $url = "http://$host/patroninfo~SO/$patron_sys_num/holds?name=$userid&code=$user_barcode&$linkid=on&currentsortorder=current_pickup&updateholdssome=YES&loc$itemid=";
+    my @out;
+    my $msg = {};
+    my $mech = WWW::Mechanize->new();
+    $response_body = $mech->post($url);
+
+    if($mech->success){
+       my $nha = $self->list_holds($userid,$user_barcode,$patron_sys_num);
+       $num_holds_after = @$nha;
+       #check to see whether the user was successfully authenticated 
+       my $auth = $self->loggedIn($response_body);
+       
+       if($auth ne "t"){
+           $msg->{"error"} = 1;
+           $msg->{"error_message"} = "The user $userid could not be authenticated";
+           push @out, $msg;
+           return \@out;
+       }
+       
+       if($num_holds_before == $num_holds_after){
+           $msg->{"error"} = 1;
+           $msg->{"error_message"} = "The hold for $search_id either does not exist or could not be deleted";
+           push @out, $msg;
+           return \@out;
+       }elsif($num_holds_before > $num_holds_after){
+           $msg->{"error"} = 0;
+           $msg->{"success_message"} = "The hold for $search_id has been deleted";
+           push @out, $msg; 
+           return \@out;
+       }
+       
+    }else{
+       $msg->{"error"} = 1;
+       $msg->{"error_message"} = "An error occured: code $mech->status";
+       push @out, $msg;
+       return \@out; 
+    }
+}
+
+
+
+
+sub parse_hold_list{
+    my $self = $_[0];
+    my $in = $_[1];
+    my $action = $_[2];
+    $in =~ s/\n//g;
+    my @holds; 
+    my @holdEntries = split(/(<tr class="patFuncEntry".*?<\/tr>)/ ,$in); 
+    my $user = {};
+    shift @holdEntries;
+    pop @holdEntries;
+    my @userSurname = ($in =~ /<h4>Patron Record for<\/h4><strong>(\w+),.*<\/strong><br \/>/);
+    my @userGivenName = ($in =~ /<h4>Patron Record for<\/h4><strong>(\w+),.*<\/strong><br \/>/);
+    my @expDate = ($in =~ /EXP DATE:(\d\d\-\d\d\-\d\d\d\d)<br \/>/);
+
+    
+    $user->{surname} = $userSurname[0]; 
+    $user->{given_name} = $userGivenName[0]; 
+    $user->{exp_date} = $expDate[0]; 
+    
+    if($action eq "lookup_user"){
+        push @holds,$user;
+    }elsif($action eq "list_holds"){
+
+        foreach(@holdEntries){
+            my $hold = {};
+            my @bibid = ($_ =~ /record=(.*)~/);
+            my @pickup = ($_ =~ /"patFuncPickup">(\w+)<\/td>/);
+            my @title = ($_ =~ /record=.*>\s(.*)<\/a>/);
+            my @status = ($_ =~ /"patFuncStatus">\s(\w+)\s<\/td>/);
+            my @itemid = ($_ =~ /id="cancel(\w+)x/); 
+            my @linkid = ($_ =~ /id="(cancel.+x\d+)"\s+\/>/); 
+        
+            $hold->{'bibid'} = $bibid[0] if(defined $bibid[0]);
+            $hold->{'pickup'} = $pickup[0] if(defined $pickup[0]);
+            $hold->{'title'} = $title[0] if(defined $title[0]);
+            $hold->{title} =~ s/<.+>.*<\/.+>//g if(defined $hold->{title});
+            $hold->{'status'} = $status[0] if(defined $status[0]);
+            $hold->{'itemid'} = $itemid[0] if(defined $status[0]);
+            $hold->{'linkid'} = $linkid[0] if(defined $status[0]);
+            push @holds, $hold if (defined $hold->{'bibid'});
+        }
+
+        if($user->{surname}){
+            $user->{error} = 0;
+            $user->{error_meessage} = '';
+        }else{
+            $user->{error} = 1;
+            $user->{error_message} = 'supplied user could not be looked up';
+        }
+    }
+    
+    return \@holds;
+}
+
+
+sub checkout{
+    my $self = $_[0];
+    my $user_barcode = $_[1];
+    my $ssh = $self->initialize;
+    my @temp_screen;   
+    my @screenData; 
+    my @lines; 
+    my $circ;
+
+    eval{
+        $ssh->print("S");
+        $ssh->waitfor(-match => '/prominently\?/',
+                   -errmode => "return") or die "Search failed. Could not retrieve user ", $user_barcode;
+        $ssh->print("P");
+        $ssh->waitfor(-match => '/RECORDS/',
+            -errmode => "return") or die "Search failed. Could not retrieve user ", $user_barcode;
+    
+        $ssh->print("D");
+        $ssh->waitfor(-match => '/BARCODE/',
+            -errmode => "return") or die "Search failed. Could not retrieve user ", $user_barcode;     
+
+        $ssh->print($user_barcode);
+    
+    }or do {
+        $circ = {error => 1, error_message => $@};
+
+    };
+
+
+}
+
+
+
+sub checkin{
+    my $self = $_[0];
+    my $user_barcode = $_[1];
+    my $ssh = $self->initialize;
+    my @temp_screen;
+    my @screenData;
+    my @lines;
+    my $circ;
+
+    eval{
+        $ssh->print("S");
+
+
+    }or do{
+
+
+    }
+
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+sub loggedIn{
+    my $self = $_[0];
+    my $response = $_[1];
+    if($response =~ /Please enter the following information/g){
+        return "f";
+    }
+
+    return "t";
+}
+
+
+
+
+
+1;
+
+
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/III/2011_1_3.pm b/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/III/2011_1_3.pm
new file mode 100644 (file)
index 0000000..e0cbdfd
--- /dev/null
@@ -0,0 +1,1156 @@
+package FulfILLment::LAIConnector::III::2011_1_3;
+use base FulfILLment::LAIConnector::III;
+use strict; use warnings;
+use OpenSRF::Utils::Logger qw/$logger/;
+
+use MARC::Record;
+use MARC::Batch;
+use MARC::File::XML (BinaryEncoding => 'utf8');
+use LWP::UserAgent;
+use HTTP::Request;
+use WWW::Mechanize;
+use XML::Simple;
+use Data::Dumper;
+
+my $ua = LWP::UserAgent->new;
+$ua->agent("FulfILLment/1.0");
+
+sub get_record_by_id_ssh {
+    my ($self, $record_id) = @_;
+
+    $self->ssh_connect or return;
+    $self->send_wait('S', 'Choose one') or return;
+    $self->send_wait('B', 'Choose one') or return;
+    $self->send_wait('R', 'Type Record') or return;
+    $self->send_wait("$record_id", 'Choose one') or return;
+    $self->send_wait('T', 'BIBLIOGRAPHIC Information') or return;
+    $self->send_wait('M');
+    my ($pre, $post) = $self->send_wait('T', 'Regular Display') or return;
+    $self->ssh_disconnect or return;
+
+    my @lines = split(/\[\d+;\d+(;\d+)?[Hm]/, $pre);
+    
+    my @marc;
+    foreach (@lines){   
+        next unless $_;
+        s/^(\s+)//g;
+        next unless /^\d{3}/;
+        s/\x1b//g;
+        s/\[0m//g;
+        s/\[0xF\]//g;
+        s/[[:cntrl:]]//g;
+        push @marc,$_;
+    }
+
+    my $rec =  breaker2marc(\@marc);
+    $rec->insert_fields_ordered(
+        MARC::Field->new('907', ' ', ' ', a => $record_id)
+    ) unless $rec->subfield('907' => 'a');
+
+    my $x =  $rec->as_xml_record;
+    $x =~ s/^<\?.+?\?>.//sm;
+    return {marc => $x};
+}
+
+sub get_items_by_record_guts {
+    my ($self, $record_id) = @_;
+    my @items;
+
+    $self->send_wait('S', 'prominently') or return;
+    $self->send_wait('B', 'SEARCHING RECORDS') or return;
+    $self->send_wait('R', 'RECORD') or return;
+    $self->send_wait($record_id, 'Choose one') or return;
+
+    my ($prematch, $match) = $self->send_wait(
+        'S', 'To see a particular|BARCODE\s*[^\s]+') or return;
+
+    if ($match =~ /BARCODE/) {
+        # single-copy bib jumps right to copy details.
+
+
+        # we need the Copy Type from the match as well, 
+        # so push it back into the main text
+        $prematch .= $match;
+        my $item = $self->parse_item_screen($prematch);
+
+        unless ($item and $item->{barcode}) {
+            $logger->warn(
+                "FF III unable to parse single-item screen for $record_id");
+            return;
+        }
+
+        push(@items, $item);
+
+    } else {
+        # multi-copy bib
+
+        my @item_indexes = ($prematch =~ /ITEM\s+(\d+)\s>/g);
+
+        for my $index (@item_indexes) {
+            my @response = $self->send_wait($index, 'Record SUMMARY') or last;
+            my $screen = $response[0];
+
+            $logger->debug("FF III item screen contains ".
+                length($screen)." characters");
+
+            my $item;
+            $logger->debug("FF III parsing item screen for entry $index");
+            $item = $self->parse_item_screen($screen);
+
+            unless ($item and $item->{barcode}) {
+                $logger->warn(
+                    "FF III unable to parse item screen for $record_id");
+                last;
+            }
+
+            push(@items, $item);
+
+            # return to the summary screen
+            $self->send_wait('S', 'To see a particular') or last;
+        }
+    }
+
+    return @items;
+}
+
+sub get_user_guts {
+    my ($self, $user_barcode, $user_pass) = @_;
+
+    $self->send_wait('S', 'prominently') or return;
+    $self->send_wait('P', 'RECORDS') or return;
+    $self->send_wait('D', 'BARCODE') or return; # 2009 uses 'B'
+
+    my ($txt, $match) = $self->send_wait(
+        $user_barcode, 
+        'Record|BARCODE not found'
+    ) or return;
+
+    if ($match =~ /BARCODE not found/) {
+        $logger->info("FF III user '$user_barcode' not found");
+        return;
+    }
+
+    $txt =~ s/\[\d+;\d+(;\d+)?[Hm]//g;
+    $txt =~ s/^\d\d\d\s+.+//g;
+    $txt =~ s/\x1b//g;
+    $txt =~ s/\[0m//g;
+    $txt =~ s/\[0xF\]//g;
+    $txt =~ s/[[:cntrl:]]//g;
+
+    my $user = {
+        exp_date => qr/EXP DATE:\s(\d+\-\d+\-\d+)/,
+        user_id => qr/PIN\s+([A-Za-z0-9]+)INPUT/,
+        notice_pref => qr/NOTICE PREF:\s(.*)TOT/,
+        lang_pref => qr/LANG PREF:\s+(\w+)/,
+        mblock => qr/MBLOCK:\s+([A-Za-z0-9\-])\s+/,
+        patron_agency => qr/PAT AGENCY:\s+(\d+)\s+/,
+        overdue_items_count => qr/HLODUES:\s+(\d+)/,
+        notes => qr/PMESSAGE:\s+([A-Za-z0-9\-])/,
+        home_ou => qr/HOME LIBR:\s+(\w+)/,
+        total_renewals  => qr/TOT RENWAL:\s+(\d+)/,
+        email_address => qr/EMAIL ADDR\s+([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,3})/,
+        claims_returned_count => qr/CL RTRND:\s(\d+)/,
+        loaned_items_count => qr/CUR CHKOUT:\s(\d+)/,
+        money_owed => qr/MONEY OWED:\s(\$\d+\.\d+)/,
+        overdue_penalty => qr/OD PENALTY:\s(\d+)/,
+        institution_code => qr/HOME LIBR:\s+(\w+)/,
+        block_until => qr/BLK UNTIL:\s(\d+\-\d+\-\d+)/,
+        full_name => qr/PATRN NAME\s+(.*)ADDRESS/,
+        street_address => qr/ADDRESS\s+(.*)SSN/,
+        ssn => qr/SSN #\s+(\d+)P BARCODE/,
+        photocopy_count => qr/PIUSE:\s+(\d+)/,
+        patron_code1 => qr/PCODE1:\s+(\d+)/,
+        patron_code2 => qr/PCODE2:\s+(\d+)/,
+        patron_code3 => qr/PCODE3:\s+(\d+)/,
+        patron_code4 => qr/PCODE4:\s+(\d+)/,
+        notes => qr/PMESSAGE:\s+([A-Za-z0-9\-])/,
+        ill_request => qr/ILL REQUES:\s+(\d+)/,
+        last_circ_date => qr/CIRCACTIV:\s(\d+\-\d+\-\d+)/,
+        patron_type => qr/P TYPE:\s(\d+)/,
+        census => qr/CENSUS:\s(\d+)\s+/,
+        total_checkouts => qr/TOT CHKOUT:\s(\d+)/,
+        checkouts => qr/CUR CHKOUT:\s+(\d+)/,
+        phone => qr/TELEPHONE\s+(\d\d\d\-\d\d\d\-\d\d\d\d)/,
+        input_by => qr/INPUT BY\s+([A-Za-z\/]+)/,
+        birth_date => qr/BIRTH DAT:(\d\d\-\d\d-\d\d)/,
+        cur_item_A => qr/CUR ITEMA:\s(\d+)/,
+        cur_item_B => qr/CUR ITEMB:\s(\d+)/,
+        cur_item_C => qr/CUR ITEMC:\s(\d+)/,
+        cur_item_D => qr/CUR ITEMD:\s(\d+)/,
+        error => 0,
+    };
+
+    for my $key (keys %$user) {
+        my ($val) = ($txt =~ $user->{$key});
+        $user->{$key} = $val;
+    }
+
+    my %prefs = (
+        z => 'email',
+        t => 'phone',
+        p => 'secondary phone'
+    );
+
+    $user->{notice_pref} = $prefs{$user->{notice_pref}};
+    $user->{barcode} = $user_barcode;
+    
+    return $user;
+}
+
+# not needed if SIP is used.
+sub get_item_via_ssh {
+    my ($self, $item_barcode) = @_;
+
+    $self->ssh_connect or return;
+
+    $self->send_wait('S', 'prominently') or return;
+    $self->send_wait('I', 'SEARCHING RECORDS') or return;
+    $self->send_wait('D', 'BARCODE') or return; # NOTE: 2009 uses 'B'
+    my ($screen) = $self->send_wait($item_barcode, 'NEW Search') or return;
+
+    $self->ssh_disconnect;
+
+    if ($screen =~ /not found|PATRON Information/g) {
+        $logger->info("FF III unable to locate item $item_barcode");
+        return;
+    }
+
+    my $item = $self->parse_item_screen($screen, $item_barcode);
+
+    $logger->error("FF III error parsing item screen for $item_barcode")
+        unless $item;
+
+    return $item;
+}
+
+
+sub place_record_hold {
+    my ($self, $record_id, $user_barcode) = @_;
+
+    my $host = $self->{host};
+    my $password = $self->{'passwd.hold'} || $self->{passwd};
+
+    # $self->{port} usually == 80; need a separate SSL port config
+    my $port = 443; 
+
+    # XXX do we need an org setting for default lender hold pickup location?
+    # TODO: web form pads the pickup lib values.  add padding via sprintf to match.
+    my $pickup_lib = 'pla    ';
+
+    chop($record_id); # strip check digit
+    $record_id = substr($record_id, 1); # strip the initial '.'
+
+    my $url = "https://$host/search~S1?/.$record_id/".
+        ".$record_id/1%2C1%2C1%2CB/request~$record_id";
+
+    $logger->info("FF III title hold URL: $url");
+
+    my $mech = WWW::Mechanize->new();
+    my $content;
+
+    eval {
+        $mech->post($url);
+        $mech->form_name('patform');
+        $mech->set_fields('pin', $password, 'code', $user_barcode);
+        $mech->select('locx00', $pickup_lib);
+        my $response = $mech->submit(); 
+        $content = $mech->content if $response;
+    };
+
+    $logger->info($content); # XXX
+  
+    if ($@ or !$content){
+        my $msg = $@ || 'no content';
+        $logger->info("FF III error placing title hold on $record_id : $msg");
+        return {error => 1, error_message => $@};
+    }
+    
+    my @title = ($content =~ /<p>Requesting <strong>(.+)<\/strong><br \/><p>/);
+    my @err_response_msg = ($content =~ /<font color="red" size="\+2">(.+)<\/font>/g);
+    my @success_response_msg = ($content =~ /Your request for .* was successful./g);
+    my @delivered_to = ($content =~ /Your request will be delivered to .* when it is available./);
+
+    if($content =~ /No Such Record/){
+        $logger->info("FF III no such record $record_id in title hold");
+        return {
+            error => 1, 
+            error_message => # TODO: i18n?
+                "A hold could not be placed on $record_id, no such record"
+        }
+    }
+    
+    if ($content =~ /Request denied/){
+        $logger->info("FF III title hold for $record_id denied");
+        $err_response_msg[0] =~ s/<strong>//;
+        $err_response_msg[0] =~ s/<\/strong>//;
+
+        return {
+            error => 1,
+            error_message => $err_response_msg[0],
+            title => $title[0]
+        };
+
+    } elsif ($content =~ /Your request for .* was successful./g){
+        $success_response_msg[0] =~ s/<strong>//;
+        $success_response_msg[0] =~ s/<\/strong>//;
+
+        return {
+            error => 0,
+            success_message => $success_response_msg[0],
+            title => $title[0]
+        };
+    }
+
+    @title = ($content =~ 
+        /class="bibInfoLabel">Title<\/td>\n<td class="bibInfoData">\n<strong>(.*)<\/strong>/g);
+    
+    if ($title[0]){
+        return {
+            error => 0,
+            success_message => "Your request for $title[0] was successful.",
+            title => $title[0]
+        };
+    }
+}
+
+
+# ---------------------------------------------------------------------------
+# Everything below here needs modification and testing
+# ---------------------------------------------------------------------------
+
+
+sub get_item_fields{
+    my $self=$_[0];
+    my $itemData=$_[1];
+    return [] unless $itemData;
+    $logger->debug("FF III get_item_fields parsing " .length($itemData)." characters");
+    #print Dumper $itemData;
+    $itemData =~ s/\e\[\d+(?>(;\d+)*)[mh]//gi;
+    $itemData =~ s/\e\[k//gi;
+    $itemData =~ s/[[:cntrl:]]//g;
+    $itemData =~ s/VOLUME/VOLUME:/g;
+    $itemData =~ s/CALL #/CALL #:/g;
+    $itemData =~ s/R > Browse Nearby EntriesI > Show similar ITEMSN >//g;
+    $itemData =~ s/R > RETURN to BrowsingZ > Show Items Nearby on ShelfF > FORWARD browseI > Show similar ITEMSN >//g;
+    $itemData =~ s/U > Show BIBLIOGRAPHIC RecordZ > Show Items Nearby on Shelf//g;
+    $itemData =~ s/\+ > ADDITIONAL options1-2,N,A,Z,I,U,T,E,\+\).*ITEM Information//g;
+    $itemData =~ s/I > Show similar ITEMSA > ANOTHER Search by RECORD #//g;
+    $itemData =~ s/U > Show similar BIBLIOGRAPHIC RecordS > Record SUMMARY//g;
+    $itemData =~ s/U > Show BIBLIOGRAPHIC RecordS > Record SUMMARY//g;
+    $itemData =~ s/T > Display MARC RecordE > Mark item for//g;
+    $itemData =~ s/U > Show BIBLIOGRAPHIC Record//g;
+    $itemData =~ s/NEW Search//g;
+    $itemData =~ s/N,A,S,Z,I,U,T,E\)//g;
+    $itemData =~ s/N >//g;
+   
+    if(my @l = ($itemData =~ m/(\d+)BARCODE/g)){
+        $itemData =~ s/VOLUME:/VOLUME: $l[0]/g;
+    }
+    
+    $itemData =~ s/(\d+)BARCODE/BARCODE:/g;
+    #$itemData =~ s/1BARCODE/BARCODE/g;
+    my @fields = grep {defined and not /^\s*$/} split /(\s{2,})|(-  -)/, $itemData;
+    #my $i=0;
+    my @newfields = [];
+   
+   foreach (@fields){
+     #$_.=':' if $_ eq 'VOLUME';
+     $_.=':' if $_ eq 'BARCODE';
+     
+     if((/^[ \-]+$/ or $newfields[$#newfields] eq 'BARCODE:') and @newfields){
+         $newfields[$#newfields] .=$_;
+     }else{
+         unless(ref($_) eq 'ARRAY'){
+            push @newfields, $_;
+         }
+     }
+       
+     #$i++;
+    }
+  
+   return \@newfields;
+}
+
+
+
+
+
+
+
+sub get_item_call_and_bib_number {
+    $logger->debug("In get_item_call_and_bib_number");
+    my $self = $_[0];
+    my $item_data = $_[1];
+    $item_data =~ s/\e\[\d+(?>(;\d+)*)[mh]//gi;
+    $item_data =~ s/\e\[k//gi;
+    $item_data =~ s/[[:cntrl:]]//g;
+
+    my @c = ($item_data =~ /BIBLIOGRAPHIC Information\s+CALL #\s+(.*?)AUTHOR/);
+    
+    if(not defined($c[0])){
+        @c = ($item_data =~ /BIBLIOGRAPHIC Information\s+CALL #\s(.*?)TITLE/);
+    }
+
+    if (!$c[0]) {
+        # in multi-copy records, the call number is embedded in the 
+        # copy data and has no predictable terminating string.
+        # capture it all and chop it off at the first occurence 
+        # of 2 consecutive spaces
+
+        @c = ($item_data =~ /CALL #\s+(.*)/);
+        if ($c[0]) {
+            $c[0] =~ s/(.*?)\s{2}.*/$1/mg;
+            $c[0] =~ s/\s+$//;
+        } else {
+            $logger->warn("FF III unable to parse callnumber");
+            $c[0] = 'UNKNOWN';
+        }
+    }
+
+    #get Barcode
+    my @b = ($item_data =~ /BARCODE:.*(B.*)\s+BIBLIOGRAPHIC Information/);
+    
+    if($b[0]){
+        $b[0] =~ s/\s//g;
+    }else{
+        @b = ($item_data =~ /BARCODE\s+([^\s]+)/);
+    }
+    #get title
+    my @f = split("TITLE",$item_data) ;
+    my @fingerprint = ($f[0] =~ /\s+(.*)\s{5,}/);    
+    my @out;
+    
+    if(not defined $fingerprint[0]){ 
+        @fingerprint = ($item_data =~ /TITLE\s+(.*)\s{5,}/);
+    }
+
+    @out = ($b[0],$c[0],$fingerprint[0]);
+    $logger->debug("Exiting get_item_call_and_bib_number");
+    return \@out;
+}
+
+
+
+         
+
+sub parse_item_screen{
+    $logger->debug("In parse_item_screen");
+    my $self = $_[0];
+    my $screen = $_[1];
+    my $jhash={};
+    my @nvp;
+    my $label;
+    my $value;
+    my @screen_split;
+    my $rs = $screen;
+    my @record_number = ($rs =~ /(I\d.*)\s+ITEM Information/g);
+    my $e = "ESC[7;2H";
+    $e = ord($e);
+    $record_number[0] =~ s/\s+//g;
+    push @screen_split, split(/ITEM Information/,$screen);
+    #print Dumper @screen_split;
+    my $fields = $self->get_item_fields($screen_split[1]); 
+
+
+    my $call_and_bib = $self->get_item_call_and_bib_number($screen_split[1]);
+
+    my $barcode = (defined($_[2])) ? $_[2] : $call_and_bib->[0];
+    my @fingerprint = ($screen =~ /TITLE\s+([^.!?\s][^.!?]*)\s+/);
+    $logger->debug("setting fields to their FulfILLment equivalents"); 
+    
+    foreach(@$fields){
+        @nvp=split(":",$_);
+        $label=$nvp[0];
+        $value=$nvp[1];
+        if($label eq "DUE DATE"){
+            $label="due_date";
+        }elsif($label eq "BARCODE"){
+            $label="barcode";
+        }elsif($label eq "STATUS"){
+            $label="holdable";
+        }elsif($label eq "LOCATION"){
+            $label="location";
+        }elsif($label eq "PRICE"){
+            $label="price";
+        }elsif($label eq "COPY #"){
+            $label = "copy_number";
+        }elsif($label eq "I TYPE"){
+            $label = "item_type";
+        }elsif($label eq "IMESSAGE"){
+            $label = "note";
+        }elsif($label eq "AGENCY"){
+            $label = "agency";
+        }elsif($label eq "IN LOC"){
+            $label = "in_location";
+        }elsif($label eq "LOU"){
+            $label = "last_checkout_date";
+        }elsif($label eq "ICODE1"){
+            $label = "item_code1";
+        }elsif($label eq "ICODE2"){
+            $label = "item_code2";
+        }elsif($label eq "ICODE3"){
+            $label = "item_code3";
+        }elsif($label eq "IUSE1"){
+            $label = "item_use1";
+        }elsif($label eq "IUSE2"){
+            $label = "item_use2";
+        }elsif($label eq "IUSE3"){
+            $label = "item_use3";
+        }elsif($label eq "OPACMSG"){
+            $label = "opac_msg";
+        }elsif($label eq "OUT LOC"){
+            $label = "out_location";
+        }elsif($label eq "# RENEWALS"){
+            $label = "num_renewals";
+        }elsif($label eq "# OVERDUE"){
+            $label = "num_overdue";
+        }elsif($label eq "LOANRULE"){
+            $label = "loanrule";
+        }elsif($label eq "LYRCIRC"){
+            $label = "last_year_circ_stats";
+        }
+
+        if($label eq "holdable" and $value eq " -"){
+            $value="t";
+        }elsif($label eq "holdable" and $value eq "e"){
+            $value="t";
+        }elsif($label eq "holdable" ){
+            $value="f";
+        }
+        
+        $jhash->{$label}=$value;
+    }
+
+        #print Dumper $call_and_bib;
+        if($jhash->{due_date}){
+            if($jhash->{'due_date'} eq "-  -"){
+                $jhash->{'due_date'} = '';
+            }
+        }
+
+        # avoid leading/trailing spaces in cn
+        $call_and_bib->[1] =~ s/^\s+//g;
+        $call_and_bib->[1] =~ s/\s+$//g;
+
+        $jhash->{'call_number'} = $call_and_bib->[1];
+        #$jhash->{'bib_id'}=$call_and_bib->[0];
+        $jhash->{'item_id'} = $record_number[0]; 
+        $jhash->{'barcode'} = $barcode;
+        $jhash->{'error_message'} = '';  
+        #$jhash->{'fingerprint'} = $fingerprint[0];
+        $jhash->{'fingerprint'} = '';
+        #print Dumper $jhash;
+        $logger->debug("Exiting parse_item_screen");
+        #print  Dumper $jhash; 
+        return $jhash;
+}
+
+
+sub get_item_by_call_number{
+    my $self = $_[0];
+    my $ssh = $self->initialize;
+    my $item_id = $_[1];
+    my $call_number_type = $_[2];
+    my @out;
+    my @entries; #If there are multiple entries for an item, the entries are stored here.
+    #print "preparing to search the catalog\n";
+    $ssh->print("S");
+    $ssh->waitfor(-match => '/prominently\?/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+    #print "ok\n";
+    #print "selecting option to search for items\n";
+    $ssh->print("I");
+    $ssh->waitfor(-match => '/SEARCHING RECORDS/',
+                  -errmode => "return")or die "search failed;", $ssh->lastline;
+    #print "ok\n";
+    #select attribute to search by, i.e title, barcode etc...
+    $ssh->print("C");
+    $ssh->waitfor(-match => '/CALL NUMBER SEARCHES/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+    
+    if(lc($call_number_type) eq "dewey"){
+        
+        $ssh->print("D");
+        $ssh->waitfor(-match => '/DEWEY CALL NO :/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+    
+    }elsif(lc($call_number_type) eq "lc"){
+        $ssh->print("C");
+        $ssh->waitfor(-match => '/LC CALL NO :/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+    
+    }elsif(lc($call_number_type) eq "local"){
+        $ssh->print("L");
+        $ssh->waitfor(-match => '/LOCAL CALL NO :/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+    }
+    
+    $ssh->print($item_id); 
+    push @out,$ssh->waitfor(-match => '/NEW Search/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+    #print "Done.\n";
+    
+    if(my @num_entries =  ($out[0] =~ /(\d+)\sentries found/)){
+        #my $i = 0;
+        #$i++;
+        $ssh->print(1);
+        push @out,$ssh->waitfor(-match => '/NEW Search/',
+                  -errmode => "return") or die "search failed;", $ssh->lastline;
+        #print "Done.\n";
+    }
+
+    $self->ssh_disconnect;
+    my @items;
+    push @items,$self->parse_item_screen($out[2]);
+    return \@items;
+}
+
+
+#Under construction
+
+sub get_item_by_bib_number_z3950{
+    my $self = $_[0];
+    my $bibID = $_[1];    
+    my $marc = $self->get_bib_records_by_record_number_z3950($bibID);
+    my $batch = MARC::Batch->new('XML', $marc ); 
+    while (my $m = $batch->next ){
+        print $m->subfield(650,"a"),"\n";
+
+    }
+    #my $record = $batch->next();
+    #print Dumper $record; 
+
+}
+
+
+
+
+
+# Method: get_bib_records_by_record_num
+# Params:
+#   idList => list of record numbers
+
+#BIBLIOGRAPHIC RECORDS
+#Notes: Section contains methods to retrieve and parse bibliographic records
+#==================================================================================
+
+
+sub get_bib_records_by_record_number{
+    my $self = $_[0];
+    my $ssh = $self->initialize;
+    my $id = $_[1];
+    my $count=0;
+    my @out;
+    my @screen;
+    my @marc;
+    my @bib = ();
+    my $jhash={};
+    
+    eval{ 
+    if(ref($ssh) eq "ARRAY"){
+        return $ssh;
+    }
+
+    #select SEARCH the catalog
+    #print "getBibRecords\n";
+    #print "preparing to search the catalog\n";
+    $ssh->print("S");
+    
+    $ssh->waitfor(-match => '/prominently\?/',
+                  -errmode => "return") or die "Search failed. Could not retrieve ", $id;
+
+    #print "ok\n";
+    #print "selecting option to search for bibliographic records\n";
+    #select item record option
+    $ssh->print("B");
+    $ssh->waitfor(-match => '/SEARCHING RECORDS/',
+                  -errmode => "return") or die "Search failed. Could not retrieve ", $id;
+    
+    #print "ok\n";
+    #print "selecting option to search by record number\n";
+    #select attribute to search by, i.e title, barcode etc...
+    $ssh->print("R"); #search by record number
+    #print "ok\n";
+    #print "searching for id=$id\n";
+    #search for id
+    $ssh->print($id);
+    my $first = 1;
+    #print "searching more of the document\n";
+    my @bid;
+    
+    my $get_more = sub {
+                        my @temp_screen = (); 
+                        my @lines;
+                        my $last = 0;
+                        my $get_more = shift;
+                        
+                        if($first == 1){
+                            $ssh->print("T");
+                            $ssh->waitfor(-match => '/BIBLIOGRAPHIC Information/',
+                                -errmode => "return") or die "Search failed. Could not retrieve ", $id;
+                                #-errmode => "return") or die "search failed;", $ssh->lastline;
+                            $ssh->print("M");
+                            #$first = 0;
+                        }    
+                        
+                        $ssh->print("M");
+                        $ssh->print("T");
+                        #delete the following two lines
+                        
+                        push @temp_screen ,$ssh->waitfor(-match => '/Regular Display/',
+                                -errmode => "return") or die "Search failed. Could not retrieve ", $id;
+                                #-errmode => "return") or die "search failed;", $ssh->lastline;
+                        #print Dumper $temp_screen[0];
+                        
+                        @bid = ($temp_screen[0] =~ /([Bb].*)\s+BIBLIOGRAPHIC Information/);
+                        if($temp_screen[0] =~ /COUNTRY:/g and $first == 0 ){
+                            #print Dumper $temp_screen[0];
+                            #print "Reached end of record... I think\n";
+                            return 1; 
+                        }
+                        push @lines,split(/\[\d+;\d+(;\d+)?[Hm]/,$temp_screen[0]);
+                        
+                        foreach  (@lines){   
+                              if($_){ 
+                                $_ =~ s/^(\s+)//g;
+                                if($_ =~/^\d\d\d\s+.+/g){
+                                   $_ =~ s/\x1b//g;
+                                   $_ =~ s/\[0m//g;
+                                   $_ =~ s/\[0xF\]//g;
+                                   $_ =~ s/[[:cntrl:]]//g;
+                                   push @marc,$_;
+                                   #print Dumper $_;
+                                }
+                             }
+                        }
+                        #print Dumper @lines;
+
+                        $first = 0;
+                        #print Dumper @marc; 
+                        $ssh->print("M");
+                        $ssh->print("T");
+                        @temp_screen = (); 
+                        $get_more->($get_more);
+                   };
+
+
+    $get_more->($get_more);
+
+    my @id = ($bid[0]) ? ($bid[0] =~ /([Bb][0-9]+)\s+/) : ""  ;
+    #print Dumper @marc; 
+     
+    #print "bid = ".$id[0]."\n";
+    #print "ok\n"; 
+    #push @out, $self->parse_bib_records( $self->grab_bib_screen($ssh,$id), $id );
+    #print "logging out\n";
+    $ssh->print("N");
+    $ssh->print("Q");
+    $ssh->print("Q");
+    $ssh->print("X");
+    $ssh->waitfor(-match => '/closed/',
+                  #-errmode => "return")or die "log out failed;", $ssh->lastline;
+                  -errmode => "return") or die "Search failed. Could not retrieve ", $id;
+    
+    #print "logged out\n";
+    my $rec =  breaker2marc(\@marc);
+    $rec->insert_fields_ordered(
+        MARC::Field->new(
+            '907',
+            ' ',
+            ' ',
+            'a' => $id[0]
+        )
+    ) if (!$rec->subfield( '907' => 'a' ));
+
+    my $x =  $rec->as_xml_record;
+    $x =~ s/^<\?.+?\?>.//sm;
+    #my $jhash={};
+    $jhash->{'marc'}=$x;
+    #$bid =~ s/\[/$bid/g;
+    $jhash->{'id'}=$id[0];
+    $jhash->{'format'}="marcxml";
+    $jhash->{error} = 0;
+    $jhash->{error_message} = '';
+    #print "xml = $x\n";
+    #my @bib = ();
+    push @bib,$jhash;
+    #warn Dumper \@bib; 
+    return \@bib;
+
+    1;
+    }or do {
+        $jhash->{error} = 1;
+        $jhash->{error_message} = $@;
+        push @bib,$jhash;
+        return \@bib;
+    }
+
+}
+
+
+
+
+sub get_range_of_records{
+    my $self = $_[0];
+    my $list = $_[1];
+    my $dir = `pwd`;
+    my $file = "/openils/lib/perl5/FulfILLment/WWW/LAIConnector/conf/III_2009B_1_2/marc/marc_dump.mrc";
+    open FILE, ">$file" or die &!;
+   
+    foreach my $r (@$list){
+        my $record = $self->get_bib_records_by_record_number($r);
+
+        print FILE $record->[0]->{marc};   
+
+    }
+    
+    close FILE;
+    my @out;
+    my $jhash;
+    push @out,$jhash;
+    return \@out;
+
+}
+
+
+
+
+
+
+
+sub breaker2marc {
+    my $lines = shift;
+    my $delim = quotemeta(shift() || '|');
+    my $rec = new MARC::Record;
+    for my $line (@$lines) {
+
+        chomp($line);
+
+        if ($line =~ /^=?(\d{3})\s{2}(.)(.)\s(.+)$/) {
+
+            my ($tag, $i1, $i2, $rest) = ($1, $2, $3, $4);
+            if ($tag < 10) {
+                $rec->insert_fields_ordered( MARC::Field->new( $tag => $rest ) );
+
+            } else {
+
+                my @subfield_data = split $delim, $rest;
+                if ($subfield_data[0]) {
+                    $subfield_data[0] = 'a' . $subfield_data[0];
+                } else {
+                    shift @subfield_data;
+                }
+
+                my @subfields;
+                for my $sfd (@subfield_data) {
+                    if ($sfd =~ /^(.)(.+)$/) {
+                        push @subfields, $1, $2;
+                    }
+                }
+
+                $rec->insert_fields_ordered(
+                    MARC::Field->new(
+                        $tag,
+                        $i1,
+                        $i2,
+                        @subfields
+                    )
+                ) if @subfields;
+            }
+        }
+    }
+
+    return $rec;
+}
+
+
+#END BIBLIOGRAPHIC RECORDS
+#=====================================================================================
+
+
+
+
+
+#Places hold on a III server through the web interface
+
+
+
+sub parse_hold_response{
+    my $self = $_[0];
+    my $txt = $_[1];
+    my @resp = split("END SEARCH WIDGET -->",$txt);
+    my $success = ($resp[1] =~ /denied/) ? "false" : "true"; 
+    if(!defined($resp[1])){$success = "false"}
+    my $msg = $resp[1];
+    $msg =~ s/<p>//g;
+    $msg =~ s/<\/p>//g;
+    $msg =~ s/<br \/>//g;
+    $msg =~ s/&nbsp;//g;
+    $msg =~ s/<!-End the bottom logo table-->//g;
+    $msg =~ s/<\/body>//g;
+    $msg =~ s/<\/html>//g;
+    $msg =~ s/<\/strong\>//g;
+    $msg =~ s/<strong>//g;
+    $msg =~ s/\./\. /g;
+    $msg =~ s/\n//g;
+    $msg =~ s/<font color="red"\s+size="\+2">/\. /g;
+    $msg =~ s/<\/font>//g;
+    my $data = {};
+    $data->{'success'} = $success;
+    $data->{'message'} = $msg;
+    my $out = []; 
+    $out->[0] = $data;
+    return $out; 
+}
+
+
+
+sub list_holds{
+    $logger->debug("In list_holds");
+    my $self = shift;
+    my $host = $self->{host};
+    my $port = $self->{port};
+    my $user_barcode = shift;
+    my $passwd = shift;
+    my $patron_sys_num = shift;
+    my $action = shift || "list_holds";
+    my $response;
+    my $content;
+    $logger->debug("params are host=$host\n port=$port\n user_barcode=$user_barcode\n passwd=$passwd\n patron_sys_number=$patron_sys_num");
+    my $url = "https://$host:$port/patroninfo~S0/$patron_sys_num/holds/?name=$user_barcode&code=$passwd";
+    my $out;
+    my $mech = WWW::Mechanize->new();
+    
+    eval{
+        $mech->post($url);
+        $mech->form_name('patform');
+        $mech->set_fields('pin',$passwd,
+                          'code',$user_barcode  
+                    
+                            );
+
+        $response = $mech->submit(); 
+        $content = $mech->content;
+    };
+    
+    if($@){
+        my $hold;
+        my @out;
+        $hold->{error} = 1;
+        $hold->{error_message} = "There was an error looking up holds for the user $user_barcode : $@";
+        push @out,$hold;
+        $logger->debug("Exiting list_holds");
+        return \@out;
+    }
+    
+    $logger->debug("Exiting list_holds");
+    return $self->parse_hold_list($content,"list_holds");
+
+}
+
+
+
+sub list_holds_by_bib{
+    my $self = shift;
+    my $host = shift;
+    my $port = shift;
+    my $login = shift;
+    my $user_barcode = shift;
+    my $bibID = shift;
+    my $patron_sys_num = shift;
+    my $holds =  $self->list_holds($host,$port,$login,$user_barcode,$patron_sys_num);
+    my @out;
+
+    foreach(@$holds){
+        if($_->{'bibid'} eq $bibID){
+            push @out,$_;
+        }
+    }
+    
+    return \@out;
+}
+
+
+
+sub list_holds_by_item{
+    my $self = shift;
+    my $host = shift;
+    my $port = shift;
+    my $login = shift;
+    my $user_barcode = shift;
+    my $itemID = shift;
+    my $patron_sys_num = shift;
+    my $holds =  $self->list_holds($host,$port,$login,$user_barcode,$patron_sys_num);
+    my @out;
+
+    foreach(@$holds){
+        if($_->{'itemid'} eq $itemID){
+            push @out,$_;
+        }
+    }
+    
+    return \@out;
+}
+
+
+sub delete_hold{
+    $logger->debug("In delete_hold");
+    my $self = shift;
+    my $host = $self->{host};
+    my $port = $self->{port};
+    my $userid = $self->{login};
+    $userid =~ s/\s/%20/g;
+    my $search_id = shift;
+    my $id_type = shift;
+    my $patron_sys_num = shift;
+    my $user_barcode = shift;
+
+    $logger->debug("fields are host=$host\n port=$port \n userid=$userid \n search_id=$search_id \n id_type=$id_type \n patron_sys_num=$patron_sys_num \n user_barcode=$user_barcode\n");
+    
+    my $response_body;
+    my $holds =  $self->list_holds($userid,$user_barcode,$patron_sys_num);
+    my $itemid;
+    my $linkid;
+    my $num_holds_before = @$holds;
+    my $num_holds_after;
+    
+    if($id_type eq "bib"){ 
+    
+        if(length($search_id) > 8){
+            chop($search_id);
+        }
+        
+        foreach(@$holds){
+            #print "bibid = $_->{bibid}  search_id = $search_id\n";
+            if($_->{'bibid'} eq $search_id){
+                #print "item id = ".$_->{'itemid'}."\n";
+                $itemid = $_->{'itemid'};
+                $linkid = $_->{'linkid'};
+            }
+        }
+    }elsif($id_type eq "item"){
+        foreach(@$holds){
+            if($_->{'itemid'} eq $search_id){
+                #print "item id = ".$_->{'itemid'}."\n";
+                $itemid = $_->{'itemid'};
+                $linkid = $_->{'linkid'};
+            }
+        }
+    }
+   
+    $itemid = (defined($itemid)) ? $itemid : "";
+    $linkid = (defined($linkid)) ? $linkid : "";
+    my $url = "https://$host:$port/patroninfo~SO/$patron_sys_num/holds?name=$userid&code=$user_barcode&$linkid=on&currentsortorder=current_pickup&updateholdssome=YES&loc$itemid=";
+    my @out;
+    my $msg = {};
+    my $mech = WWW::Mechanize->new();
+    $response_body = $mech->post($url);
+
+    if($mech->success){
+       my $nha = $self->list_holds($userid,$user_barcode,$patron_sys_num);
+       $num_holds_after = @$nha;
+       #check to see whether the user was successfully authenticated 
+       my $auth = $self->loggedIn($response_body);
+       
+       if($auth ne "t"){
+           $msg->{"error"} = 1;
+           $msg->{"error_message"} = "The user $userid could not be authenticated";
+           push @out, $msg;
+           return \@out;
+       }
+       
+       if($num_holds_before == $num_holds_after){
+           $msg->{"error"} = 1;
+           $msg->{"error_message"} = "The hold for $search_id either does not exist or could not be deleted";
+           push @out, $msg;
+           return \@out;
+       }elsif($num_holds_before > $num_holds_after){
+           $msg->{"error"} = 0;
+           $msg->{"success_message"} = "The hold for $search_id has been deleted";
+           push @out, $msg; 
+           return \@out;
+       }
+       
+    }else{
+       $msg->{"error"} = 1;
+       $msg->{"error_message"} = "An error occured: code $mech->status";
+       push @out, $msg;
+       return \@out; 
+    }
+}
+
+
+
+
+sub parse_hold_list{
+    my $self = shift;
+    my $in = shift;
+    my $action = shift;
+    $in =~ s/\n//g;
+    my @holds; 
+    my @holdEntries = split(/(<tr class="patFuncEntry".*?<\/tr>)/ ,$in); 
+    my $user = {};
+    shift @holdEntries;
+    pop @holdEntries;
+    my @userSurname = ($in =~ /<h4>Patron Record for<\/h4><strong>(\w+),.*<\/strong><br \/>/);
+    my @userGivenName = ($in =~ /<h4>Patron Record for<\/h4><strong>(\w+),.*<\/strong><br \/>/);
+    my @expDate = ($in =~ /EXP DATE:(\d\d\-\d\d\-\d\d\d\d)<br \/>/);
+
+    
+    $user->{surname} = $userSurname[0]; 
+    $user->{given_name} = $userGivenName[0]; 
+    $user->{exp_date} = $expDate[0]; 
+    
+    if($action eq "lookup_user"){
+        push @holds,$user;
+    }elsif($action eq "list_holds"){
+
+        foreach(@holdEntries){
+            my $hold = {};
+            my @bibid = ($_ =~ /record=(.*)~/);
+            my @pickup = ($_ =~ /"patFuncPickup">(\w+)<\/td>/);
+            my @title = ($_ =~ /record=.*>\s(.*)<\/a>/);
+            my @status = ($_ =~ /"patFuncStatus">\s(\w+)\s<\/td>/);
+            my @itemid = ($_ =~ /id="cancel(\w+)x/); 
+            my @linkid = ($_ =~ /id="(cancel.+x\d+)"\s+\/>/); 
+        
+            $hold->{'bibid'} = $bibid[0] if(defined $bibid[0]);
+            $hold->{'pickup'} = $pickup[0] if(defined $pickup[0]);
+            $hold->{'title'} = $title[0] if(defined $title[0]);
+            $hold->{title} =~ s/<.+>.*<\/.+>//g if(defined $hold->{title});
+            $hold->{'status'} = $status[0] if(defined $status[0]);
+            $hold->{'itemid'} = $itemid[0] if(defined $status[0]);
+            $hold->{'linkid'} = $linkid[0] if(defined $status[0]);
+            push @holds, $hold if (defined $hold->{'bibid'});
+        }
+
+        if($user->{surname}){
+            $user->{error} = 0;
+            $user->{error_meessage} = '';
+        }else{
+            $user->{error} = 1;
+            $user->{error_message} = 'supplied user could not be looked up';
+        }
+    }
+    
+    return \@holds;
+}
+
+
+sub loggedIn{
+    my $self = $_[0];
+    my $response = $_[1];
+    if($response =~ /Please enter the following information/g){
+        return "f";
+    }
+
+    return "t";
+}
+
+
+
+
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Koha.pm b/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Koha.pm
new file mode 100644 (file)
index 0000000..76e7282
--- /dev/null
@@ -0,0 +1,254 @@
+package FulfILLment::LAIConnector::Koha;
+use base FulfILLment::LAIConnector;
+use strict; use warnings;
+use XML::LibXML;
+use LWP::UserAgent;
+use OpenSRF::Utils::Logger qw/$logger/;
+
+# TODO: for holds
+use DateTime;
+my $U = 'OpenILS::Application::AppUtils';
+use OpenILS::Utils::CStoreEditor qw/:funcs/;
+
+# special thanks to Koha => misc/migration_tools/koha-svc.pl
+sub svc_login { 
+    my $self = shift;
+    return $self->{svc_agent} if $self->{svc_agent};
+
+    my $username = $self->{extra}->{'svc.user'} || $self->{user};
+    my $password = $self->{extra}->{'svc.password'} || $self->{passwd};
+
+    # TODO: https (setting)
+    my $url = sprintf(
+        "http://%s/cgi-bin/koha/svc", 
+        $self->{extra}->{'svc.host'} || $self->{host}
+    ); 
+
+    my $ua = LWP::UserAgent->new();
+    $ua->cookie_jar({});
+
+    $logger->info("FF Koha logging in at $url/authentication");
+
+    my $resp = $ua->post(
+        "$url/authentication",
+        {userid => $username, password => $password}
+    );
+
+    if (!$resp->is_success) {
+        $logger->error("FF Koha svc login failed " . $resp->status_line);
+        return;
+    }
+
+    $self->{svc_url} = $url;
+    $self->{svc_agent} = $ua;
+
+    return 1;
+}
+
+sub escape_xml {
+    my $str = shift;
+    $str =~ s/&/&amp;/sog;
+    $str =~ s/</&lt;/sog;
+    $str =~ s/>/&gt;/sog;
+    return $str;
+}
+
+# sends a MARCXML stub record w/ a single embedded copy
+sub create_borrower_copy {
+    my ($self, $ref_copy, $circ_lib_code) = @_;
+    return unless $self->svc_login;
+
+    my $marc = <<XML;
+<record
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
+  xmlns="http://www.loc.gov/MARC21/slim">
+  <datafield tag="100" ind1="1" ind2=" ">
+    <subfield code="a">AUTHOR</subfield>
+  </datafield>
+  <datafield tag="245" ind1="1" ind2="0">
+    <subfield code="a">TITLE</subfield>
+  </datafield>
+  <datafield tag="952" ind1=" " ind2=" ">
+    <subfield code="p">BARCODE</subfield>
+    <subfield code="o">CALLNUMBER</subfield>
+    <subfield code="a">LOCATION</subfield>
+  </datafield>
+</record>
+XML
+
+    my $title = escape_xml($ref_copy->call_number->record->simple_record->title);
+    my $author = escape_xml($ref_copy->call_number->record->simple_record->author);
+    my $barcode = escape_xml($ref_copy->barcode); # TODO: setting for leading org id
+    my $callnumber = escape_xml($ref_copy->call_number->label);
+
+    $marc =~ s/TITLE/$title/g;
+    $marc =~ s/AUTHOR/$author/g;
+    $marc =~ s/BARCODE/$barcode/g;
+    $marc =~ s/CALLNUMBER/$callnumber/g;
+    $marc =~ s/LOCATION/$circ_lib_code/g;
+
+    $logger->info("FF Koha borrower rec/copy: $marc");
+
+    my $resp = $self->{svc_agent}->post(
+        $self->{svc_url} . "/new_bib?items=1",
+        {POSTDATA => $marc} 
+        # note: passing Content => $marc fails
+    );
+
+    if (!$resp->is_success) {
+        $logger->error("FF Koha create_borrower_copy " . $resp->status_line);
+        return;
+    }
+
+    $logger->info($resp->decoded_content);
+
+    my $resp_xml = XML::LibXML->new->parse_string($resp->decoded_content);
+    $logger->info($resp_xml);
+    $logger->info($resp_xml->toString);
+
+    my $error = $resp_xml->getElementsByTagName('error')->string_value;
+    my $marcxml = $resp_xml->getElementsByTagName('record')->shift;
+
+    return {
+        error => $error,
+        barcode => $error ? '' : $barcode, # return bc on success
+        title => $title,
+        author => $author,
+        location => $circ_lib_code,
+        call_number => $callnumber,
+        remote_id => $resp_xml->getElementsByTagName('biblionumber')->string_value,
+        status => $resp_xml->getElementsByTagName('status')->string_value,
+        marcxml => $marcxml ? $marcxml->toString : ''
+    };
+}
+
+sub get_record_by_id {
+    my ($self, $record_id, $with_items) = @_;
+    return unless $self->svc_login;
+
+    $with_items = '?items=1' if $with_items;
+
+    my $url = $self->{svc_url}."/bib/$record_id$with_items";
+    my $resp = $self->{svc_agent}->get($url);
+
+    if (!$resp->is_success) {
+        $logger->error("FF Koha record_by_id failed " . $resp->status_line);
+        return;
+    }
+
+    return $resp->decoded_content
+}
+
+# NOTE: unused, but kept for reference
+sub get_record_by_id_z3950 {
+    my ($self, $record_id) = @_;
+
+    my $attr = $self->{args}{extra}{'z3950.search_attr'};
+
+    # Koha returns holdings by default, which is useful
+    # for get_items_by_record (below).
+
+    my $xml = $self->z39_client->get_record_by_id(
+        $record_id, $attr, undef, 'xml', 1) or return;
+
+    return {marc => $xml, id => $record_id};
+}
+
+sub get_items_by_record {
+    my ($self, $record_id) = @_;
+
+    my $rec = $self->get_record_by_id($record_id, 1) or return [];
+    
+    # when calling get_record_by_id_z3950 
+    # my $doc = XML::LibXML->new->parse_string($rec->{marc}) or return [];
+
+    my $doc = XML::LibXML->new->parse_string($rec) or return [];
+
+    # marc code to copy field map
+    my %map = (
+        o => 'call_number',
+        p => 'barcode',
+        a => 'location_code'
+    );
+
+    my @items;
+    for my $node ($doc->findnodes('//*[@tag="952"]')) {
+
+        my $item = {bib_id => $record_id};
+
+        for my $key (keys %map) {
+            my $val = $node->findnodes("./*[\@code='$key']")->string_value;
+            next unless $val;
+            $val =~ s/^\s+|\s+$//g; # cleanup
+            $item->{$map{$key}} = $val;
+        }
+
+        push (@items, $item);
+    }
+
+    return \@items;
+}
+
+# NOTE: initial code review suggests Koha only supports bib-level
+# holds via SIP, but they are created via copy barcode (not bib id).
+# Needs more research
+
+sub place_borrower_hold {
+    my ($self, $item_barcode, $user_barcode, $pickup_lib) = @_;
+
+    # NOTE: i believe koha ignores (but requires) the hold type
+    my $hold = $self->place_hold_via_sip(
+        undef, $item_barcode, $user_barcode, $pickup_lib, 3)
+        or return;
+
+    $hold->{hold_type} = 'T';
+    return $hold;
+}
+
+sub place_lender_hold {
+    my ($self, $item_barcode, $user_barcode, $pickup_lib) = @_;
+
+    # NOTE: i believe koha ignores (but requires) the hold type
+    my $hold = $self->place_hold_via_sip(
+        undef, $item_barcode, $user_barcode, $pickup_lib, 2)
+        or return;
+
+    $hold->{hold_type} = 'T';
+    return $hold;
+}
+
+sub delete_borrower_hold {
+    my ($self, $item_barcode, $user_barcode) = @_;
+
+    # TODO: find the hold in the FF db to determine the pickup_lib
+    # for now, assume pickup lib matches the user's home lib
+    my $user = $self->flesh_user($user_barcode);
+    my $pickup_lib = $user->home_ou->shortname if $user;
+
+    my $resp = $self->sip_client->delete_hold(
+        $user_barcode, undef, undef, 
+        $pickup_lib, 3, $item_barcode)
+        or return;
+
+    return unless $resp;
+    return $self->translate_sip_hold($resp);
+}
+
+sub delete_lender_hold {
+    my ($self, $item_barcode, $user_barcode) = @_;
+
+    my $user = $self->flesh_user($user_barcode);
+    my $pickup_lib = $user->home_ou->shortname if $user;
+
+    my $resp = $self->sip_client->delete_hold(
+        $user_barcode, undef, undef, 
+        $pickup_lib, 2, $item_barcode)
+        or return;
+
+    return unless $resp;
+    return $self->translate_sip_hold($resp);
+}
+
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Polaris.pm b/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Polaris.pm
new file mode 100644 (file)
index 0000000..c740554
--- /dev/null
@@ -0,0 +1,6 @@
+package FulfILLment::LAIConnector::Polaris;
+use base FulfILLment::LAIConnector;
+use strict; use warnings;
+use OpenSRF::Utils::Logger qw/$logger/;
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Symphony.pm b/Open-ILS/src/perlmods/lib/FulfILLment/LAIConnector/Symphony.pm
new file mode 100644 (file)
index 0000000..ba0debe
--- /dev/null
@@ -0,0 +1,6 @@
+package FulfILLment::LAIConnector::Symphony;
+use base FulfILLment::LAIConnector;
+use strict; use warnings;
+use OpenSRF::Utils::Logger qw/$logger/;
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/Util/NCIP.pm b/Open-ILS/src/perlmods/lib/FulfILLment/Util/NCIP.pm
new file mode 100644 (file)
index 0000000..a9c8b45
--- /dev/null
@@ -0,0 +1,151 @@
+package FulfILLment::Util::NCIP;
+use strict;
+use warnings;
+use IO::Socket;
+use Data::Dumper;
+use XML::LibXML;
+use LWP::UserAgent;
+use HTTP::Request;
+use Template;
+use OpenSRF::Utils::Logger qw/$logger/;
+
+my $ua = LWP::UserAgent->new;
+$ua->agent("FulfILLment/1.0");
+$ua->default_header('Content-Type' => 'application/xml; charset="utf-8"');
+
+sub new {
+    my ($class, %args) = @_;
+    return bless(\%args, $class);
+}
+
+sub request {
+    my ($self, $type, %params) = @_;
+
+    $logger->info("FF NCIP sending message $type");
+
+    my $xml = $self->compile_xml($type, %params);
+    return unless $xml;
+
+    my $proto = $self->{protocol} || '';
+    my $resp_xml;
+    if ($proto =~ /http/i) {
+        $resp_xml = $self->send_via_http($xml);
+    } elsif ($proto =~ /tcp/i) {
+        $resp_xml = $self->send_via_tcp($xml);
+    } else {
+        $logger->error("FF Invalid NCIP protocol '$proto'");
+        return;
+    }
+
+    my $doc = $self->parse_xml($resp_xml) or return;
+    return ($doc, $self->extract_ncip_errors($doc));
+}
+
+# parses/verifies XML and returns an XML doc
+sub parse_xml {
+    my ($self, $xml) = @_;
+
+    my $parser = XML::LibXML->new;
+    $parser->keep_blanks(0);
+
+    my $doc;
+    eval { $doc = $parser->parse_string($xml) };
+
+    if (!$doc) {
+        $logger->error("FF invalid XML for NCIP message $@ : $xml");
+        return;
+    }
+
+    my $log_xml = $doc->toString;
+    $log_xml =~ s/\n/ /g;
+    $logger->debug("FF NCIP XML : $log_xml");
+
+    return $doc;
+}
+
+
+# extract all //Problem/* text values
+sub extract_ncip_errors {
+    my ($self, $doc) = @_;
+    my @errors;
+    my $prob_xpath = '//Problem//Value';
+    push(@errors, $_->textContent) for $doc->findnodes($prob_xpath);
+    return @errors;
+}
+
+# sends the xml template for the requested message type 
+# through TT to generate the final XML message.
+sub compile_xml {
+    my ($self, $type, %params) = @_;
+
+    # insert the agency info into the template environment
+    $params{ff_agency_name}  = $self->{ff_agency_name};
+    $params{ff_agency_uri}   = $self->{ff_agency_uri};
+    $params{ils_agency_name} = $self->{ils_agency_name};
+    $params{ils_agency_uri}  = $self->{ils_agency_uri};
+
+    my $template = "$type.tt2";
+
+    my $tt = Template->new({
+        ENCODING => 'utf-8',
+        INCLUDE_PATH => $self->{template_paths}
+    });
+
+    my $xml = '';
+    if ($tt->process($template, \%params, \$xml)) {
+
+        my $doc = $self->parse_xml($xml) or return;
+        return $doc->toString;
+
+    } else {
+        $logger->error("FF NCIP XML template error : ".$tt->error);
+        return;
+    }
+}
+
+sub send_via_http {
+    my ($self, $xml) = @_;
+
+    my $url = sprintf(
+        '%s://%s:%s%s', 
+        $self->{protocol}, 
+        $self->{host}, 
+        $self->{port}, 
+        $self->{path}
+    );
+
+    $logger->debug("FF NCIP url = $url");
+
+    my $r = HTTP::Request->new('POST', $url);
+    $r->content($xml);
+    my $resp = $ua->request($r);
+
+    return $resp->decoded_content if $resp->is_success;
+
+    $logger->error("FF NCIP HTTP(S) Error : " . $resp->status_line);
+    return;
+}
+
+sub send_via_tcp {
+    my ($self, $xml) = @_;
+
+    my $sock = IO::Socket::INET->new(
+        PeerAddr => $self->{host},
+        PeerPort => $self->{port},
+        Proto => 'tcp',
+        Timeout => 10,
+    );    
+
+    if (!$sock) {
+        $logger->error("FF NCIP TCP connection error $!");
+        return;
+    }
+
+    $sock->send($xml);
+    my $resp_xml = <$sock>; 
+
+    $sock->close or $logger->warn("FF error closing socket $!");
+    return $resp_xml;
+}
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/Util/SIP2Client.pm b/Open-ILS/src/perlmods/lib/FulfILLment/Util/SIP2Client.pm
new file mode 100644 (file)
index 0000000..d7f68a1
--- /dev/null
@@ -0,0 +1,459 @@
+#
+#===============================================================================
+#
+#         FILE: SIP2client.pm
+#
+#  DESCRIPTION: 
+#
+#        FILES: ---
+#         BUGS: ---
+#        NOTES: ---
+#       AUTHOR: Michael Davadrian Smith (), msmith@esilibrary.com
+#      COMPANY: Equinox Software
+#      VERSION: 1.0
+#      CREATED: 05/14/2012 03:27:10 PM
+#     REVISION: ---
+#===============================================================================
+
+package FulfILLment::Util::SIP2Client;
+
+use strict;
+use warnings;
+use Data::Dumper;
+use IO::Socket::INET;
+use Encode;
+use Sip qw(:all);
+use Sip::Checksum qw(checksum); 
+use Sip::Constants qw(:all); 
+use Sip::MsgType;
+use OpenSRF::Utils::Logger qw/:logger/;
+
+$Sip::protocol_version = 2;
+$Sip::error_detection = 1;
+$/ = "\r";
+
+sub new { 
+    my ($type) = $_[0]; 
+    my $self = {}; 
+    $self->{host} = $_[1]; 
+    $self->{login_username} = $_[2]; 
+    $self->{login_passwd} = $_[3]; 
+    $self->{port} = $_[4];
+    $self->{protocol} = $_[5];
+    $self->{location} = $_[6];
+    bless($self,$type);
+}
+
+sub socket{
+    my $self = shift;
+
+    if ($self->{socket}) {
+        # logout may cause disconnect
+        return $self->{socket} if $self->{socket}->connected;
+
+        # probably excessive, but can't hurt to clean up
+        $self->{socket}->shutdown(2);
+        $self->{socket}->close;
+    }
+
+    $logger->debug("FF creating SIP socket to ".$self->{host});
+
+    $self->{socket} = IO::Socket::INET->new(
+        PeerAddr => $self->{host},
+        Proto => $self->{protocol},
+        PeerPort => $self->{port}
+    ) or die "Cannot connect to host $self->{host} $@";
+
+    return $self->{socket};
+}
+
+sub sendMsg{
+  my $self = $_[0];
+  my $msg = $_[1];
+  my $seqno = $_[2] || 1;
+  my $resp;
+  my %fields;
+  my $sock = $self->socket;
+  $sock->autoflush(1); 
+  $logger->info("FF SIP request => $msg");
+  write_msg({seqno => $seqno},$msg,$sock);
+  $resp = <$sock>;
+  # SIP Msg hates leading spaces especially
+  $resp =~ s/^\s+|\s+$//mg;
+  $logger->info("FF SIP response => $resp");
+  return $resp;
+}
+
+sub login{
+    my $self = $_[0];
+    return $self->{login_resp} if ($self->{logged_in});
+    my $userid = $self->{login_username};
+    my $userpasswd = $self->{login_passwd};
+    my $locationCode = $self->{location};
+
+    # some SIP servers do not require login
+    return $self->{logged_in} = 1 unless $userid and $userpasswd;
+
+    my $msg = "93  CN$userid|CO$userpasswd|"; 
+    $self->{login_resp} = $self->sendMsg($msg);
+    my $u = Sip::MsgType->new($self->{login_resp},0);
+    my ($ok) = @{$u->{fixed_fields}};
+    $self->{logged_in} = ($ok eq 'Y');
+    return $ok;
+
+}
+
+sub logout{
+    my $self = $_[0];
+    $self->{login_resp} = $self->{logged_in} = undef;
+    if ($self->{socket}) {
+        $self->{socket}->shutdown(2);
+        $self->{socket}->close;
+        $self->{socket} = undef;
+    }
+}
+
+# args:
+# patron_id
+# patron_pass - optional
+# start_index - optional
+# end_index   - optional
+sub lookup_user {
+    my $self = shift;
+    my $args = shift;
+
+    $args->{enable_summary_pos} = 0 # backwards compat
+        unless $args->{enable_summary_pos};
+
+    my $msg = '63001'; # message 63, language english (001)
+    $msg .= Sip::timestamp();
+
+    # summary field is 10 slots, all spaces, one Y allowed.
+    $msg .= $args->{enable_summary_pos} == $_ ? 'Y' : ' ' for (0..9);
+
+    $msg .= add_field(FID_INST_ID, $self->{location});
+    $msg .= add_field(FID_PATRON_ID, $args->{patron_id});
+
+    # optional fields
+    $msg .= maybe_add(FID_TERMINAL_PWD, $self->{login_passwd});
+    $msg .= maybe_add(FID_PATRON_PWD, $args->{patron_pass});
+    $msg .= maybe_add(FID_START_ITEM, $args->{start_index});
+    $msg .= maybe_add(FID_END_ITEM, $args->{end_index});
+
+    $self->login;
+
+    my $resp = $self->sendMsg($msg, 5);
+    my $u = Sip::MsgType->new($resp, 0, 1);
+    my $out = $self->lookup_user_handler($u);
+
+    $self->logout;
+    return $out;
+}
+
+
+# deprecated, start using lookup_user() instead
+sub lookupUser{
+    my $self = $_[0];
+    my $terminalPwd = $self->{login_passwd};
+    my $location = $self->{location};   #institution id
+    my $patronId = $_[1];
+    my $patronPasswd = $_[2];
+    my $start_item = $_[3] || 1;
+    my $end_item = $_[4] || 10;
+    my $msg = '63001';  #sets message id to 63 and the language to english, 001
+    $msg .= Sip::timestamp()."Y";
+    $msg .= ' ' x 9;  #Adds an empty 10 spaces for the summary field
+    $msg .= add_field(FID_INST_ID,$location);
+    $msg .= add_field(FID_PATRON_ID,$patronId);
+    $msg .= maybe_add(FID_TERMINAL_PWD,$terminalPwd);
+    $msg .= maybe_add(FID_PATRON_PWD,$patronPasswd);
+    $msg .= maybe_add(FID_START_ITEM,$start_item);
+    $msg .= maybe_add(FID_END_ITEM,$end_item);  
+    $self->login;
+    my $resp = $self->sendMsg($msg,5);
+    #$self->logout;
+    my $u = Sip::MsgType->new($resp,0);
+    my $out = $self->lookup_user_handler($u);
+    $self->logout;
+    return $out;
+}
+
+sub lookup_user_handler{
+    my $self = shift;
+    my $data = shift;
+
+    if (!$data) {
+        $logger->error("FF SIP lookup_user returned no response");
+        return;
+    }
+
+    my $fields = $data->{fields};
+    my $fixed_fields = $data->{fixed_fields};
+    my @wholename = split(',',$fields->{AE});
+    my $surname = $wholename[0];
+    my $given_name = $wholename[1];
+    my $valid_patron = $fields->{BL};
+    my $error = 0;
+    my $error_message = "";
+    $fields->{AF} ||= '';
+
+    if($valid_patron eq "N" || $fields->{AF} eq "User not found"){
+        $error = 1;
+        $error_message =  "User is not valid";   
+    }
+     
+    my $out = {
+                #user_id => $fields->{AA},
+                patron_status => $fixed_fields->[0],
+                langauge => $fixed_fields->[1],
+                transaction_date => $fixed_fields->[2],
+                hold_items_count => $fixed_fields->[3],
+                overdue_items_count => $data->{fixed_fields}->[4],
+                charged_items_count => $data->{fixed_fields}->[5],
+                fine_items_count => $data->{fixed_fields}->[6],
+                recall_items_count => $data->{fixed_fields}->[7],
+                unavailable_holds_count => $data->{fixed_fields}->[8],
+                institution_id => $fields->{AO},
+                patron_identifier => $fields->{AA},
+                personal_name => $fields->{AE},
+                holds_items_limit => $fields->{BZ},
+                overdue_items_limit => $fields->{CA},
+                charged_items_limit => $fields->{CB},
+                valid_patron => $fields->{BL},
+                valid_patron_password => $fields->{CQ},
+                currency_type => $fields->{BH},
+                fee_amount => $fields->{BV},
+                fee_limit => $fields->{CC},
+                hold_items => $fields->{AS},
+                overdue_items => $fields->{AT},
+                charged_items => $fields->{AU},
+                fine_items => $fields->{AV},
+                recall_items => $fields->{BU},
+                unavailable_hold_items => $fields->{CD},
+                home_address => $fields->{BD},
+                email_address => $fields->{BE},
+                home_phone_number => $fields->{BF},
+                screen_message => $fields->{AF},
+                print_line => $fields->{AG},
+    };
+
+    return $out;
+}
+
+sub checkout{
+    my $self = $_[0];
+    my $terminalPwd = $self->{login_passwd};
+    my $location = $self->{location};   #institution id
+    my $patron_id = $_[1];
+    my $patron_passwd =$_[2];
+    my $item_id = $_[3];
+    my $fee_ack = $_[4] || "N";  #value should be Y or N
+    my $cancel = $_[5] || "N";  #value should be Y or N
+    my $due_date_epoch = $_[6];
+    my $msg = '11NN';  #sets message id to 11, no blocking, no autorenew
+    $msg .= Sip::timestamp();
+    $msg .= Sip::timestamp($due_date_epoch);
+    $msg .= maybe_add(FID_INST_ID,$location);
+    $msg .= add_field(FID_PATRON_ID,$patron_id);
+    $msg .= add_field(FID_ITEM_ID,$item_id);
+    $msg .= add_field(FID_TERMINAL_PWD,$terminalPwd);
+    $msg .= maybe_add(FID_PATRON_PWD,$patron_passwd);
+    $msg .= maybe_add(FID_FEE_ACK,$fee_ack);
+    $msg .= maybe_add(FID_CANCEL,$cancel);
+    $self->login;
+    my $resp = $self->sendMsg($msg,5);
+    my $co = Sip::MsgType->new($resp,0);
+    $self->logout;
+    return $co;
+
+}
+
+sub checkin{
+    my $self = $_[0];
+    my $terminalPwd = $self->{login_passwd};
+    my $location = $self->{location};   #institution id
+    my $patron_id = $_[1];
+    my $patron_passwd =$_[2];
+    my $item_id = $_[3];
+    my $item_properties = $_[4];
+    my $cancel = $_[5] || "N";  #value should be Y or N
+    my $fee_ack = $_[6] || "N";  #value should be Y or N
+    my $msg = '09N';  #sets message id to 09, no blocking
+    $msg .= Sip::timestamp();
+    $msg .= Sip::timestamp();
+    $msg .= add_field(FID_CURRENT_LOCN,$location);
+    $msg .= add_field(FID_INST_ID,$location);
+    $msg .= add_field(FID_PATRON_ID,$patron_id);
+    $msg .= add_field(FID_ITEM_ID,$item_id);
+    $msg .= add_field(FID_TERMINAL_PWD,$terminalPwd);
+    $msg .= add_field(FID_ITEM_PROPS,$item_properties);
+    $msg .= add_field(FID_CANCEL,$cancel);
+    $msg .= add_field(FID_FEE_ACK,$fee_ack);
+    $self->login;
+    my $resp = $self->sendMsg($msg,5);
+    my $ci = Sip::MsgType->new($resp,0);
+    $self->logout;
+    return $ci;
+
+}
+
+# translates a SIP checkout or checkin response to a human-friendlier hash
+sub sip_msg_to_circ {
+    my ($self, $msg, $type) = @_;
+    $type |= '';
+
+    my $fields = $msg->{fields};
+    my $fixed_fields = $msg->{fixed_fields};
+
+    $logger->debug("FF mapping SIP to circ for " . Dumper($msg));
+
+    my $circ = {
+        error => !$fixed_fields->[0],
+        magnetic_media => $fixed_fields->[2], # Y N U
+        transaction_date => $fixed_fields->[4],
+        institution_id => $fields->{AO},
+        patron_id => $fields->{AA},
+        item_id => $fields->{AB},
+        due_date => $fields->{AH},
+        fee_type => $fields->{BT},
+        security_inhibit => $fields->{BI},
+        currency_type => $fields->{BH},
+        fee_amount => $fields->{BV},
+        media_type => $fields->{BV},
+        item_properties => $fields->{CH},
+        transaction_id => $fields->{BK},
+        screen_message => $fields->{AF},
+        print_line => $fields->{AG},
+        permanent_location => $fields->{AQ}
+        #title => 
+        #call_number =>
+        #price => 
+    };
+
+    if ($type eq 'checkout') {
+        $circ->{renewal_ok} = $fixed_fields->[1];
+        $circ->{desensitize} = $fixed_fields->[3];
+    } else {
+        $circ->{resensitize} = $fixed_fields->[1];
+        $circ->{alert} = $fixed_fields->[3];
+    }
+
+    return $circ;
+}
+
+
+
+sub item_information_request{
+    my ($self,$item_id) = @_;    
+    $Sip::protocol_version = 2;
+    my $msg = "17".Sip::timestamp();
+    $msg .= add_field(FID_INST_ID,$self->{location});
+    $msg .= add_field(FID_ITEM_ID,$item_id);
+    $msg .= add_field(FID_TERMINAL_PWD,$self->{login_passwd});
+    $self->login;
+    my $resp = $self->sendMsg($msg,6);
+    my $u = Sip::MsgType->new($resp,0);
+    $self->logout;
+    return unless $u;
+    my $out = $self->item_information_request_handler($u);
+    return $out;
+}
+
+sub item_information_request_handler{
+    my $self  = $_[0];
+    my $response = $_[1];
+    my $fixed_fields = $response->{fixed_fields};
+    my $fields = $response->{fields};
+    
+    my $out = {
+                circulation_status => $fixed_fields->[0],
+                security_marker =>  $fixed_fields->[1],
+                fee_type => $fixed_fields->[2],
+                transaction_date => $fixed_fields->[3],
+                hold_queue_length => $fields->{CF},
+                due_date => $fields->{AH},
+                recall_date => $fields->{CJ},
+                hold_pickup_date => $fields->{CM},
+                item_identifier => $fields->{AB},
+                title_identifier => $fields->{AJ},
+                owner => $fields->{BG},
+                currency_type => $fields->{BH},
+                fee_amount => $fields->{BV},
+                media_type => $fields->{CK},
+                permanent_location => $fields->{AQ},
+                current_location => $fields->{AP},
+                item_properties => $fields->{CH},
+                screen_message => $fields->{AF},
+                print_line => $fields->{AG},
+    }; 
+    
+    return $out; 
+}
+
+
+sub build_hold_msg{
+    my ($self,$patron_id, $patron_pwd ,$holdMode,
+        $expiration_date, $pickup_location, $hold_type,
+        $item_id, $title_id, $fee_acknowledged 
+        ) = @_;
+    
+    my $location = $self->{location};
+    my $msg = "15";
+    my $terminal_pwd = $self->{login_passwd} ; 
+
+    if($holdMode eq "add"){
+        $holdMode = "+";
+    }elsif($holdMode eq "delete"){
+        $holdMode = "-";
+    }elsif($holdMode eq "change"){
+        $holdMode = "*";
+    }
+
+    $msg .= $holdMode . Sip::timestamp();
+    $msg .= maybe_add(FID_EXPIRATION,$expiration_date);
+    $msg .= maybe_add(FID_PICKUP_LOCN,$pickup_location);
+    $msg .= maybe_add(FID_HOLD_TYPE,$hold_type);
+    $msg .= maybe_add(FID_INST_ID,$location);
+    $msg .= add_field(FID_PATRON_ID,$patron_id);
+    $msg .= maybe_add(FID_PATRON_PWD,$patron_pwd); 
+    $msg .= maybe_add(FID_ITEM_ID,$item_id); 
+    $msg .= maybe_add(FID_TITLE_ID,$title_id);
+    $msg .= maybe_add(FID_TERMINAL_PWD,$terminal_pwd); 
+    #$msg .= maybe_add(FID_FEE_ACK,$fee_acknowledged);  #This field did not work when tested with the Sirsi-dynex implementation 
+    
+    return $msg;
+    
+}
+
+sub place_hold{
+    
+    my ($self,$patron_id, $patron_pwd ,$expiration_date, $pickup_location, $hold_type,
+        $item_id, $title_id, $fee_acknowledged 
+        ) = @_;
+
+    my $hold_mode = "add";
+    my $msg = $self->build_hold_msg($patron_id, $patron_pwd,$hold_mode,
+        $expiration_date ,$pickup_location,$hold_type,$item_id,$title_id,$fee_acknowledged);
+    $self->login;
+    my $resp = $self->sendMsg($msg,5);
+    my $u = Sip::MsgType->new($resp,0);
+    $self->logout;
+    return $u;
+}
+
+sub delete_hold{
+    my ($self,$patron_id, $patron_pwd ,$expiration_date, $pickup_location, $hold_type,
+        $item_id, $title_id, $fee_acknowledged 
+        ) = @_;
+
+    my $hold_mode = "delete";
+    my $msg = $self->build_hold_msg($patron_id, $patron_pwd,$hold_mode,
+        $expiration_date ,$pickup_location,$hold_type,$item_id,$title_id,$fee_acknowledged);
+    $self->login;
+    my $resp = $self->sendMsg($msg,5);
+    my $u = Sip::MsgType->new($resp,0);
+    return $u;
+    $self->logout;
+}
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/Util/Z3950.pm b/Open-ILS/src/perlmods/lib/FulfILLment/Util/Z3950.pm
new file mode 100644 (file)
index 0000000..2b5fb7b
--- /dev/null
@@ -0,0 +1,244 @@
+#
+#===============================================================================
+#
+#         FILE: z39.50.pm
+#
+#  DESCRIPTION: 
+#
+#        FILES: ---
+#         BUGS: ---
+#        NOTES: ---
+#       AUTHOR: Michael Davadrian Smith (), msmith@esilibrary.com
+#      COMPANY: Equinox Software
+#      VERSION: 1.0
+#      CREATED: 12/12/2011 01:12:32 PM
+#     REVISION: ---
+#===============================================================================
+
+package FulfILLment::Util::Z3950;
+use strict;
+use warnings;
+use Data::Dumper;
+use JSON::XS;
+use ZOOM;
+use MARC::Record;
+use MARC::File::XML ( BinaryEncoding => 'utf8' );
+use MARC::Charset;
+use OpenSRF::Utils::Logger qw/$logger/;
+
+MARC::Charset->ignore_errors(1);
+
+sub new{
+    my($type) = shift;
+    my $host = shift;
+    #$host =~ s/http:\/\///;
+    my $port = shift;
+    my $database = shift; 
+    my $login= shift;
+    my $password= shift;
+    my ($self)={
+        'host'=>$host,
+        'login' => $login,
+        'password' => $password,
+        'database' => $database,
+        'port' => $port,
+        'out'=>'',#will hold output of query 
+    };
+   
+    bless($self,$type);
+
+}
+
+
+sub breaker2marc {
+    my $lines = shift;
+    my $delim = quotemeta(shift() || '$');
+
+    my $rec = new MARC::Record;
+
+    my $first = 1;
+    for my $line (@$lines) {
+
+        chomp($line);
+
+        if ($first) {
+            if ($line =~ /^\d/) {
+                $rec->leader($line);
+                $first--;
+            }
+        } elsif ($line =~ /^=?(\d{3}) (.)(.) (.+)$/) {
+
+            my ($tag, $i1, $i2, $rest) = ($1, $2, $3, $4);
+
+            if ($tag < 10) {
+                $rec->insert_fields_ordered( MARC::Field->new( $tag => $rest ) );
+
+            } else {
+
+                my @subfield_data = split $delim, $rest;
+                if ($subfield_data[0]) {
+                    $subfield_data[0] = 'a ' . $subfield_data[0];
+                } else {
+                    shift @subfield_data;
+                }
+
+                my @subfields;
+                for my $sfd (@subfield_data) {
+                    if ($sfd =~ /^(.) (.+)$/) {
+                        push @subfields, $1, $2;
+                    }
+                }
+
+                $rec->insert_fields_ordered(
+                    MARC::Field->new(
+                        $tag,
+                        $i1,
+                        $i2,
+                        @subfields
+                    )
+                ) if @subfields;
+            }
+        }
+    }
+    
+    return $rec;
+}
+
+
+sub get_record_by_id {
+    my $self = shift;
+    my $recid = shift;
+    my $attr = shift || '12';
+    my $asxml = shift || 1;
+    my $syntax = shift || 'usmarc';
+    my $return_raw = shift || 0;
+
+    my $conn = new ZOOM::Connection(
+        $self->{'host'},
+        $self->{'port'},
+        databaseName => $self->{'database'},
+        preferredRecordSyntax => $syntax
+    );
+
+    my $query = "\@attr 1=$attr \"$recid\"";
+
+    $logger->info(sprintf(
+        "FF Z3950 sending query to %s/%s => %s", 
+        $self->{host}, $self->{database}, $query));
+
+    my $rs = $conn->search_pqf($query);
+     
+    if ($conn->errcode() != 0) {
+        $logger->error("Z39 bib-by-id failed: " . $conn->errmsg());
+        return;
+    }         
+
+    $logger->info("Z39 bib-by-id returned ".$rs->size." hit(s)");
+
+    return unless $rs->size;
+
+    return $rs->record(0)->raw if $return_raw;
+
+    # warning: render() is highly subjective and may
+    # not behave as expected on all Z servers and formats
+    my $m =  $rs->record(0)->render();
+
+    my $rec =  breaker2marc([ split /\n/, $m ]);
+
+    my $x =  $rec->as_xml_record;
+    $x =~ s/^<\?.+?\?>.//sm;
+
+    my @out;
+    $conn->destroy();
+    if($asxml == 1){
+        return $x;
+    }elsif($asxml == 0){
+        return $m;
+    }
+}
+
+
+
+sub getBibByTitle{
+    my $self = shift;
+    my $title = shift;
+    my $attr = "4";
+    my $asxml = shift || 1;
+
+    my $conn = new ZOOM::Connection(
+        $self->{'host'},
+        $self->{'port'},
+        databaseName => $self->{'database'},
+        preferredRecordSyntax => "usmarc"
+    );
+
+     my $rs = $conn->search_pqf("\@attr 1=$attr \"$title\"");
+     
+     if ($conn->errcode() != 0) {
+        die("something went wrong: " . $conn->errmsg())
+     }         
+    
+    my $m =  $rs->record(0)->render();
+    my $rec =  breaker2marc([ split /\n/, $m ]);
+    my $x =  $rec->as_xml_record;
+    $x =~ s/^<\?.+?\?>.//sm;
+
+    my @out;
+    $conn->destroy();
+    if($asxml == 1){
+        return $x;
+    }elsif($asxml == 0){
+        return $m;
+    }
+}
+
+
+
+
+
+sub queryServer{
+    my $self = shift;
+    my $query = shift;
+    my $asxml = shift || 1;
+    my $conn = new ZOOM::Connection(
+        $self->{host},
+        $self->{port},
+        databaseName => $self->{database},
+        preferredRecordSyntax => "usmarc"
+    );
+    my $rs = $conn->search_pqf($query);
+    
+    if($conn->errcode() != 0){
+        die("something went wrong: ".$conn->errmsg());
+    }
+
+    my $m =  $rs->record(0)->render();
+    my $rec =  breaker2marc([ split /\n/, $m ]);
+    my $x =  $rec->as_xml_record;
+    $x =~ s/^<\?.+?\?>.//sm;
+    my @out;
+    $conn->destroy();
+    if($asxml == 1){
+        return $x;
+    }elsif($asxml == 0){
+        return $m;
+    }
+
+
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1;
diff --git a/Open-ILS/src/perlmods/lib/FulfILLment/WWW/FastImport.pm b/Open-ILS/src/perlmods/lib/FulfILLment/WWW/FastImport.pm
new file mode 100755 (executable)
index 0000000..486cc26
--- /dev/null
@@ -0,0 +1,186 @@
+package FulfILLment::WWW::FastImport;
+use strict;
+use warnings;
+use bytes;
+
+use Apache2::Log;
+use Apache2::Const -compile => qw(OK REDIRECT DECLINED NOT_FOUND FORBIDDEN :log);
+use APR::Const    -compile => qw(:error SUCCESS);
+use APR::Table;
+
+use Apache2::RequestRec ();
+use Apache2::RequestIO ();
+use Apache2::RequestUtil;
+use CGI;
+use Data::Dumper;
+
+use OpenSRF::EX qw(:try);
+use OpenSRF::Utils::Cache;
+use OpenSRF::System;
+use OpenSRF::Utils::SettingsClient;
+use OpenSRF::AppSession;
+use OpenSRF::MultiSession;
+use OpenSRF::Utils::JSON;
+use XML::LibXML;
+
+use OpenILS::Utils::Fieldmapper;
+use OpenSRF::Utils::Logger qw/$logger/;
+
+use MARC::Batch;
+use MARC::File::XML ( BinaryEncoding => 'utf-8' );
+use MARC::Charset;
+use Getopt::Long;
+use Unicode::Normalize;
+use Encode;
+
+use UNIVERSAL::require;
+
+MARC::Charset->ignore_errors(1);
+
+our @formats = qw/USMARC UNIMARC XML BRE/;
+my $MAX_FILE_SIZE = 1073741824; # 1GB
+my $FILE_READ_SIZE = 4096;
+
+# set the bootstrap config and template include directory when
+# this module is loaded
+my $bootstrap;
+
+sub import {
+        my $self = shift;
+        $bootstrap = shift;
+}
+
+
+sub child_init {
+        OpenSRF::System->bootstrap_client( config_file => $bootstrap );
+}
+
+sub handler {
+    my $r = shift;
+    my $cgi = new CGI;
+
+    my $auth = $cgi->param('ses') || $cgi->cookie('ses');
+
+    unless(verify_login($auth)) {
+        $logger->error("authentication failed on vandelay record import: $auth");
+        return Apache2::Const::FORBIDDEN;
+    }
+
+    my $fh = $cgi->param('loadFile');
+    my $x;
+    my $mtype = (sysread($fh,$x,1) && $x =~ /^\D/o) ? 'XML' : 'USMARC';
+
+    sysseek($fh,0,0);
+
+    $r->content_type('html');
+    print '<div>';
+
+    my $conf = OpenSRF::Utils::SettingsClient->new;
+    my $parallel = $conf->config_value(
+        apps => 'fulfillment.www.fast-import' => app_settings => 'parallel'
+    ) || 1;
+
+    my $owner = $cgi->param('uploadLocation');
+
+    my $multi = OpenSRF::MultiSession->new(
+        app => 'open-ils.cstore', 
+        cap => $parallel, 
+        api_level => 1
+    );
+
+    my $batch = new MARC::Batch ($mtype, $fh);
+    $batch->strict_off;
+
+    my $count = 0;
+    my $rec = -1;
+    while (try { $rec = $batch->next } otherwise { $rec = -1 }) {
+        $count++;
+        warn "record $count\n";
+        if ($rec == -1) {
+            print "<div>Processing of record $count in set $fh failed.  Skipping this record</div>";
+            next;
+        }
+
+        try {
+            # Avoid an over-eager MARC::File::XML that may try to convert
+            # our record from MARC8 to UTF8 and break because the record
+            # is obviously already UTF8
+            my $ldr = $rec->leader();
+            if (($mtype eq 'XML') && (substr($ldr, 9, 1) ne 'a')) {
+                print "<div style='color: orange;'>MARCXML record LDR/09 was not 'a'; record leader may be corrupt</div>";
+                substr($ldr,9,1,'a');
+                $rec->leader($ldr);
+            }
+
+            (my $xml = $rec->as_xml_record()) =~ s/\n//sog;
+            $xml =~ s/^<\?xml.+\?\s*>//go;
+            $xml =~ s/>\s+</></go;
+            $xml =~ s/\p{Cc}//go;
+
+            $xml = entityize($xml);
+            $xml =~ s/[\x00-\x1f]//go;
+
+            $multi->request(
+                'open-ils.cstore.json_query',
+                { from => [ 'biblio.fast_import', $owner, $xml ] }
+            );
+
+        } catch Error with {
+            my $error = shift;
+            print "<div style='color: red;'>Encountered a bad record during fast import: $error</div>";
+        };
+
+    }
+
+    print "<div>Completed processing of $count records from $fh</div>";
+    $multi->session_wait(1);
+    $multi->disconnect;
+
+    print '</div>';
+
+    return Apache2::Const::OK;
+}
+
+# xml-escape non-ascii characters
+sub entityize {
+    my($string, $form) = @_;
+    $form ||= "";
+
+    # If we're going to convert non-ASCII characters to XML entities,
+    # we had better be dealing with a UTF8 string to begin with
+    $string = decode_utf8($string);
+
+    if ($form eq 'D') {
+        $string = NFD($string);
+    } else {
+        $string = NFC($string);
+    }
+
+    # Convert raw ampersands to entities
+    $string =~ s/&(?!\S+;)/&amp;/gso;
+
+    # Convert Unicode characters to entities
+    $string =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
+
+    return $string;
+}
+
+sub verify_login {
+        my $auth_token = shift;
+        return undef unless $auth_token;
+
+        my $user = OpenSRF::AppSession
+                ->create("open-ils.auth")
+                ->request( "open-ils.auth.session.retrieve", $auth_token )
+                ->gather(1);
+
+        if (ref($user) eq 'HASH' && $user->{ilsevent} == 1001) {
+                return undef;
+        }
+
+        return $user if ref($user);
+        return undef;
+}
+
+1;
+
diff --git a/Open-ILS/web/staff/js/circ_tracker.js b/Open-ILS/web/staff/js/circ_tracker.js
new file mode 100644 (file)
index 0000000..406596e
--- /dev/null
@@ -0,0 +1,603 @@
+dojo.require("fieldmapper.AutoIDL");
+dojo.require("dijit.layout.ContentPane");
+dojo.require("dijit.layout.TabContainer");
+dojo.require("dijit.form.TextBox");
+dojo.require("openils.widget.AutoGrid");
+dojo.require("openils.User");
+dojo.require("openils.PermaCrud");
+dojo.require("openils.widget.OrgUnitFilteringSelect");
+
+function objSize (obj) {
+    var size = 0, key;
+    for (key in obj) {
+        if (obj.hasOwnProperty(key)) size++;
+    }
+    return size;
+}
+
+
+var user = new openils.User ({authcookie:'ses'});
+openils.User.authtoken = user.authtoken;
+var pcrud = new openils.PermaCrud ({authtoken:user.authtoken});
+var cgi = new CGI ();
+
+var cache = {acp:{}, au:{}, ac:{}, acn:{}, circ:{}, atc:{}};
+var firstrun = true;
+
+function resetLocation(loc,which) {
+
+    if (!dojo.isObject(which)) which = {};
+    var hasSome = objSize(which) > 0;
+
+    if (!firstrun) toMeGrid.resetStore();
+    if (!hasSome || which.tmg) toMeGrid.loadAll(
+        {order_by:{"circ":"xact_start"}},
+        {circ_lib : loc, checkin_time : null}
+    );
+
+    if (!firstrun) fromMeGrid.resetStore();
+    if (!hasSome || which.fmg) fromMeGrid.loadAll(
+        {order_by:{"circ":"xact_start"}},
+        {   checkin_time : null,
+            target_copy : {
+                "in" : {
+                    select: { acp : ['id'] },
+                    from : 'acp',
+                    where: { circ_lib : loc, id : { '=' : {'+circ' : 'target_copy'} } }
+                }
+            }
+        }
+    );
+
+
+    if(!firstrun) nonHoldTransitsFromMeGrid.resetStore();
+    if (!hasSome || which.nhtfmg) nonHoldTransitsFromMeGrid.loadAll(
+        {order_by:{"atc":"source_send_time DESC"}},
+        {source:loc, dest_recv_time:null,
+            id:{"not in":{
+                select : {'ahtc' : ['id']},
+                from : 'ahtc',
+                where :{source:loc, dest_recv_time:null}
+            }}
+        }
+    ); 
+
+
+    if(!firstrun) nonHoldTransitsToMeGrid.resetStore();
+    if (!hasSome || which.nhttmg) nonHoldTransitsToMeGrid.loadAll(
+        {order_by:{"atc":"source_send_time DESC"}},
+        {dest:loc, dest_recv_time:null,
+            id:{"not in":{
+                select : {'ahtc' : ['id']},
+                from : 'ahtc',
+                where :{dest:loc, dest_recv_time:null}
+            }}
+        }
+    ); 
+
+
+
+
+
+    firstrun = false;
+}
+
+
+function processCopyBarcodeAction (bc, resetPart) {
+    var c = pcrud.search(
+        "circ",
+        {   checkin_time : null,
+            circ_lib : circLocation.getValue(),
+            target_copy : {
+                "in" : {
+                    select: { acp : ['id'] },
+                    from : 'acp',
+                    where: {
+                        deleted : 'f',
+                        circ_lib : { '<>' : circLocation.getValue() },
+                        barcode : bc,
+                        id : { '=' : {'+circ' : 'target_copy'} }
+                    }
+                }
+            }
+        }
+    )[0];
+
+    var stat = {
+        barcode: bc,        // barcode we were passed
+        copy: null,         // item with the barcode we were passed
+        patron: null,       // patron FM object from circ.usr()
+        card: null,         // item with the barcode we were passed
+        circ: null,         // circ FM object
+        transit: null,         // circ FM object
+        action: null,       // Check In, Recall
+    };
+
+    if (c) { // circ found, it's leaving
+        checkinAction(c.id(),resetPart);
+
+    } else { // going out ... maybe
+        c = pcrud.search(
+            "circ",
+            {   checkin_time : null,
+                circ_lib : { '<>' : circLocation.getValue() },
+                target_copy : {
+                    "in" : {
+                        select: { acp : ['id'] },
+                        from : 'acp',
+                        where: {
+                            deleted : 'f',
+                            circ_lib : circLocation.getValue(),
+                            barcode : bc,
+                            id : { '=' : {'+circ' : 'target_copy'} }
+                        }
+                    }
+                }
+            }
+        )[0];
+
+        if (c) { // found a remote circ
+            recallAction(c.id(),{x:false});
+
+        } else { // return transit, perhaps?
+            stat.transit = pcrud.search(
+                "atc",
+                {   target_copy : {   
+                        "in" : {   
+                            select: { acp : ['id'] },
+                            from : 'acp',
+                            where: {
+                                deleted : 'f',
+                                circ_lib : circLocation.getValue(),
+                                barcode : bc,
+                                id : { '=' : {'+atc' : 'target_copy'} }
+                            }
+                        }
+                    },
+                    dest: circLocation.getValue(),
+                    dest_recv_time: null
+                }, {flesh: 2, flesh_fields: {aou:['ill_address'], atc: ['source','dest']}}
+            )[0]; // grab any transit
+
+            if (stat.transit) {
+                if (!cache.acp[stat.transit.target_copy()]) cache.acp[stat.transit.target_copy()] = pcrud.retrieve("acp", stat.transit.target_copy());
+                stat.copy = cache.acp[stat.transit.target_copy()];
+                if (stat.copy) stat.barcode = stat.copy.barcode();
+
+                fieldmapper.standardRequest(
+                    ['open-ils.circ','open-ils.circ.checkin.override'],
+                    [user.authtoken, {noop: 1, force: 1, barcode: bc}]
+                );
+
+                stat.action = 'Incoming';
+                stat.direction = 'Item Received';
+            } else {
+                stat.action = 'None';
+                stat.direction = 'Unknown';
+            }
+
+            return renderScanStatusRow(stat);
+
+        }
+    }
+
+}
+
+function truncate_date (ts) { return ts.substring(0,10) }
+
+var stat_list = [];
+
+function renderScanStatusRow(stat) {
+    stat_list.push(stat);
+    var stat_pos = stat_list.length - 1;
+
+    var row = dojo.clone( dojo.query('.statusTemplate')[0] );
+    row.setAttribute('stat_pos', stat_pos);
+
+    dojo.query('.action', row)[0].innerHTML = stat.action;
+
+    if (stat.card) dojo.query('.patron', row)[0].innerHTML = stat.card.barcode();
+
+    if (stat.circ || stat.transit) {
+        dojo.query('.receipt', row)[0].setAttribute('href','javascript:printCircStatDetail(' + stat_pos + ')');
+    }
+
+    if (stat.copy) {
+         dojo.query('.item', row)[0].innerHTML = formatCopyBarcode(stat.copy);
+    } else {
+         dojo.query('.item', row)[0].innerHTML = stat.barcode;
+    }
+
+    if (stat.transit) {
+        if (stat.transit.dest() == circLocation.getValue()) {
+            dojo.query('.location', row)[0].innerHTML = formatCirclib(
+                fieldmapper.aou.findOrgUnit(stat.transit.source())
+            );
+        } else {
+            dojo.query('.location', row)[0].innerHTML = formatCirclib(
+                fieldmapper.aou.findOrgUnit(stat.transit.dest())
+            );
+        }
+    }
+
+    dojo.byId('statusTarget').appendChild(row);
+}
+
+function printAllCircStatDetail() {
+    for (var i = 0; i < stat_list.length; i++) {
+        printCircStatDetail(i);
+    }
+}
+
+function printCircStatDetail(stat_pos) {
+    var stat = stat_list[stat_pos];
+    var template = fieldmapper.standardRequest(
+        ['open-ils.actor','open-ils.actor.web_action_print_template.fetch'],
+        [circLocation.getValue(), 'circ', stat.action, stat.direction ]
+    );
+
+    var f_list = [ 'copy', 'patron', 'card', 'circ', 'transit' ];
+
+    var stat_copy = { action : stat.action, barcode: stat.barcode, direction: stat.direction };
+    for (var i = 0; i < f_list.length; i++) {
+        if (stat[f_list[i]]) stat_copy[f_list[i]] = stat[f_list[i]].toHash(true, [], true)
+    }
+
+    var w = window.open();
+    w.document.write( dojo.string.substitute( template.template(), stat_copy, function (v) { if (!v) return ''; return v; } ) );
+    w.document.close();
+    w.print();
+    w.close();
+}
+
+function fetchCopy(rowIndex, item) {
+    if (!item) return null;
+
+    var acp_id = this.grid.store.getValue(item, "target_copy");
+    if (!cache.acp[acp_id])
+        cache.acp[acp_id] = pcrud.retrieve("acp", this.grid.store.getValue(item, "target_copy"));
+
+    return cache.acp[acp_id]
+}
+
+function formatBarcode(value) {
+    if (!value) return ""; // XXX
+    return value.barcode();
+}
+
+function fetchSource(rowIndex, item) {
+    if (!item) return null;
+
+    return fieldmapper.aou.findOrgUnit(this.grid.store.getValue(item, "source"));
+}
+
+function fetchDest(rowIndex, item) {
+    if (!item) return null;
+
+    return fieldmapper.aou.findOrgUnit(this.grid.store.getValue(item, "dest"));
+}
+
+function fetchCirclib(rowIndex, item) {
+    if (!item) return null;
+
+    return fieldmapper.aou.findOrgUnit(this.grid.store.getValue(item, "circ_lib"));
+}
+
+function formatCirclib(value) {
+    if (!value) return ""; // XXX
+    return value.name();
+}
+
+function fetchCirc(rowIndex, item) {
+    if (!item) return null;
+    return item;
+}
+
+function fetchTransit(rowIndex, item) {
+    if (!item) return null;
+    return item;
+}
+
+function formatIncomingTransitActions(item) {
+    if (!item) return "";
+
+    var c_id = this.grid.store.getValue(item, "id");
+
+    var link_text = '<span style="white-space:nowrap;">' +
+                    '<a href="javascript:checkinAction(' + c_id + ',{nhttmg:true});">Receive Item</a>' +
+                    '</span>';
+
+    return link_text;
+}
+
+function formatOutgoingTransitActions(item) { return ""; }
+
+function formatLocalActions(item) {
+    if (!item) return ""; // XXX
+    var circ_id = this.grid.store.getValue(item, "id");
+    return '<span style="white-space:nowrap;">' +
+           '<a href="javascript:checkinAction(' + circ_id + ',{tmg:true});">Check In</a>' +
+           '<br/>' +
+           '<a href="javascript:renewAction(' + circ_id + ',{tmg:true});">Renew</a>' +
+           '<br/>' +
+           '<a href="javascript:lostAction(' + circ_id + ',{tmg:true});">Mark Lost</a>' +
+           '<br/>' +
+           '<a href="javascript:CRAction(' + circ_id + ',{tmg:true});">Mark Claims Returned</a>' +
+           '</span>';
+}
+
+function formatRemoteActions(item) {
+    if (!item) return ""; // XXX
+    return '<span style="white-space:nowrap;">' +
+           '<a href="javascript:recallAction(' + this.grid.store.getValue(item, "id") + ',{fmg:true});">Recall</a>' +
+           '</span>';
+}
+
+function formatCopyBarcode(value) {
+    if (!value) return ""; // XXX
+    if (!cache.acn[value.call_number()])
+        cache.acn[value.call_number()] = pcrud.retrieve("acn", value.call_number());
+    var cn = cache.acn[value.call_number()];
+    return '<a target="_blank" href=/opac/skin/bt/xml/rdetail.xml?r=' + cn.record() + '>' + value.barcode() + '</a>';
+}
+
+function checkinAction(circid, resetPart) {
+    if (!cache.circ[circid])
+        cache.circ[circid] = pcrud.retrieve("circ", circid);
+
+    var c = cache.circ[circid];
+    fieldmapper.standardRequest(
+        ['open-ils.circ','open-ils.circ.checkin.override'],
+        [user.authtoken, {force: 1, copy_id: c.target_copy(), patron: c.usr()}]
+    );
+    resetLocation(circLocation.getValue(),resetPart);
+
+    var stat = {
+        barcode: null,        // barcode we were passed
+        copy: null,         // item with the barcode we were passed
+        patron: null,       // patron FM object from circ.usr()
+        card: null,         // item with the barcode we were passed
+        circ: null,         // circ FM object
+        transit: null,         // circ FM object
+        action: 'Checked In',       // Check In, Recall
+    };
+
+    c = pcrud.retrieve("circ", c.id()); // refetch the circ
+    cache.circ[c.id()] = c;
+    stat.circ = c;
+
+    if (!cache.acp[c.target_copy()]) cache.acp[c.target_copy()] = pcrud.retrieve("acp", stat.circ.target_copy()); // fetch the copy
+    stat.copy = cache.acp[c.target_copy()];
+    stat.barcode = stat.copy.barcode();        // barcode we would have been passed
+
+    if (!cache.au[c.usr()]) cache.au[c.usr()] = pcrud.retrieve("au", stat.circ.usr()); // fetch the copy
+    stat.patron = cache.au[c.usr()];
+
+    if (!cache.ac[stat.patron.card()]) cache.ac[stat.patron.card()] = pcrud.retrieve("ac", stat.patron.card()); // fetch the patron card
+    stat.card = cache.ac[stat.patron.card()];
+
+    stat.transit = pcrud.search(
+        "atc",
+        {   target_copy: stat.copy.id(),
+            source: circLocation.getValue(),
+            dest_recv_time: null
+        }, {flesh: 2, flesh_fields: {aou:['ill_address'], atc: ['source','dest']}}
+    )[0]; // grab any transit
+
+    if (stat.transit) cache.atc[stat.transit.id()] = stat.transit;
+
+    return renderScanStatusRow(stat);
+}
+
+function renewAction(circid,resetPart) {
+    if (!resetPart) resetPart = {tmg:true};
+    if (!cache.circ[circid])
+        cache.circ[circid] = pcrud.retrieve("circ", circid);
+
+    var c = cache.circ[circid];
+    fieldmapper.standardRequest(
+        ['open-ils.circ','open-ils.circ.renew.override'],
+        [user.authtoken, {copy_id: c.target_copy(), patron: c.usr()}]
+    );
+    resetLocation(circLocation.getValue(),resetPart);
+
+    var stat = {
+        barcode: null,        // barcode we were passed
+        copy: null,         // item with the barcode we were passed
+        patron: null,       // patron FM object from circ.usr()
+        card: null,         // item with the barcode we were passed
+        circ: null,         // circ FM object
+        transit: null,         // circ FM object
+        action: 'Item Renewed',       // Check In, Recall
+    };
+
+    var new_c = pcrud.search(       // Fetch the new circ
+        "circ",
+        {   checkin_time : null,
+            circ_lib : circLocation.getValue(),
+            target_copy : c.target_copy()
+        }
+    )[0];
+
+    if (!new_c) {
+        stat.action = 'Item Renewal Failed';
+    } else if (c.id() != new_c.id()) {
+        c = new_c;
+        cache.circ[c.id()] = c;
+        stat.circ = c;
+    
+    } else if (c.id() == new_c.id()) {
+        c = pcrud.retrieve("circ", c.id()); // refetch the circ
+        cache.circ[c.id()] = c;
+        stat.circ = c;
+        stat.action = 'Item Renewal Failed';
+    }
+
+    if (!cache.acp[c.target_copy()]) cache.acp[c.target_copy()] = pcrud.retrieve("acp", stat.circ.target_copy()); // fetch the copy
+    stat.copy = cache.acp[c.target_copy()];
+    stat.barcode = stat.copy.barcode();        // barcode we would have been passed
+
+    if (!cache.au[c.usr()]) cache.au[c.usr()] = pcrud.retrieve("au", stat.circ.usr()); // fetch the copy
+    stat.patron = cache.au[c.usr()];
+
+    if (!cache.ac[stat.patron.card()]) cache.ac[stat.patron.card()] = pcrud.retrieve("ac", stat.patron.card()); // fetch the patron card
+    stat.card = cache.ac[stat.patron.card()];
+
+    return renderScanStatusRow(stat);
+}
+
+function lostAction(circid,resetPart) {
+    if (!resetPart) resetPart = {tmg:true};
+    if (!cache.circ[circid])
+        cache.circ[circid] = pcrud.retrieve("circ", circid);
+
+    var c = cache.circ[circid];
+
+    if (!cache.acp[c.target_copy()])
+        cache.acp[c.target_copy()] = pcrud.retrieve("acp", c.target_copy());
+
+    var cp = cache.acp[c.target_copy()];
+
+    fieldmapper.standardRequest(
+        ['open-ils.circ','open-ils.circ.circulation.set_lost'],
+        [user.authtoken, {barcode: cp.barcode()}]
+    );
+    resetLocation(circLocation.getValue(),resetPart);
+
+    var stat = {
+        barcode: null,        // barcode we were passed
+        copy: null,         // item with the barcode we were passed
+        patron: null,       // patron FM object from circ.usr()
+        card: null,         // item with the barcode we were passed
+        circ: null,         // circ FM object
+        transit: null,         // circ FM object
+        action: 'Item Marked Lost',       // Check In, Recall
+    };
+
+    c = pcrud.retrieve("circ", c.id()); // refetch the circ
+    cache.circ[c.id()] = c;
+    stat.circ = c;
+
+    if (!cache.acp[c.target_copy()]) cache.acp[c.target_copy()] = pcrud.retrieve("acp", stat.circ.target_copy()); // fetch the copy
+    stat.copy = cache.acp[c.target_copy()];
+    stat.barcode = stat.copy.barcode();        // barcode we would have been passed
+
+    if (!cache.au[c.usr()]) cache.au[c.usr()] = pcrud.retrieve("au", stat.circ.usr()); // fetch the copy
+    stat.patron = cache.au[c.usr()];
+
+    if (!cache.ac[stat.patron.card()]) cache.ac[stat.patron.card()] = pcrud.retrieve("ac", stat.patron.card()); // fetch the patron card
+    stat.card = cache.ac[stat.patron.card()];
+
+    return renderScanStatusRow(stat);
+}
+
+function CRAction(circid,resetPart) {
+    if (!resetPart) resetPart = {tmg:true};
+    if (!cache.circ[circid])
+        cache.circ[circid] = pcrud.retrieve("circ", circid);
+
+    var c = cache.circ[circid];
+
+    if (!cache.acp[c.target_copy()])
+        cache.acp[c.target_copy()] = pcrud.retrieve("acp", c.target_copy());
+
+    var cp = cache.acp[c.target_copy()];
+
+    fieldmapper.standardRequest(
+        ['open-ils.circ','open-ils.circ.circulation.set_claims_returned.override'],
+        [user.authtoken, {barcode: cp.barcode(), use_due_date: 1}]
+    );
+    resetLocation(circLocation.getValue(),resetPart);
+
+    var stat = {
+        barcode: null,        // barcode we were passed
+        copy: null,         // item with the barcode we were passed
+        patron: null,       // patron FM object from circ.usr()
+        card: null,         // item with the barcode we were passed
+        circ: null,         // circ FM object
+        transit: null,         // circ FM object
+        action: 'Item Marked Claims-Returned',       // Check In, Recall
+    };
+
+    c = pcrud.retrieve("circ", c.id()); // refetch the circ
+    cache.circ[c.id()] = c;
+    stat.circ = c;
+
+    if (!cache.acp[c.target_copy()]) cache.acp[c.target_copy()] = pcrud.retrieve("acp", stat.circ.target_copy()); // fetch the copy
+    stat.copy = cache.acp[c.target_copy()];
+    stat.barcode = stat.copy.barcode();        // barcode we would have been passed
+
+    if (!cache.au[c.usr()]) cache.au[c.usr()] = pcrud.retrieve("au", stat.circ.usr()); // fetch the copy
+    stat.patron = cache.au[c.usr()];
+
+    if (!cache.ac[stat.patron.card()]) cache.ac[stat.patron.card()] = pcrud.retrieve("ac", stat.patron.card()); // fetch the patron card
+    stat.card = cache.ac[stat.patron.card()];
+
+    return renderScanStatusRow(stat);
+}
+
+function recallAction(circid,resetPart) {
+    if (!resetPart) resetPart = {fmg:true};
+    alert('recalling ' + circid);
+    resetLocation(circLocation.getValue(),resetPart);
+
+    var stat = {
+        barcode: null,        // barcode we were passed
+        copy: null,         // item with the barcode we were passed
+        card: null,         // item with the barcode we were passed
+        patron: null,       // patron FM object from circ.usr()
+        circ: null,         // circ FM object
+        transit: null,         // circ FM object
+        action: 'Item Recalled',       // Check In, Recall
+    };
+
+    c = pcrud.retrieve("circ", c.id()); // refetch the circ
+    cache.circ[c.id()] = c;
+    stat.circ = c;
+
+    if (!cache.acp[c.target_copy()]) cache.acp[c.target_copy()] = pcrud.retrieve("acp", stat.circ.target_copy()); // fetch the copy
+    stat.copy = cache.acp[c.target_copy()];
+    stat.barcode = stat.copy.barcode();        // barcode we would have been passed
+
+    if (!cache.au[c.usr()]) cache.au[c.usr()] = pcrud.retrieve("au", stat.circ.usr()); // fetch the copy
+    stat.patron = cache.au[c.usr()];
+
+    if (!cache.ac[stat.patron.card()]) cache.ac[stat.patron.card()] = pcrud.retrieve("ac", stat.patron.card()); // fetch the patron card
+    stat.card = cache.ac[stat.patron.card()];
+
+    stat.transit = pcrud.search(
+        "atc",
+        {   target_copy: stat.copy.id(),
+            source: circLocation.getValue(),
+            dest_recv_time: null
+        }, {flesh: 2, flesh_fields: {aou:['ill_address'], atc: ['source','dest']}}
+    )[0]; // grab any transit
+
+    if (stat.transit) cache.atc[stat.transit.id()] = stat.transit;
+
+    return renderScanStatusRow(stat);
+}
+
+function fetchCard(rowIndex, item) {
+    if (!item) return null;
+
+    var auid = this.grid.store.getValue(item, "usr");
+    if (!cache.au[auid])
+        cache.au[auid] = pcrud.retrieve("au", auid);
+
+    var acid = cache.au[auid].card();
+    if (!cache.ac[acid])
+        cache.ac[acid] = pcrud.retrieve("ac", acid);
+
+    return cache.ac[acid]
+}
+
+dojo.addOnLoad(function(){
+    user.buildPermOrgSelector(
+        'STAFF_LOGIN',
+        circLocation,
+        cgi.param('l') || user.user.home_ou()
+    );
+});
+
+
diff --git a/Open-ILS/web/staff/js/hold_tracker.js b/Open-ILS/web/staff/js/hold_tracker.js
new file mode 100644 (file)
index 0000000..247cc0b
--- /dev/null
@@ -0,0 +1,776 @@
+dojo.require("fieldmapper.AutoIDL");
+dojo.require("dijit.layout.ContentPane");
+dojo.require("dijit.layout.TabContainer");
+dojo.require("dijit.form.TextBox");
+dojo.require("openils.widget.AutoGrid");
+dojo.require("openils.User");
+dojo.require("openils.PermaCrud");
+dojo.require("openils.widget.OrgUnitFilteringSelect");
+
+function objSize (x) {
+    var size = 0, key;
+    for (key in x) {
+        if (x.hasOwnProperty(key)) size++;
+    }
+    return size;
+}
+
+var user = new openils.User ({authcookie:'ses'});
+openils.User.authtoken = user.authtoken;
+
+var pcrud = new openils.PermaCrud ({authtoken:user.authtoken});
+var cgi = new CGI ();
+
+var cache = { acp : {}, acn : {}, ahr : {}, au : {}, ac : {}, ahtc_by_hold : {}, atc : {}, circ : {} };
+var firstrun = true;
+
+function resetLocation(loc, which) {
+
+    if (!dojo.isObject(which)) which = {};
+
+    var hasSome = objSize(which) > 0;
+
+    if (!firstrun) toMeGrid.resetStore();
+    if (!hasSome || which.tmg) toMeGrid.loadAll(
+        {order_by:{"ahr": "request_time DESC"}},
+        {request_lib : loc, fulfillment_time : null, cancel_time : null}
+    );
+
+
+    if (!firstrun) fromMeGrid.resetStore();
+    if (!hasSome || which.fmg) fromMeGrid.loadAll(
+        {order_by:{"ahr": "request_time DESC"}},
+        {   fulfillment_time : null,
+            cancel_time : null,
+            current_copy : {
+                "in" : {
+                    select: { acp : ['id'] },
+                    from : 'acp',
+                    where: { circ_lib : loc, id : { '=' : {'+ahr' : 'current_copy'} } }
+                }
+            }
+        }
+    );
+
+
+
+    if (!firstrun) holdTransitFromMeGrid.resetStore();
+    if (!hasSome || which.htfmg) holdTransitFromMeGrid.loadAll(
+        {order_by:{"ahtc":"source_send_time DESC"}},
+        {source:loc, dest_recv_time:null}
+     );
+
+
+    if(!firstrun) holdTransitToMeGrid.resetStore();
+    if (!hasSome || which.httmg) holdTransitToMeGrid.loadAll(
+        {order_by:{"ahtc":"source_send_time DESC"}},
+        {dest:loc, dest_recv_time:null}
+    );
+
+    firstrun = false;
+}
+
+function processCopyBarcodeAction (bc) {
+    console.log("processing barcode " + bc);
+
+    var h = pcrud.search(
+        "ahr",
+        {   fulfillment_time : null,
+            cancel_time : null,
+            frozen : 'f',
+            request_lib : holdLocation.getValue(),
+            current_copy : {
+                "in" : {
+                    select: { acp : ['id'] },
+                    from : 'acp',
+                    where: {
+                        deleted : 'f',
+                        circ_lib : { '<>' : holdLocation.getValue() },
+                        barcode : bc,
+                        id : { '=' : {'+ahr' : 'current_copy'} }
+                    }
+                }
+            }
+        }
+    )[0];
+
+    var stat = {
+        barcode: bc,        // barcode we were passed
+        copy: null,         // item with the barcode we were passed
+        hold: h,         // hold FM object
+        patron: null,       // patron FM object
+        card: null,         // card FM object
+        transit: null,      // transit FM object
+        action: null,       // Receive, Check Out, Capture 
+        direction: null,    // Incoming, Outgoing
+    };
+
+    if (h) { // hold coming here
+        stat.direction = 'Incoming';
+
+        if (h.shelf_time()) { // captured, ready for circ
+            stat.action = 'Check Out';
+            checkoutAction(h.id(),stat, null, 'ill-foreign-checkout');
+        } else { // receiving, check it in to capture
+            stat.action = 'Check In';
+            checkinAction(h.id(),stat, null, 'ill-foreign-receive');
+        }
+
+    } else { // going out ... maybe
+        h = pcrud.search(
+            "ahr",
+            {   fulfillment_time : null,
+                cancel_time : null,
+                frozen : 'f',
+                request_lib : { '<>' : holdLocation.getValue() },
+                current_copy : {
+                    in : {
+                        select: { acp : ['id'] },
+                        from : 'acp',
+                        where: {
+                            deleted : 'f',
+                            circ_lib : holdLocation.getValue(),
+                            barcode : bc,
+                            id : { '=' : {'+ahr' : 'current_copy'} }
+                        }
+                    }
+                }
+            }
+        )[0];
+
+        if (h) { // yep ... going out
+            stat.direction = 'Outgoing';
+
+            if (!h.capture_time()) { // time to capture it
+                stat.action = 'Capture';
+                checkinAction(h.id(),stat,null,'ill-home-capture');
+            } else { // else, already captured, just grab the transit
+                stat.hold = h;
+                cache.ahr[stat.hold.id()] = stat.hold;
+        
+                if (!cache.acp[stat.hold.current_copy()]) cache.acp[stat.hold.current_copy()] = pcrud.retrieve("acp", stat.hold.current_copy());
+                stat.copy = cache.acp[stat.hold.current_copy()];
+                if (stat.copy) stat.barcode = stat.copy.barcode();
+        
+                if (!cache.au[stat.hold.usr()]) cache.au[stat.hold.usr()] = pcrud.retrieve("au", stat.hold.usr());
+                stat.patron = cache.au[stat.hold.usr()];
+        
+                if (!cache.ac[stat.patron.card()]) cache.ac[stat.patron.card()] = pcrud.retrieve("ac", stat.patron.card());
+                stat.card = cache.ac[stat.patron.card()];
+        
+                // Grab the transit again, with stuff fleshed for printing
+                stat.transit = pcrud.search("ahtc", { hold: stat.hold.id() }, {flesh: 2, flesh_fields: {aou:['ill_address'], ahtc: ['source','dest']}})[0];
+    
+                return renderScanStatusRow(stat);
+            }
+
+   
+        } else { // return transit, perhaps?
+            stat.transit = pcrud.search(
+                "atc",
+                {   target_copy : {
+                        "in" : {   
+                            select: { acp : ['id'] },
+                            from : 'acp',
+                            where: {
+                                deleted : 'f',
+                                circ_lib : holdLocation.getValue(),
+                                barcode : bc,
+                                id : { '=' : {'+atc' : 'target_copy'} }
+                            }
+                        }
+                    },
+                    dest: holdLocation.getValue(),
+                    dest_recv_time: null
+                }, {flesh: 2, flesh_fields: {aou:['ill_address'], atc: ['source','dest']}}
+            )[0]; // grab any transit
+
+            if (stat.transit) { // coming home
+                if (!cache.acp[stat.transit.target_copy()]) 
+                    cache.acp[stat.transit.target_copy()] = 
+                        pcrud.retrieve("acp", stat.transit.target_copy());
+
+                stat.copy = cache.acp[stat.transit.target_copy()];
+                if (stat.copy) stat.barcode = stat.copy.barcode();
+
+                fieldmapper.standardRequest(
+                    ['open-ils.circ','open-ils.circ.checkin.override'],
+                    [user.authtoken, {noop: 1, force: 1, barcode: bc, ff_action: 'transit-home-receive'}]
+                );
+
+                stat.direction = 'Incoming';
+                stat.action = 'Check In';
+            } else { // maybe it's checked out here
+                var c = pcrud.search(
+                    "circ",
+                    {   checkin_time : null,
+                        circ_lib : holdLocation.getValue(),
+                        target_copy : {
+                            "in" : {
+                                select: { acp : ['id'] },
+                                from : 'acp',
+                                where: {
+                                    deleted : 'f',
+                                    circ_lib : { '<>' : holdLocation.getValue() },
+                                    barcode : bc,
+                                    id : { '=' : {'+circ' : 'target_copy'} }
+                                }
+                            }
+                        }
+                    }
+                )[0];
+            
+                if (c) {
+                    stat.action = 'Check In';
+                    stat.direction = 'Outgoing';
+                    return checkinILLAction(c, stat);
+
+                } else {
+
+                    stat.direction = 'Unknown';
+
+                    // maybe it's already checked in here and transiting home
+                    // TODO
+                    // all the commented-out stuff below needs re-thinking
+                    // with borrowing tmp copy barcode in mind
+
+                    /*
+                    stat.transit = pcrud.search(
+                        "atc",
+                        {   target_copy : {
+                                "in" : {   
+                                    select: { acp : ['id'] },
+                                    from : 'acp',
+                                    where: {
+                                        deleted : 'f',
+                                        circ_lib : holdLocation.getValue(),
+                                        barcode : bc,
+                                        id : { '=' : {'+atc' : 'target_copy'} }
+                                    }
+                                }
+                            },
+                            dest: holdLocation.getValue(),
+                            dest_recv_time: null
+                        }, {flesh: 2, flesh_fields: {aou:['ill_address'], atc: ['source','dest']}}
+                    )[0]; // grab any transit
+
+
+                    // this copy means nothing to us, but show it in the UI so the user
+                    // has some conformation it was entered.
+
+
+
+                    // find the real copy if we can
+                    for (var id in cache) {
+                        if (cache[id].barcode() == stat.barcode) {
+                            stat.copy = cache[id];
+                        }
+                    }
+                    if (!stat.copy) {
+                        stat.copy = pcrud.search(
+                            'acp', {
+                                barcode : stat.barcode, 
+                                deleted : 'f'
+                            }
+                        )[0];
+                        if (stat.copy) 
+                            cache.acp[stat.copy.id()] = stat.copy;
+                    }
+
+                    if (stat.copy) {
+                        // find the last transit this copy took part in
+                        // to provide some context to the user.
+                        stat.transit = pcrud.search("atc", 
+                            {target_copy : stat.copy.id()},
+                            {   flesh: 2, 
+                                flesh_fields: {aou:['ill_address'], atc: ['source','dest']},
+                                order_by : {atc : "source_send_time DESC"},
+                                limit : 1
+                            }
+                        )[0]; 
+
+                        if (stat.transit && !stat.transit.dest_recv_time &&
+                                stat.dest.id() != holdLocation.getValue() ) {
+
+                            // copy has already been checked in at the 
+                            // borrowing library and wants to get back home.
+                            stat.direction = 'Outgoing';
+
+                            // report on the most recent circulation
+                            stat.circ = pcrud.search(
+                                "circ",
+                                {   circ_lib : holdLocation.getValue(),
+                                    target_copy : 
+                                        "in" : {
+                                            select: { acp : ['id'] },
+                                            from : 'acp',
+                                            where: {
+                                                deleted : 'f',
+                                                circ_lib : { '<>' : holdLocation.getValue() },
+                                                barcode : bc,
+                                                id : { '=' : {'+circ' : 'target_copy'} }
+                                            }
+                                        }
+                                    }
+                                }
+                            )[0];
+
+                        }
+
+                    }
+                    */
+
+                }
+            }
+
+            return renderScanStatusRow(stat);
+        }
+    }
+}
+
+function checkinILLAction(c, stat) {
+    fieldmapper.standardRequest(
+        ['open-ils.circ','open-ils.circ.checkin.override'],
+        [user.authtoken, {force: 1, copy_id: c.target_copy(), patron: c.usr(), ff_action: 'ill-foreign-checkin'}]
+    );
+
+    c = pcrud.retrieve("circ", c.id()); // refetch the circ
+    cache.circ[c.id()] = c;
+    stat.circ = c;
+
+    if (!cache.acp[c.target_copy()]) cache.acp[c.target_copy()] = pcrud.retrieve("acp", stat.circ.target_copy()); // fetch the copy
+    stat.copy = cache.acp[c.target_copy()];
+    stat.barcode = stat.copy.barcode();        // barcode we would have been passed
+
+    if (!cache.au[c.usr()]) cache.au[c.usr()] = pcrud.retrieve("au", stat.circ.usr()); // fetch the copy
+    stat.patron = cache.au[c.usr()];
+
+    if (!cache.ac[stat.patron.card()]) cache.ac[stat.patron.card()] = pcrud.retrieve("ac", stat.patron.card()); // fetch the patron card
+    stat.card = cache.ac[stat.patron.card()];
+
+    stat.transit = pcrud.search(
+        "atc",
+        {   target_copy: stat.copy.id(),
+            source: holdLocation.getValue(),
+            dest_recv_time: null
+        }, {flesh: 2, flesh_fields: {aou:['ill_address'], atc: ['source','dest']}}
+    )[0]; // grab any transit
+
+    if (stat.transit) cache.atc[stat.transit.id()] = stat.transit;
+
+    return renderScanStatusRow(stat);
+}
+
+function truncate_date (ts) { return ts.substring(0,10) }
+
+var stat_list = [];
+
+function renderScanStatusRow(stat) {
+    stat_list.push(stat);
+    var stat_pos = stat_list.length - 1;
+
+    var row = dojo.clone( dojo.query('.statusTemplate')[0] );
+    row.setAttribute('stat_pos', stat_pos);
+
+    dojo.query('.action', row)[0].innerHTML = stat.action; // TODO i18n
+    dojo.query('.direction', row)[0].innerHTML = stat.direction; // TODO i18n
+
+    if (stat.card) dojo.query('.patron', row)[0].innerHTML = stat.card.barcode();
+
+    if (stat.circ) {
+        dojo.query('.receipt', row)[0].setAttribute('href','javascript:printCircStatDetail(' + stat_pos + ')');
+    } else if (stat.hold || stat.transit) {
+        dojo.query('.receipt', row)[0].setAttribute('href','javascript:printHoldStatDetail(' + stat_pos + ')');
+    }
+
+    if (stat.copy) {
+         dojo.query('.item', row)[0].innerHTML = formatCopyBarcode(stat.copy);
+    } else {
+         dojo.query('.item', row)[0].innerHTML = stat.barcode;
+    }
+
+    if (stat.transit) {
+        if (stat.transit.dest().id() == holdLocation.getValue()) {
+            dojo.query('.location', row)[0].innerHTML = formatCirclib( stat.transit.source() );
+        } else {
+            dojo.query('.location', row)[0].innerHTML = formatCirclib( stat.transit.dest() );
+        }
+    }
+
+    dojo.byId('statusTarget').appendChild(row);
+}
+
+
+function printAllHoldStatDetail() {
+    for (var i = 0; i < stat_list.length; i++) {
+        console.log('stat list: ' + js2JSON(stat_list));
+        if (stat_list[i].circ) printCircStatDetail(i);
+        else printHoldStatDetail(i);
+    }
+}
+
+function printHoldStatDetail(stat_pos) {
+    var stat = stat_list[stat_pos];
+    var template = fieldmapper.standardRequest(
+        ['open-ils.actor','open-ils.actor.web_action_print_template.fetch'],
+        [holdLocation.getValue(), 'hold', stat.action, stat.direction ]
+    );
+
+    var f_list = [ 'copy', 'patron', 'card', 'hold', 'transit' ];
+
+    var stat_copy = { action : stat.action, barcode: stat.barcode, direction: stat.direction };
+    for (var i = 0; i < f_list.length; i++) {
+        if (stat[f_list[i]]) stat_copy[f_list[i]] = stat[f_list[i]].toHash(true, [], true)
+    }
+
+    var w = window.open();
+    w.document.write( dojo.string.substitute( template.template(), stat_copy, function (v) { if (!v) return ''; return v; } ) );
+    w.document.close();
+    w.print();
+    w.close();
+}
+
+function printCircStatDetail(stat_pos) {
+    var stat = stat_list[stat_pos];
+    var template = fieldmapper.standardRequest(
+        ['open-ils.actor','open-ils.actor.web_action_print_template.fetch'],
+        [holdLocation.getValue(), 'circ', stat.action, stat.direction ]
+    );
+
+    var f_list = [ 'copy', 'patron', 'card', 'circ', 'transit' ];
+
+    var stat_copy = { action : stat.action, barcode: stat.barcode, direction: stat.direction };
+    for (var i = 0; i < f_list.length; i++) {
+        if (stat[f_list[i]]) stat_copy[f_list[i]] = stat[f_list[i]].toHash(true, [], true)
+    }
+
+    var w = window.open();
+    w.document.write( dojo.string.substitute( template.template(), stat_copy, function (v) { if (!v) return ''; return v; } ) );
+    w.document.close();
+    w.print();
+    w.close();
+}
+
+function fetchCopy(rowIndex, item) {
+    if (!item) return null;
+
+    var acp_id = this.grid.store.getValue(item, "current_copy");
+    if (!cache.acp[acp_id]) cache.acp[acp_id] =  pcrud.retrieve("acp", this.grid.store.getValue(item, "current_copy"));
+    return cache.acp[acp_id];
+}
+
+function fetchTargetCopy(rowIndex, item) {
+    if (!item) return null;
+
+    var acp_id = this.grid.store.getValue(item, "target_copy");
+    if (!cache.acp[acp_id]) cache.acp[acp_id] =  pcrud.retrieve("acp", this.grid.store.getValue(item, "target_copy"));
+    return cache.acp[acp_id];
+}
+
+function formatBarcode(value) {
+    if (!value) return ""; // XXX
+    return value.barcode();
+}
+
+function fetchSource(rowIndex, item) {
+    if (!item) return null;
+
+    return fieldmapper.aou.findOrgUnit(this.grid.store.getValue(item, "source"));
+}
+
+function fetchDest(rowIndex, item) {
+    if (!item) return null;
+
+    return fieldmapper.aou.findOrgUnit(this.grid.store.getValue(item, "dest"));
+}
+
+function fetchCirclib(rowIndex, item) {
+    if (!item) return null;
+
+    return fieldmapper.aou.findOrgUnit(this.grid.store.getValue(item, "pickup_lib"));
+}
+
+function formatCirclib(value) {
+    if (!value) return ""; // XXX
+    return value.name();
+}
+
+function fetchCard(rowIndex, item) {
+    if (!item) return null;
+
+    var u_id = this.grid.store.getValue(item, "usr");
+    if (!cache.au[u_id]) cache.au[u_id] = pcrud.retrieve("au", u_id);
+
+    if (!cache.ac[cache.au[u_id].card()]) cache.ac[cache.au[u_id].card()] = pcrud.retrieve("ac", cache.au[u_id].card());
+
+    return cache.ac[cache.au[u_id].card()];
+}
+
+function fetchHold(rowIndex, item) {
+    if (!item) return null;
+    return item;
+}
+
+function fetchTransit(rowIndex, item) {
+    if (!item) return null;
+    return item;
+}
+
+function formatLocalActions(item) {
+    if (!item) return ""; // XXX
+
+    var h_id = this.grid.store.getValue(item, "id");
+
+    var f_text = '<a href="javascript:freezeAction(' + h_id + ',\'t\',{tmg:true});">Freeze Request</a><br/>';
+    if (this.grid.store.getValue(item, "frozen") == 't')
+        f_text = '<a href="javascript:freezeAction(' + h_id + ',\'f\',{tmg:true});">Thaw Request</a><br/>';
+
+    var cc_text = '<a href="javascript:rejectAction(' + h_id + ',{tmg:true});">Target Request</a><br/>';
+    if (this.grid.store.getValue(item, "current_copy")) cc_text = '';
+
+    var cap_text = '<a href="javascript:checkinAction(' + h_id + ',{direction:\'Incoming\',action:\'Check In\'},{tmg:true},\'ill-foreign-receive\');">Receive Item</a><br/>';
+    if (this.grid.store.getValue(item, "shelf_time")) cap_text = '<a href="javascript:checkoutAction(' + h_id + ',{direction:\'Incoming\',action:\'Check Out\'},{tmg:true},\'ill-foreign-checkout\');">Circulate Item</a><br/>';
+
+    var link_text = '<span style="white-space:nowrap;">' +
+                    cap_text + f_text + cc_text +
+                    '<a href="javascript:cancelAction(' + h_id + ',{tmg:true});">Cancel Request</a>' +
+                    '</span>';
+    return link_text;
+}
+
+function formatRemoteActions(item) {
+    if (!item) return ""; // XXX
+
+    var h_id = this.grid.store.getValue(item, "id");
+
+    var cap_text = '<a href="javascript:checkinAction(' + h_id + ',{direction:\'Outgoing\',action:\'Capture\'},{fmg:true},\'ill-home-capture\');">Capture Item</a><br/>';
+    if (this.grid.store.getValue(item, "capture_time")) cap_text = '';
+
+    var link_text = '<span style="white-space:nowrap;">' +
+                    cap_text +
+                    '<a href="javascript:rejectAction(' + h_id + ',{fmg:true});">Reject Request</a>' +
+                    '</span>';
+    return link_text;
+}
+
+function formatIncomingTransitActions(item) {
+    if (!item) return ""; // XXX
+
+    var h_id = this.grid.store.getValue(item, "hold");
+
+    var link_text = '<span style="white-space:nowrap;">' +
+                    '<a href="javascript:checkinAction(' + h_id + ',{direction:\'Incoming\',action:\'Check In\'},{httmg:true},\'transit-home-receive\');">Receive Item</a><br/>' +
+                    '<a href="javascript:cancelAction(' + h_id + ',{httmg:true});">Cancel Request</a>' +
+                    '</span>';
+    return link_text;
+}
+
+function formatOutgoingTransitActions(item) {
+    if (!item) return ""; // XXX
+
+    var h_id = this.grid.store.getValue(item, "hold");
+
+    var link_text = '<span style="white-space:nowrap;">' +
+                    '<a href="javascript:cancelAction(' + h_id + ',{htfmg:true});">Cancel Request</a>' +
+                    '</span>';
+    return link_text;
+}
+
+function formatCopyBarcode(value) {
+    if (!value) return ""; // XXX
+    if (!cache.acn[value.call_number()]) cache.acn[value.call_number()] = pcrud.retrieve("acn", value.call_number());
+    return '<a target="_blank" href=/opac/skin/bt/xml/rdetail.xml?r=' + cache.acn[value.call_number()].record() + '>' + value.barcode() + '</a>';
+}
+
+function checkinAction(holdid, stat, resetPart, ff_a) {
+    if (!resetPart) resetPart = {tmg : true};
+    var hold = pcrud.retrieve("ahr", holdid);
+    fieldmapper.standardRequest(
+        ['open-ils.circ','open-ils.circ.checkin.override'],
+        [user.authtoken, {force: 1, copy_id: hold.current_copy(), patron: hold.usr(), ff_action: ff_a}]
+    );
+    resetLocation(holdLocation.getValue(), resetPart);
+
+    stat.hold = pcrud.retrieve("ahr", holdid); // refetch the hold
+    cache.ahr[stat.hold.id()] = stat.hold;
+
+    if (!cache.acp[stat.hold.current_copy()]) cache.acp[stat.hold.current_copy()] = pcrud.retrieve("acp", stat.hold.current_copy());
+    stat.copy = cache.acp[stat.hold.current_copy()];
+
+    if (!cache.au[stat.hold.usr()]) cache.au[stat.hold.usr()] = pcrud.retrieve("au", stat.hold.usr());
+    stat.patron = cache.au[stat.hold.usr()];
+
+    if (!cache.ac[stat.patron.card()]) cache.ac[stat.patron.card()] = pcrud.retrieve("ac", stat.patron.card());
+    stat.card = cache.ac[stat.patron.card()];
+
+    // Grab the transit again, with stuff fleshed for printing
+    stat.transit = pcrud.search("ahtc", { hold: stat.hold.id() }, {flesh: 2, flesh_fields: {aou:['ill_address'], ahtc: ['source','dest']}})[0];
+
+    return renderScanStatusRow(stat);
+
+}
+
+function checkoutAction(holdid, stat, resetPart, ff_a) {
+    if (!resetPart) resetPart = {tmg : true};
+    var hold = pcrud.retrieve("ahr", holdid);
+    fieldmapper.standardRequest(
+        ['open-ils.circ','open-ils.circ.checkout.full'],
+        [user.authtoken, {copy_id: hold.current_copy(), patron: hold.usr(), ff_action: ff_a}]
+    );
+    resetLocation(holdLocation.getValue(), resetPart);
+
+    stat.hold = pcrud.retrieve("ahr", holdid); // refetch the hold
+    cache.ahr[stat.hold.id()] = stat.hold;
+
+    var c = pcrud.search(
+        "circ",
+        {   checkin_time : null,
+            circ_lib : holdLocation.getValue(),
+            target_copy : hold.current_copy()
+        }
+    )[0];
+
+    cache.circ[c.id()] = c;
+    stat.circ = cache.circ[c.id()];
+    if (!cache.acp[stat.hold.current_copy()]) cache.acp[stat.hold.current_copy()] = pcrud.retrieve("acp", stat.hold.current_copy());
+    stat.copy = cache.acp[stat.hold.current_copy()];
+
+    if (!cache.au[stat.hold.usr()]) cache.au[stat.hold.usr()] = pcrud.retrieve("au", stat.hold.usr());
+    stat.patron = cache.au[stat.hold.usr()];
+
+    if (!cache.ac[stat.patron.card()]) cache.ac[stat.patron.card()] = pcrud.retrieve("ac", stat.patron.card());
+    stat.card = cache.ac[stat.patron.card()];
+
+    // Grab the transit again, with stuff fleshed for printing
+    stat.transit = pcrud.search("ahtc", { hold: stat.hold.id() }, {flesh: 2, flesh_fields: {aou:['ill_address'], ahtc: ['source','dest']}})[0];
+
+    return renderScanStatusRow(stat);
+
+}
+
+function cancelAction(holdid, resetPart) {
+    if (!resetPart) resetPart = {tmg : true};
+    fieldmapper.standardRequest(
+        ['open-ils.circ','open-ils.circ.hold.cancel'],
+        [user.authtoken, holdid]
+    );
+    resetLocation(holdLocation.getValue(), resetPart);
+
+    var stat = {
+        barcode: '',        // barcode we were passed
+        copy: null,         // item with the barcode we were passed
+        hold: null,         // hold FM object
+        patron: null,       // patron FM object
+        card: null,         // card FM object
+        transit: null,      // transit FM object
+        action: 'Cancel',       // Receive, Check Out, Capture 
+        direction: 'Incoming',    // Incoming, Outgoing
+    };
+
+    stat.hold = pcrud.retrieve("ahr", holdid); // refetch the hold
+    cache.ahr[stat.hold.id()] = stat.hold;
+
+    if (!cache.acp[stat.hold.current_copy()]) cache.acp[stat.hold.current_copy()] = pcrud.retrieve("acp", stat.hold.current_copy());
+    stat.copy = cache.acp[stat.hold.current_copy()];
+    if (stat.copy) stat.barcode = stat.copy.barcode();
+
+    if (!cache.au[stat.hold.usr()]) cache.au[stat.hold.usr()] = pcrud.retrieve("au", stat.hold.usr());
+    stat.patron = cache.au[stat.hold.usr()];
+
+    if (!cache.ac[stat.patron.card()]) cache.ac[stat.patron.card()] = pcrud.retrieve("ac", stat.patron.card());
+    stat.card = cache.ac[stat.patron.card()];
+
+    // Grab the transit again, with stuff fleshed for printing
+    stat.transit = pcrud.search("ahtc", { hold: stat.hold.id() }, {flesh: 2, flesh_fields: {aou:['ill_address'], ahtc: ['source','dest']}})[0];
+
+    return renderScanStatusRow(stat);
+
+}
+
+function freezeAction(holdid, setting, resetPart) {
+    if (!resetPart) resetPart = {tmg : true};
+    fieldmapper.standardRequest(
+        ['open-ils.circ','open-ils.circ.hold.update'],
+        [user.authtoken, null, { id : holdid, frozen : setting }]
+    );
+    resetLocation(holdLocation.getValue(), resetPart);
+
+    var stat = {
+        barcode: '',        // barcode we were passed
+        copy: null,         // item with the barcode we were passed
+        hold: null,         // hold FM object
+        patron: null,       // patron FM object
+        card: null,         // card FM object
+        transit: null,      // transit FM object
+        action: setting == 't' ? 'Freeze' : 'Thaw',       // Receive, Check Out, Capture 
+        direction: 'Incoming',    // Incoming, Outgoing
+    };
+
+    stat.hold = pcrud.retrieve("ahr", holdid); // refetch the hold
+    cache.ahr[stat.hold.id()] = stat.hold;
+
+    if (!cache.acp[stat.hold.current_copy()]) cache.acp[stat.hold.current_copy()] = pcrud.retrieve("acp", stat.hold.current_copy());
+    stat.copy = cache.acp[stat.hold.current_copy()];
+    if (stat.copy) stat.barcode = stat.copy.barcode();
+
+    if (!cache.au[stat.hold.usr()]) cache.au[stat.hold.usr()] = pcrud.retrieve("au", stat.hold.usr());
+    stat.patron = cache.au[stat.hold.usr()];
+
+    if (!cache.ac[stat.patron.card()]) cache.ac[stat.patron.card()] = pcrud.retrieve("ac", stat.patron.card());
+    stat.card = cache.ac[stat.patron.card()];
+
+    // Grab the transit again, with stuff fleshed for printing
+    stat.transit = pcrud.search("ahtc", { hold: stat.hold.id() }, {flesh: 2, flesh_fields: {aou:['ill_address'], ahtc: ['source','dest']}})[0];
+
+    return renderScanStatusRow(stat);
+
+}
+
+function rejectAction(holdid, resetPart) {
+    if (!resetPart) resetPart = {fmg : true};
+    fieldmapper.standardRequest(
+        ['open-ils.circ','open-ils.circ.hold.reset'],
+        [user.authtoken, holdid]
+    );
+    resetLocation(holdLocation.getValue(), resetPart);
+
+    var stat = {
+        barcode: '',        // barcode we were passed
+        copy: null,         // item with the barcode we were passed
+        hold: null,         // hold FM object
+        patron: null,       // patron FM object
+        card: null,         // card FM object
+        transit: null,      // transit FM object
+        action: 'Reject',       // Receive, Check Out, Capture 
+        direction: 'Outgoing',    // Incoming, Outgoing
+    };
+
+    stat.hold = pcrud.retrieve("ahr", holdid); // refetch the hold
+    cache.ahr[stat.hold.id()] = stat.hold;
+
+    if (!cache.acp[stat.hold.current_copy()]) cache.acp[stat.hold.current_copy()] = pcrud.retrieve("acp", stat.hold.current_copy());
+    stat.copy = cache.acp[stat.hold.current_copy()];
+    if (stat.copy) stat.barcode = stat.copy.barcode();
+
+    if (!cache.au[stat.hold.usr()]) cache.au[stat.hold.usr()] = pcrud.retrieve("au", stat.hold.usr());
+    stat.patron = cache.au[stat.hold.usr()];
+
+    if (!cache.ac[stat.patron.card()]) cache.ac[stat.patron.card()] = pcrud.retrieve("ac", stat.patron.card());
+    stat.card = cache.ac[stat.patron.card()];
+
+    // Grab the transit again, with stuff fleshed for printing
+    stat.transit = pcrud.search("ahtc", { hold: stat.hold.id() }, {flesh: 2, flesh_fields: {aou:['ill_address'], ahtc: ['source','dest']}})[0];
+
+    return renderScanStatusRow(stat);
+
+}
+
+dojo.addOnLoad(function(){
+    user.buildPermOrgSelector(
+        'STAFF_LOGIN',
+        holdLocation,
+        cgi.param('l') || user.user.home_ou()
+    );
+});
+
diff --git a/Open-ILS/web/staff/js/record_mgmt.js b/Open-ILS/web/staff/js/record_mgmt.js
new file mode 100644 (file)
index 0000000..30e4521
--- /dev/null
@@ -0,0 +1,41 @@
+dojo.require("fieldmapper.AutoIDL");
+dojo.require("dijit.layout.ContentPane");
+dojo.require("dijit.layout.TabContainer");
+dojo.require("openils.widget.AutoGrid");
+dojo.require("openils.User");
+dojo.require("openils.PermaCrud");
+dojo.require("openils.widget.OrgUnitFilteringSelect");
+dojo.require("dojo.io.iframe");
+
+var user = new openils.User ({authcookie:'ses'});
+openils.User.authtoken = user.authtoken;
+var pcrud = new openils.PermaCrud ({authtoken:user.authtoken});
+var cgi = new CGI ();
+
+function wireForm(){
+    var canvas=dojo.byId("response");
+    dojo.connect(dojo.byId('localMarcForm'),"onsubmit",function(event){
+        dojo.stopEvent(event);
+
+        dojo.byId("response").innerHTML="&nbsp;&nbsp;Sending data, please wait...";
+        dojo.io.iframe.send({
+            form:     "localMarcForm",
+            handleAs: "html",
+            load:     function(data){ canvas.innerHTML = data.innerHTML },
+            error:    function(error){ canvas.innerHTML = error.innerHTML }
+        });
+
+    });
+}
+
+dojo.addOnLoad(function(){
+    user.buildPermOrgSelector(
+        'STAFF_LOGIN',
+        loc,
+        cgi.param('l') || user.user.home_ou()
+    );
+
+    wireForm();
+});
+
+
diff --git a/Open-ILS/web/staff/php/barcode/barcode.php b/Open-ILS/web/staff/php/barcode/barcode.php
new file mode 100755 (executable)
index 0000000..67eae11
--- /dev/null
@@ -0,0 +1,174 @@
+<?php\r
+/*\r
+Barcode Render Class for PHP using the GD graphics library \r
+Copyright (C) 2001  Karim Mribti\r
+                                                               \r
+   Version  0.0.7a  2001-04-01  \r
+                                                               \r
+This library is free software; you can redistribute it and/or\r
+modify it under the terms of the GNU General Public\r
+License as published by the Free Software Foundation; either\r
+version 2.1 of the License, or (at your option) any later version.\r
+                                                                                                                                 \r
+This library is distributed in the hope that it will be useful,\r
+but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
+General Public License for more details.\r
+                                                                                          \r
+You should have received a copy of the GNU General Public\r
+License along with this library; if not, write to the Free Software\r
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
+                                                                                                                                                \r
+Copy of GNU General Public License at: http://www.gnu.org/copyleft/gpl.txt\r
+                                                                                                        \r
+Source code home page: http://www.mribti.com/barcode/\r
+Contact author at: barcode@mribti.com\r
+*/\r
+\r
+require("debug.php");\r
+\r
+/***************************** base class ********************************************/\r
+/** NB: all GD call's is here **/\r
+\r
+/* Styles */\r
+\r
+/* Global */\r
+define("BCS_BORDER"            ,    1);\r
+define("BCS_TRANSPARENT"    ,    2);\r
+define("BCS_ALIGN_CENTER"   ,    4);\r
+define("BCS_ALIGN_LEFT"     ,    8);\r
+define("BCS_ALIGN_RIGHT"    ,   16);\r
+define("BCS_IMAGE_JPEG"     ,   32);\r
+define("BCS_IMAGE_PNG"      ,   64);\r
+define("BCS_DRAW_TEXT"      ,  128);\r
+define("BCS_STRETCH_TEXT"   ,  256);\r
+define("BCS_REVERSE_COLOR"  ,  512);\r
+/* For the I25 Only  */\r
+define("BCS_I25_DRAW_CHECK" , 2048);\r
+\r
+/* Default values */\r
+\r
+/* Global */\r
+define("BCD_DEFAULT_BACKGROUND_COLOR", 0xFFFFFF);\r
+define("BCD_DEFAULT_FOREGROUND_COLOR", 0x000000);\r
+define("BCD_DEFAULT_STYLE"           , BCS_BORDER | BCS_ALIGN_CENTER | BCS_IMAGE_PNG);\r
+define("BCD_DEFAULT_WIDTH"           , 460);\r
+define("BCD_DEFAULT_HEIGHT"          , 120);\r
+define("BCD_DEFAULT_FONT"            ,   5);\r
+define("BCD_DEFAULT_XRES"            ,   2);\r
+/* Margins */\r
+define("BCD_DEFAULT_MAR_Y1"          ,  10);\r
+define("BCD_DEFAULT_MAR_Y2"          ,  10);\r
+define("BCD_DEFAULT_TEXT_OFFSET"     ,   2);\r
+/* For the I25 Only */\r
+define("BCD_I25_NARROW_BAR"             ,   1);\r
+define("BCD_I25_WIDE_BAR"               ,   2);\r
+\r
+/* For the C39 Only */\r
+define("BCD_C39_NARROW_BAR"             ,   1);\r
+define("BCD_C39_WIDE_BAR"               ,   2);\r
+\r
+/* For Code 128 */\r
+define("BCD_C128_BAR_1"              ,   1);\r
+define("BCD_C128_BAR_2"              ,   2);\r
+define("BCD_C128_BAR_3"              ,   3);\r
+define("BCD_C128_BAR_4"              ,   4);\r
+\r
+  class BarcodeObject {\r
+                                      \r
+    var $mWidth, $mHeight, $mStyle, $mBgcolor, $mBrush;\r
+       var $mImg, $mFont;\r
+       var $mError;\r
+       \r
+       function BarcodeObject ($Width = BCD_DEFAULT_Width, $Height = BCD_DEFAULT_HEIGHT, $Style = BCD_DEFAULT_STYLE)  {\r
+           $this->mWidth   = $Width; \r
+               $this->mHeight  = $Height;\r
+               $this->mStyle   = $Style; \r
+               $this->mFont    = BCD_DEFAULT_FONT;\r
+               $this->mImg     = ImageCreate($this->mWidth, $this->mHeight);\r
+               $dbColor        = $this->mStyle & BCS_REVERSE_COLOR ? BCD_DEFAULT_FOREGROUND_COLOR : BCD_DEFAULT_BACKGROUND_COLOR;\r
+               $dfColor        = $this->mStyle & BCS_REVERSE_COLOR ? BCD_DEFAULT_BACKGROUND_COLOR : BCD_DEFAULT_FOREGROUND_COLOR;\r
+               $this->mBgcolor = ImageColorAllocate($this->mImg, ($dbColor & 0xFF0000) >> 16, \r
+                                                 ($dbColor & 0x00FF00) >> 8 , $dbColor & 0x0000FF);               \r
+               $this->mBrush   = ImageColorAllocate($this->mImg, ($dfColor & 0xFF0000) >> 16, \r
+                                                                 ($dfColor & 0x00FF00) >> 8 , $dfColor & 0x0000FF);\r
+               if (!($this->mStyle & BCS_TRANSPARENT)) {                                                                   \r
+                       ImageFill($this->mImg, $this->mWidth, $this->mHeight, $this->mBgcolor); \r
+               }\r
+         __TRACE__("OBJECT CONSTRUCTION: ".$this->mWidth." ".$this->mHeight." ".$this->mStyle);\r
+       }\r
+       \r
+    function DrawObject ($xres)        {\r
+         /* there is not implementation neded, is simply the asbsract function. */\r
+        __TRACE__("OBJECT DRAW: NEED VIRTUAL FUNCTION IMPLEMENTATION");\r
+        return false;\r
+       }\r
+       \r
+       function DrawBorder () {\r
+           ImageRectangle($this->mImg, 0, 0, $this->mWidth-1, $this->mHeight-1, $this->mBrush);\r
+           __TRACE__("DRAWING BORDER");\r
+       }\r
+       \r
+       function DrawChar ($Font, $xPos, $yPos, $Char) {\r
+                 ImageString($this->mImg,$Font,$xPos,$yPos,$Char,$this->mBrush);\r
+       }\r
+       \r
+       function DrawText ($Font, $xPos, $yPos, $Char) {\r
+                 ImageString($this->mImg,$Font,$xPos,$yPos,$Char,$this->mBrush);\r
+       }   \r
+               \r
+       function DrawSingleBar($xPos, $yPos, $xSize, $ySize) {\r
+         if ($xPos>=0 && $xPos<=$this->mWidth  && ($xPos+$xSize)<=$this->mWidth &&\r
+             $yPos>=0 && $yPos<=$this->mHeight && ($yPos+$ySize)<=$this->mHeight) {\r
+                   for ($i=0;$i<$xSize;$i++) {\r
+                          ImageLine($this->mImg, $xPos+$i, $yPos, $xPos+$i, $yPos+$ySize, $this->mBrush);\r
+                          }\r
+                          return true;\r
+                        }\r
+            __DEBUG__("DrawSingleBar: Out of range");\r
+                return false;    \r
+         }                                       \r
+                                                 \r
+       function GetError() {  \r
+         return $this->mError;\r
+       }                                          \r
+                                                  \r
+       function GetFontHeight($font) {\r
+        return ImageFontHeight($font);\r
+       }                                                          \r
+                                                                  \r
+       function GetFontWidth($font)  {\r
+        return ImageFontWidth($font);\r
+       }                                         \r
+                                                 \r
+       function SetFont($font) {\r
+        $this->mFont = $font;\r
+       }                                         \r
+                                                 \r
+       function GetStyle () {\r
+        return $this->mStyle;\r
+       }                                         \r
+                                                 \r
+       function SetStyle ($Style) {\r
+     __TRACE__("CHANGING STYLE");\r
+        $this->mStyle = $Style;\r
+       }                         \r
+                                 \r
+       function FlushObject () {\r
+        if (($this->mStyle & BCS_BORDER)) {\r
+              $this->DrawBorder();\r
+           }                                  \r
+          if ($this->mStyle & BCS_IMAGE_PNG) {\r
+         Header("Content-Type: image/png");\r
+            ImagePng($this->mImg);\r
+          } else if ($this->mStyle & BCS_IMAGE_JPEG) {\r
+                       Header("Content-Type: image/jpeg");\r
+                           ImageJpeg($this->mImg);\r
+                       } else __DEBUG__("FlushObject: No output type");\r
+        }                                                                                      \r
+                                                                                               \r
+     function DestroyObject () {\r
+          ImageDestroy($obj->mImg);\r
+        }\r
+   }\r
+?>\r
diff --git a/Open-ILS/web/staff/php/barcode/c128aobject.php b/Open-ILS/web/staff/php/barcode/c128aobject.php
new file mode 100755 (executable)
index 0000000..af4c130
--- /dev/null
@@ -0,0 +1,320 @@
+<?php\r
+/*\r
+Barcode Render Class for PHP using the GD graphics library \r
+Copyright (C) 2001  Karim Mribti\r
+                                                               \r
+   Version  0.0.7a  2001-04-01  \r
+                                                               \r
+This library is free software; you can redistribute it and/or\r
+modify it under the terms of the GNU General Public\r
+License as published by the Free Software Foundation; either\r
+version 2.1 of the License, or (at your option) any later version.\r
+                                                                                                                                 \r
+This library is distributed in the hope that it will be useful,\r
+but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
+General Public License for more details.\r
+                                                                                          \r
+You should have received a copy of the GNU General Public\r
+License along with this library; if not, write to the Free Software\r
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
+                                                                                                                                                \r
+Copy of GNU General Public License at: http://www.gnu.org/copyleft/gpl.txt\r
+                                                                                                        \r
+Source code home page: http://www.mribti.com/barcode/\r
+Contact author at: barcode@mribti.com\r
+*/\r
+  \r
+  /* \r
+    Render for Code 128-A   \r
+       Code 128-A is a continuous, multilevel and include all upper case alphanumeric characters and ASCII control characters .\r
+  */\r
+    \r
+    \r
+  class C128AObject extends BarcodeObject {\r
+   var $mCharSet, $mChars;\r
+   function C128AObject($Width, $Height, $Style, $Value)\r
+   {\r
+     $this->BarcodeObject($Width, $Height, $Style);\r
+        $this->mValue   = $Value;\r
+        $this->mChars   = " !\"#$%&'()*+´-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_";\r
+        $this->mCharSet = array\r
+         (\r
+                       "212222",   /*   00 */\r
+                       "222122",   /*   01 */\r
+                       "222221",   /*   02 */\r
+                       "121223",   /*   03 */\r
+                       "121322",   /*   04 */\r
+                       "131222",   /*   05 */\r
+                       "122213",   /*   06 */\r
+                       "122312",   /*   07 */\r
+                       "132212",   /*   08 */\r
+                       "221213",   /*   09 */\r
+                       "221312",   /*   10 */\r
+                       "231212",   /*   11 */\r
+                       "112232",   /*   12 */\r
+                       "122132",   /*   13 */\r
+                       "122231",   /*   14 */\r
+                       "113222",   /*   15 */\r
+                       "123122",   /*   16 */\r
+                       "123221",   /*   17 */\r
+                       "223211",   /*   18 */\r
+                       "221132",   /*   19 */\r
+                       "221231",   /*   20 */\r
+                       "213212",   /*   21 */\r
+                       "223112",   /*   22 */\r
+                       "312131",   /*   23 */\r
+                       "311222",   /*   24 */\r
+                       "321122",   /*   25 */\r
+                       "321221",   /*   26 */\r
+                       "312212",   /*   27 */\r
+                       "322112",   /*   28 */\r
+                       "322211",   /*   29 */\r
+                       "212123",   /*   30 */\r
+                       "212321",   /*   31 */\r
+                       "232121",   /*   32 */\r
+                       "111323",   /*   33 */\r
+                       "131123",   /*   34 */\r
+                       "131321",   /*   35 */\r
+                       "112313",   /*   36 */\r
+                       "132113",   /*   37 */\r
+                       "132311",   /*   38 */\r
+                       "211313",   /*   39 */\r
+                       "231113",   /*   40 */\r
+                       "231311",   /*   41 */\r
+                       "112133",   /*   42 */\r
+                       "112331",   /*   43 */\r
+                       "132131",   /*   44 */\r
+                       "113123",   /*   45 */\r
+                       "113321",   /*   46 */\r
+                       "133121",   /*   47 */\r
+                       "313121",   /*   48 */\r
+                       "211331",   /*   49 */\r
+                       "231131",   /*   50 */\r
+                       "213113",   /*   51 */\r
+                       "213311",   /*   52 */\r
+                       "213131",   /*   53 */\r
+                       "311123",   /*   54 */\r
+                       "311321",   /*   55 */\r
+                       "331121",   /*   56 */\r
+                       "312113",   /*   57 */\r
+                       "312311",   /*   58 */\r
+                       "332111",   /*   59 */\r
+                       "314111",   /*   60 */\r
+                       "221411",   /*   61 */\r
+                       "431111",   /*   62 */\r
+                       "111224",   /*   63 */\r
+                       "111422",   /*   64 */\r
+                       "121124",   /*   65 */\r
+                       "121421",   /*   66 */\r
+                       "141122",   /*   67 */\r
+                       "141221",   /*   68 */\r
+                       "112214",   /*   69 */\r
+                       "112412",   /*   70 */\r
+                       "122114",   /*   71 */\r
+                       "122411",   /*   72 */\r
+                       "142112",   /*   73 */\r
+                       "142211",   /*   74 */\r
+                       "241211",   /*   75 */\r
+                       "221114",   /*   76 */\r
+                       "413111",   /*   77 */\r
+                       "241112",   /*   78 */\r
+                       "134111",   /*   79 */\r
+                       "111242",   /*   80 */\r
+                       "121142",   /*   81 */\r
+                       "121241",   /*   82 */\r
+                       "114212",   /*   83 */\r
+                       "124112",   /*   84 */\r
+                       "124211",   /*   85 */\r
+                       "411212",   /*   86 */\r
+                       "421112",   /*   87 */\r
+                       "421211",   /*   88 */\r
+                       "212141",   /*   89 */\r
+                       "214121",   /*   90 */\r
+                       "412121",   /*   91 */\r
+                       "111143",   /*   92 */\r
+                       "111341",   /*   93 */\r
+                       "131141",   /*   94 */\r
+                       "114113",   /*   95 */\r
+                       "114311",   /*   96 */\r
+                       "411113",   /*   97 */\r
+                       "411311",   /*   98 */\r
+                       "113141",   /*   99 */\r
+                       "114131",   /*  100 */\r
+                       "311141",   /*  101 */\r
+                       "411131"    /*  102 */\r
+       );\r
+   }\r
+   \r
+   function GetCharIndex ($char) {\r
+    for ($i=0;$i<64;$i++) {\r
+         if ($this->mChars[$i] == $char)\r
+            return $i;\r
+        }\r
+        return -1;\r
+   }\r
+   \r
+   function GetBarSize ($xres, $char) { \r
+     switch ($char)\r
+        {\r
+         case '1':\r
+                               $cVal = BCD_C128_BAR_1;\r
+                               break;\r
+         case '2':\r
+                               $cVal = BCD_C128_BAR_2;\r
+                               break;\r
+         case '3':\r
+                               $cVal = BCD_C128_BAR_3;\r
+                               break;\r
+         case '4':\r
+                               $cVal = BCD_C128_BAR_4;\r
+                               break;\r
+         default:\r
+                               $cVal = 0;\r
+        }\r
+    return  $cVal * $xres;\r
+   }\r
+\r
+    \r
+   function GetSize($xres) {\r
+     $len = strlen($this->mValue);\r
+        \r
+        if ($len == 0)  {\r
+          $this->mError = "Null value";\r
+          __DEBUG__("GetRealSize: null barcode value");\r
+          return false;\r
+          }\r
+        $ret = 0;\r
+        for ($i=0;$i<$len;$i++) {\r
+         if (($id = $this->GetCharIndex($this->mValue[$i])) == -1) {\r
+               $this->mError = "C128A not include the char '".$this->mValue[$i]."'";\r
+                       return false;\r
+                } else {\r
+                                $cset = $this->mCharSet[$id];\r
+                                $ret += $this->GetBarSize($xres, $cset[0]);\r
+                                $ret += $this->GetBarSize($xres, $cset[1]);\r
+                                $ret += $this->GetBarSize($xres, $cset[2]);\r
+                                $ret += $this->GetBarSize($xres, $cset[3]);\r
+                                $ret += $this->GetBarSize($xres, $cset[4]);\r
+                                $ret += $this->GetBarSize($xres, $cset[5]);\r
+                               }\r
+        }   \r
+                \r
+        /* length of Check character */\r
+        $cset = $this->GetCheckCharValue();\r
+        for ($i=0;$i<6;$i++) {\r
+          $CheckSize += $this->GetBarSize($cset[$i], $xres);\r
+        } \r
+        $StartSize = 2*BCD_C128_BAR_2*$xres + 3*BCD_C128_BAR_1*$xres + BCD_C128_BAR_4*$xres;\r
+        $StopSize  = 2*BCD_C128_BAR_2*$xres + 3*BCD_C128_BAR_1*$xres + 2*BCD_C128_BAR_3*$xres;\r
+        return $StartSize + $ret + $CheckSize + $StopSize;\r
+   }\r
+   \r
+   function GetCheckCharValue()\r
+   {\r
+     $len = strlen($this->mValue);\r
+        $sum = 103; // 'A' type;\r
+        for ($i=0;$i<$len;$i++) {\r
+         $sum +=  $this->GetCharIndex($this->mValue[$i]) * ($i+1);\r
+        }\r
+        $check  = $sum % 103;\r
+        return $this->mCharSet[$check];\r
+    }\r
+\r
+   function DrawStart($DrawPos, $yPos, $ySize, $xres)\r
+   {  /* Start code is '211412' */\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('2', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('2', $xres);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('1', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $DrawPos += $this->GetBarSize('4', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('1', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $DrawPos += $this->GetBarSize('2', $xres);\r
+         return $DrawPos;\r
+   }\r
+   \r
+   function DrawStop($DrawPos, $yPos, $ySize, $xres)\r
+   {  /* Stop code is '2331112' */\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('2', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('2', $xres);\r
+         $DrawPos += $this->GetBarSize('3', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('3', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('3', $xres);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('1', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('2', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('2', $xres);\r
+         return $DrawPos;\r
+   }\r
+   \r
+   function DrawCheckChar($DrawPos, $yPos, $ySize, $xres)\r
+   {\r
+     $cset = $this->GetCheckCharValue();\r
+        $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[0], $xres) , $ySize);\r
+        $DrawPos += $this->GetBarSize($cset[0], $xres);\r
+        $DrawPos += $this->GetBarSize($cset[1], $xres);\r
+        $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[2], $xres) , $ySize);\r
+        $DrawPos += $this->GetBarSize($cset[2], $xres);\r
+        $DrawPos += $this->GetBarSize($cset[3], $xres);\r
+        $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[4], $xres) , $ySize);\r
+        $DrawPos += $this->GetBarSize($cset[4], $xres);\r
+        $DrawPos += $this->GetBarSize($cset[5], $xres); \r
+        return $DrawPos;\r
+    }\r
+   \r
+   function DrawObject ($xres)\r
+   {\r
+     $len = strlen($this->mValue);               \r
+        if (($size = $this->GetSize($xres))==0) {\r
+       __DEBUG__("GetSize: failed");\r
+           return false;\r
+        }    \r
+                 \r
+        if ($this->mStyle & BCS_ALIGN_CENTER) $sPos = (integer)(($this->mWidth - $size ) / 2);\r
+        else if ($this->mStyle & BCS_ALIGN_RIGHT) $sPos = $this->mWidth - $size;\r
+                 else $sPos = 0;               \r
+                                                               \r
+        /* Total height of bar code -Bars only- */                                     \r
+        if ($this->mStyle & BCS_DRAW_TEXT) $ysize = $this->mHeight - BCD_DEFAULT_MAR_Y1 - BCD_DEFAULT_MAR_Y2 - $this->GetFontHeight($this->mFont);\r
+        else $ysize = $this->mHeight - BCD_DEFAULT_MAR_Y1 - BCD_DEFAULT_MAR_Y2;\r
+                                                                                \r
+        /* Draw text */ \r
+        if ($this->mStyle & BCS_DRAW_TEXT) {\r
+                if ($this->mStyle & BCS_STRETCH_TEXT) {\r
+                       for ($i=0;$i<$len;$i++) {\r
+                               $this->DrawChar($this->mFont, $sPos+(2*BCD_C128_BAR_2*$xres + 3*BCD_C128_BAR_1*$xres + BCD_C128_BAR_4*$xres)+($size/$len)*$i,\r
+                                                $ysize + BCD_DEFAULT_MAR_Y1 + BCD_DEFAULT_TEXT_OFFSET, $this->mValue[$i]);\r
+                               }                                                                                  \r
+                } else {/* Center */\r
+                        $text_width = $this->GetFontWidth($this->mFont) * strlen($this->mValue);\r
+                        $this->DrawText($this->mFont, $sPos+(($size-$text_width)/2)+(2*BCD_C128_BAR_2*$xres + 3*BCD_C128_BAR_1*$xres + BCD_C128_BAR_4*$xres),\r
+                                                         $ysize + BCD_DEFAULT_MAR_Y1 + BCD_DEFAULT_TEXT_OFFSET, $this->mValue);\r
+             }  \r
+         }                      \r
+                                                                       \r
+        $cPos = 0;                                      \r
+        $DrawPos = $this->DrawStart($sPos, BCD_DEFAULT_MAR_Y1 , $ysize, $xres); \r
+        do {                                \r
+               $c     = $this->GetCharIndex($this->mValue[$cPos]);\r
+               $cset  = $this->mCharSet[$c];                   \r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[0], $xres) , $ysize);\r
+           $DrawPos += $this->GetBarSize($cset[0], $xres);\r
+           $DrawPos += $this->GetBarSize($cset[1], $xres);\r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[2], $xres) , $ysize);\r
+           $DrawPos += $this->GetBarSize($cset[2], $xres);\r
+           $DrawPos += $this->GetBarSize($cset[3], $xres);\r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[4], $xres) , $ysize);\r
+           $DrawPos += $this->GetBarSize($cset[4], $xres);\r
+           $DrawPos += $this->GetBarSize($cset[5], $xres);\r
+               $cPos++; \r
+         } while ($cPos<$len);\r
+         $DrawPos = $this->DrawCheckChar($DrawPos, BCD_DEFAULT_MAR_Y1 , $ysize, $xres);\r
+         $DrawPos =  $this->DrawStop($DrawPos, BCD_DEFAULT_MAR_Y1 , $ysize, $xres);\r
+         return true;\r
+        }\r
+  }\r
+?>\r
diff --git a/Open-ILS/web/staff/php/barcode/c128bobject.php b/Open-ILS/web/staff/php/barcode/c128bobject.php
new file mode 100755 (executable)
index 0000000..b6ec95b
--- /dev/null
@@ -0,0 +1,321 @@
+<?php\r
+/*\r
+Barcode Render Class for PHP using the GD graphics library \r
+Copyright (C) 2001  Karim Mribti\r
+                                                               \r
+   Version  0.0.7a  2001-04-01  \r
+                                                               \r
+This library is free software; you can redistribute it and/or\r
+modify it under the terms of the GNU General Public\r
+License as published by the Free Software Foundation; either\r
+version 2.1 of the License, or (at your option) any later version.\r
+                                                                                                                                 \r
+This library is distributed in the hope that it will be useful,\r
+but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
+General Public License for more details.\r
+                                                                                          \r
+You should have received a copy of the GNU General Public\r
+License along with this library; if not, write to the Free Software\r
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
+                                                                                                                                                \r
+Copy of GNU General Public License at: http://www.gnu.org/copyleft/gpl.txt\r
+                                                                                                        \r
+Source code home page: http://www.mribti.com/barcode/\r
+Contact author at: barcode@mribti.com\r
+*/\r
+  \r
+  /* \r
+    Render for Code 128-B   \r
+       Code 128-B is a continuous, multilevel and full ASCII code .\r
+  */\r
+    \r
+    \r
+  class C128BObject extends BarcodeObject {\r
+   var $mCharSet, $mChars;\r
+   function C128BObject($Width, $Height, $Style, $Value)\r
+   {\r
+     $this->BarcodeObject($Width, $Height, $Style);\r
+        $this->mValue   = $Value;\r
+        $this->mChars   = " !\"#$%&'()*+´-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{ }~";\r
+        $this->mCharSet = array\r
+         (\r
+                       "212222",   /*   00 */\r
+                       "222122",   /*   01 */\r
+                       "222221",   /*   02 */\r
+                       "121223",   /*   03 */\r
+                       "121322",   /*   04 */\r
+                       "131222",   /*   05 */\r
+                       "122213",   /*   06 */\r
+                       "122312",   /*   07 */\r
+                       "132212",   /*   08 */\r
+                       "221213",   /*   09 */\r
+                       "221312",   /*   10 */\r
+                       "231212",   /*   11 */\r
+                       "112232",   /*   12 */\r
+                       "122132",   /*   13 */\r
+                       "122231",   /*   14 */\r
+                       "113222",   /*   15 */\r
+                       "123122",   /*   16 */\r
+                       "123221",   /*   17 */\r
+                       "223211",   /*   18 */\r
+                       "221132",   /*   19 */\r
+                       "221231",   /*   20 */\r
+                       "213212",   /*   21 */\r
+                       "223112",   /*   22 */\r
+                       "312131",   /*   23 */\r
+                       "311222",   /*   24 */\r
+                       "321122",   /*   25 */\r
+                       "321221",   /*   26 */\r
+                       "312212",   /*   27 */\r
+                       "322112",   /*   28 */\r
+                       "322211",   /*   29 */\r
+                       "212123",   /*   30 */\r
+                       "212321",   /*   31 */\r
+                       "232121",   /*   32 */\r
+                       "111323",   /*   33 */\r
+                       "131123",   /*   34 */\r
+                       "131321",   /*   35 */\r
+                       "112313",   /*   36 */\r
+                       "132113",   /*   37 */\r
+                       "132311",   /*   38 */\r
+                       "211313",   /*   39 */\r
+                       "231113",   /*   40 */\r
+                       "231311",   /*   41 */\r
+                       "112133",   /*   42 */\r
+                       "112331",   /*   43 */\r
+                       "132131",   /*   44 */\r
+                       "113123",   /*   45 */\r
+                       "113321",   /*   46 */\r
+                       "133121",   /*   47 */\r
+                       "313121",   /*   48 */\r
+                       "211331",   /*   49 */\r
+                       "231131",   /*   50 */\r
+                       "213113",   /*   51 */\r
+                       "213311",   /*   52 */\r
+                       "213131",   /*   53 */\r
+                       "311123",   /*   54 */\r
+                       "311321",   /*   55 */\r
+                       "331121",   /*   56 */\r
+                       "312113",   /*   57 */\r
+                       "312311",   /*   58 */\r
+                       "332111",   /*   59 */\r
+                       "314111",   /*   60 */\r
+                       "221411",   /*   61 */\r
+                       "431111",   /*   62 */\r
+                       "111224",   /*   63 */\r
+                       "111422",   /*   64 */\r
+                       "121124",   /*   65 */\r
+                       "121421",   /*   66 */\r
+                       "141122",   /*   67 */\r
+                       "141221",   /*   68 */\r
+                       "112214",   /*   69 */\r
+                       "112412",   /*   70 */\r
+                       "122114",   /*   71 */\r
+                       "122411",   /*   72 */\r
+                       "142112",   /*   73 */\r
+                       "142211",   /*   74 */\r
+                       "241211",   /*   75 */\r
+                       "221114",   /*   76 */\r
+                       "413111",   /*   77 */\r
+                       "241112",   /*   78 */\r
+                       "134111",   /*   79 */\r
+                       "111242",   /*   80 */\r
+                       "121142",   /*   81 */\r
+                       "121241",   /*   82 */\r
+                       "114212",   /*   83 */\r
+                       "124112",   /*   84 */\r
+                       "124211",   /*   85 */\r
+                       "411212",   /*   86 */\r
+                       "421112",   /*   87 */\r
+                       "421211",   /*   88 */\r
+                       "212141",   /*   89 */\r
+                       "214121",   /*   90 */\r
+                       "412121",   /*   91 */\r
+                       "111143",   /*   92 */\r
+                       "111341",   /*   93 */\r
+                       "131141",   /*   94 */\r
+                       "114113",   /*   95 */\r
+                       "114311",   /*   96 */\r
+                       "411113",   /*   97 */\r
+                       "411311",   /*   98 */\r
+                       "113141",   /*   99 */\r
+                       "114131",   /*  100 */\r
+                       "311141",   /*  101 */\r
+                       "411131"    /*  102 */\r
+       );\r
+   }\r
+   \r
+   function GetCharIndex ($char) {\r
+    for ($i=0;$i<95;$i++) {\r
+         if ($this->mChars[$i] == $char)\r
+            return $i;\r
+        }\r
+        return -1;\r
+   }\r
+   \r
+   function GetBarSize ($xres, $char) { \r
+     switch ($char)\r
+        {\r
+         case '1':\r
+                               $cVal = BCD_C128_BAR_1;\r
+                               break;\r
+         case '2':\r
+                               $cVal = BCD_C128_BAR_2;\r
+                               break;\r
+         case '3':\r
+                               $cVal = BCD_C128_BAR_3;\r
+                               break;\r
+         case '4':\r
+                               $cVal = BCD_C128_BAR_4;\r
+                               break;\r
+         default:\r
+                               $cVal = 0;\r
+        }\r
+    return  $cVal * $xres;\r
+   }\r
+\r
+    \r
+   function GetSize($xres) {\r
+     $len = strlen($this->mValue);\r
+        \r
+        if ($len == 0)  {\r
+          $this->mError = "Null value";\r
+          __DEBUG__("GetRealSize: null barcode value");\r
+          return false;\r
+          }\r
+        $ret = 0;\r
+        for ($i=0;$i<$len;$i++) {\r
+         if (($id = $this->GetCharIndex($this->mValue[$i])) == -1) {\r
+               $this->mError = "C128B not include the char '".$this->mValue[$i]."'";\r
+                       return false;\r
+                } else {\r
+                                $cset = $this->mCharSet[$id];\r
+                                $ret += $this->GetBarSize($xres, $cset[0]);\r
+                                $ret += $this->GetBarSize($xres, $cset[1]);\r
+                                $ret += $this->GetBarSize($xres, $cset[2]);\r
+                                $ret += $this->GetBarSize($xres, $cset[3]);\r
+                                $ret += $this->GetBarSize($xres, $cset[4]);\r
+                                $ret += $this->GetBarSize($xres, $cset[5]);\r
+                               }\r
+        }   \r
+        /* length of Check character */\r
+        $cset = $this->GetCheckCharValue();\r
+        for ($i=0;$i<6;$i++) {\r
+          $CheckSize += $this->GetBarSize($cset[$i], $xres);\r
+        }\r
+                 \r
+        $StartSize = 2*BCD_C128_BAR_2*$xres + 3*BCD_C128_BAR_1*$xres + BCD_C128_BAR_4*$xres;\r
+        $StopSize  = 2*BCD_C128_BAR_2*$xres + 3*BCD_C128_BAR_1*$xres + 2*BCD_C128_BAR_3*$xres;\r
+         \r
+         return $StartSize + $ret + $CheckSize + $StopSize;\r
+   }\r
+   \r
+   function GetCheckCharValue()\r
+   {\r
+     $len = strlen($this->mValue);\r
+        $sum = 104; // 'B' type;\r
+        for ($i=0;$i<$len;$i++) {\r
+         $sum +=  $this->GetCharIndex($this->mValue[$i]) * ($i+1);\r
+        }\r
+        $check  = $sum % 103;\r
+        return $this->mCharSet[$check];\r
+    }\r
+   \r
+   function DrawStart($DrawPos, $yPos, $ySize, $xres)\r
+   {  /* Start code is '211214' */\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('2', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('2', $xres);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('1', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $DrawPos += $this->GetBarSize('2', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('1', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $DrawPos += $this->GetBarSize('4', $xres);\r
+         return $DrawPos;\r
+   }\r
+   \r
+   function DrawStop($DrawPos, $yPos, $ySize, $xres)\r
+   {  /* Stop code is '2331112' */\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('2', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('2', $xres);\r
+         $DrawPos += $this->GetBarSize('3', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('3', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('3', $xres);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('1', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('2', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('2', $xres);\r
+         return $DrawPos;\r
+   }\r
+       \r
+   function DrawCheckChar($DrawPos, $yPos, $ySize, $xres)\r
+   {\r
+     $cset = $this->GetCheckCharValue();\r
+        $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[0], $xres) , $ySize);\r
+        $DrawPos += $this->GetBarSize($cset[0], $xres);\r
+        $DrawPos += $this->GetBarSize($cset[1], $xres);\r
+        $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[2], $xres) , $ySize);\r
+        $DrawPos += $this->GetBarSize($cset[2], $xres);\r
+        $DrawPos += $this->GetBarSize($cset[3], $xres);\r
+        $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[4], $xres) , $ySize);\r
+        $DrawPos += $this->GetBarSize($cset[4], $xres);\r
+        $DrawPos += $this->GetBarSize($cset[5], $xres); \r
+        return $DrawPos;\r
+    }\r
+        \r
+    function DrawObject ($xres)\r
+    {\r
+     $len = strlen($this->mValue);               \r
+        if (($size = $this->GetSize($xres))==0) {\r
+       __DEBUG__("GetSize: failed");\r
+           return false;\r
+        }    \r
+                 \r
+        if ($this->mStyle & BCS_ALIGN_CENTER) $sPos = (integer)(($this->mWidth - $size ) / 2);\r
+        else if ($this->mStyle & BCS_ALIGN_RIGHT) $sPos = $this->mWidth - $size;\r
+                 else $sPos = 0;               \r
+                                                               \r
+        /* Total height of bar code -Bars only- */                                     \r
+        if ($this->mStyle & BCS_DRAW_TEXT) $ysize = $this->mHeight - BCD_DEFAULT_MAR_Y1 - BCD_DEFAULT_MAR_Y2 - $this->GetFontHeight($this->mFont);\r
+        else $ysize = $this->mHeight - BCD_DEFAULT_MAR_Y1 - BCD_DEFAULT_MAR_Y2;\r
+                                                                                \r
+        /* Draw text */ \r
+        if ($this->mStyle & BCS_DRAW_TEXT) {\r
+                if ($this->mStyle & BCS_STRETCH_TEXT) {\r
+                       for ($i=0;$i<$len;$i++) {\r
+                               $this->DrawChar($this->mFont, $sPos+(2*BCD_C128_BAR_2*$xres + 3*BCD_C128_BAR_1*$xres + BCD_C128_BAR_4*$xres)+($size/$len)*$i,\r
+                                                $ysize + BCD_DEFAULT_MAR_Y1 + BCD_DEFAULT_TEXT_OFFSET, $this->mValue[$i]);\r
+                               }                                                                                  \r
+                } else {/* Center */\r
+                        $text_width = $this->GetFontWidth($this->mFont) * strlen($this->mValue);\r
+                        $this->DrawText($this->mFont, $sPos+(($size-$text_width)/2)+(2*BCD_C128_BAR_2*$xres + 3*BCD_C128_BAR_1*$xres + BCD_C128_BAR_4*$xres),\r
+                                                         $ysize + BCD_DEFAULT_MAR_Y1 + BCD_DEFAULT_TEXT_OFFSET, $this->mValue);\r
+             }  \r
+         }                      \r
+                                                                       \r
+        $cPos = 0;                                      \r
+        $DrawPos = $this->DrawStart($sPos, BCD_DEFAULT_MAR_Y1 , $ysize, $xres); \r
+        do {                                \r
+               $c     = $this->GetCharIndex($this->mValue[$cPos]);\r
+               $cset  = $this->mCharSet[$c];                   \r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[0], $xres) , $ysize);\r
+           $DrawPos += $this->GetBarSize($cset[0], $xres);\r
+           $DrawPos += $this->GetBarSize($cset[1], $xres);\r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[2], $xres) , $ysize);\r
+           $DrawPos += $this->GetBarSize($cset[2], $xres);\r
+           $DrawPos += $this->GetBarSize($cset[3], $xres);\r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[4], $xres) , $ysize);\r
+           $DrawPos += $this->GetBarSize($cset[4], $xres);\r
+           $DrawPos += $this->GetBarSize($cset[5], $xres);\r
+               $cPos++; \r
+         } while ($cPos<$len);\r
+         $DrawPos = $this->DrawCheckChar($DrawPos, BCD_DEFAULT_MAR_Y1 , $ysize, $xres);\r
+         $DrawPos =  $this->DrawStop($DrawPos, BCD_DEFAULT_MAR_Y1 , $ysize, $xres);\r
+         return true;\r
+        }\r
+  }\r
+?>\r
diff --git a/Open-ILS/web/staff/php/barcode/c128cobject.php b/Open-ILS/web/staff/php/barcode/c128cobject.php
new file mode 100755 (executable)
index 0000000..dc602cd
--- /dev/null
@@ -0,0 +1,344 @@
+<?php\r
+/*\r
+Barcode Render Class for PHP using the GD graphics library \r
+Copyright (C) 2001  Karim Mribti\r
+  -- Written on 2001-08-03 by Sam Michaels\r
+       to add Code 128-C support.\r
+       swampgas@swampgas.org\r
+                                                               \r
+   Version  0.0.7a  2001-08-03  \r
+                                                               \r
+This library is free software; you can redistribute it and/or\r
+modify it under the terms of the GNU General Public\r
+License as published by the Free Software Foundation; either\r
+version 2.1 of the License, or (at your option) any later version.\r
+                                                                                                                                 \r
+This library is distributed in the hope that it will be useful,\r
+but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
+General Public License for more details.\r
+                                                                                          \r
+You should have received a copy of the GNU General Public\r
+License along with this library; if not, write to the Free Software\r
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
+                                                                                                                                                \r
+Copy of GNU General Public License at: http://www.gnu.org/copyleft/gpl.txt\r
+                                                                                                        \r
+Source code home page: http://www.mribti.com/barcode/\r
+Contact author at: barcode@mribti.com\r
+*/\r
+  \r
+  /* \r
+    Render for Code 128-C\r
+        Code 128-C is numeric only and provides the most efficiency.\r
+  */\r
+    \r
+    \r
+  class C128CObject extends BarcodeObject {\r
+   var $mCharSet, $mChars;\r
+   function C128CObject($Width, $Height, $Style, $Value)\r
+   {\r
+     $this->BarcodeObject($Width, $Height, $Style);\r
+        $this->mValue   = $Value;\r
+         $this->mChars   = array\r
+          (\r
+            "00", "01", "02", "03", "04", "05", "06", "07", "08", "09",\r
+            "10", "11", "12", "13", "14", "15", "16", "17", "18", "19",\r
+            "20", "21", "22", "23", "24", "25", "26", "27", "28", "29",\r
+            "30", "31", "32", "33", "34", "35", "36", "37", "38", "39",\r
+            "40", "41", "42", "43", "44", "45", "46", "47", "48", "49",\r
+            "50", "51", "52", "53", "54", "55", "56", "57", "58", "59",\r
+            "60", "61", "62", "63", "64", "65", "66", "67", "68", "69",\r
+            "70", "71", "72", "73", "74", "75", "76", "77", "78", "79",\r
+            "80", "81", "82", "83", "84", "85", "86", "87", "88", "89",\r
+            "90", "91", "92", "93", "94", "95", "96", "97", "98", "99",\r
+          );\r
+        $this->mCharSet = array\r
+         (\r
+                       "212222",   /*   00 */\r
+                       "222122",   /*   01 */\r
+                       "222221",   /*   02 */\r
+                       "121223",   /*   03 */\r
+                       "121322",   /*   04 */\r
+                       "131222",   /*   05 */\r
+                       "122213",   /*   06 */\r
+                       "122312",   /*   07 */\r
+                       "132212",   /*   08 */\r
+                       "221213",   /*   09 */\r
+                       "221312",   /*   10 */\r
+                       "231212",   /*   11 */\r
+                       "112232",   /*   12 */\r
+                       "122132",   /*   13 */\r
+                       "122231",   /*   14 */\r
+                       "113222",   /*   15 */\r
+                       "123122",   /*   16 */\r
+                       "123221",   /*   17 */\r
+                       "223211",   /*   18 */\r
+                       "221132",   /*   19 */\r
+                       "221231",   /*   20 */\r
+                       "213212",   /*   21 */\r
+                       "223112",   /*   22 */\r
+                       "312131",   /*   23 */\r
+                       "311222",   /*   24 */\r
+                       "321122",   /*   25 */\r
+                       "321221",   /*   26 */\r
+                       "312212",   /*   27 */\r
+                       "322112",   /*   28 */\r
+                       "322211",   /*   29 */\r
+                       "212123",   /*   30 */\r
+                       "212321",   /*   31 */\r
+                       "232121",   /*   32 */\r
+                       "111323",   /*   33 */\r
+                       "131123",   /*   34 */\r
+                       "131321",   /*   35 */\r
+                       "112313",   /*   36 */\r
+                       "132113",   /*   37 */\r
+                       "132311",   /*   38 */\r
+                       "211313",   /*   39 */\r
+                       "231113",   /*   40 */\r
+                       "231311",   /*   41 */\r
+                       "112133",   /*   42 */\r
+                       "112331",   /*   43 */\r
+                       "132131",   /*   44 */\r
+                       "113123",   /*   45 */\r
+                       "113321",   /*   46 */\r
+                       "133121",   /*   47 */\r
+                       "313121",   /*   48 */\r
+                       "211331",   /*   49 */\r
+                       "231131",   /*   50 */\r
+                       "213113",   /*   51 */\r
+                       "213311",   /*   52 */\r
+                       "213131",   /*   53 */\r
+                       "311123",   /*   54 */\r
+                       "311321",   /*   55 */\r
+                       "331121",   /*   56 */\r
+                       "312113",   /*   57 */\r
+                       "312311",   /*   58 */\r
+                       "332111",   /*   59 */\r
+                       "314111",   /*   60 */\r
+                       "221411",   /*   61 */\r
+                       "431111",   /*   62 */\r
+                       "111224",   /*   63 */\r
+                       "111422",   /*   64 */\r
+                       "121124",   /*   65 */\r
+                       "121421",   /*   66 */\r
+                       "141122",   /*   67 */\r
+                       "141221",   /*   68 */\r
+                       "112214",   /*   69 */\r
+                       "112412",   /*   70 */\r
+                       "122114",   /*   71 */\r
+                       "122411",   /*   72 */\r
+                       "142112",   /*   73 */\r
+                       "142211",   /*   74 */\r
+                       "241211",   /*   75 */\r
+                       "221114",   /*   76 */\r
+                       "413111",   /*   77 */\r
+                       "241112",   /*   78 */\r
+                       "134111",   /*   79 */\r
+                       "111242",   /*   80 */\r
+                       "121142",   /*   81 */\r
+                       "121241",   /*   82 */\r
+                       "114212",   /*   83 */\r
+                       "124112",   /*   84 */\r
+                       "124211",   /*   85 */\r
+                       "411212",   /*   86 */\r
+                       "421112",   /*   87 */\r
+                       "421211",   /*   88 */\r
+                       "212141",   /*   89 */\r
+                       "214121",   /*   90 */\r
+                       "412121",   /*   91 */\r
+                       "111143",   /*   92 */\r
+                       "111341",   /*   93 */\r
+                       "131141",   /*   94 */\r
+                       "114113",   /*   95 */\r
+                       "114311",   /*   96 */\r
+                       "411113",   /*   97 */\r
+                       "411311",   /*   98 */\r
+                       "113141",   /*   99 */\r
+       );\r
+   }\r
+   \r
+   function GetCharIndex ($char) {\r
+    for ($i=0;$i<100;$i++) {\r
+         if ($this->mChars[$i] == $char)\r
+            return $i;\r
+        }\r
+        return -1;\r
+   }\r
+   \r
+   function GetBarSize ($xres, $char) { \r
+     switch ($char)\r
+        {\r
+         case '1':\r
+                               $cVal = BCD_C128_BAR_1;\r
+                               break;\r
+         case '2':\r
+                               $cVal = BCD_C128_BAR_2;\r
+                               break;\r
+         case '3':\r
+                               $cVal = BCD_C128_BAR_3;\r
+                               break;\r
+         case '4':\r
+                               $cVal = BCD_C128_BAR_4;\r
+                               break;\r
+         default:\r
+                               $cVal = 0;\r
+        }\r
+    return  $cVal * $xres;\r
+   }\r
+\r
+    \r
+   function GetSize($xres) {\r
+     $len = strlen($this->mValue);\r
+        \r
+        if ($len == 0)  {\r
+          $this->mError = "Null value";\r
+          __DEBUG__("GetRealSize: null barcode value");\r
+          return false;\r
+          }\r
+        $ret = 0;\r
+\r
+         for ($i=0;$i<$len;$i++) {\r
+         if ((ord($this->mValue[$i])<48) || (ord($this->mValue[$i])>57)) {\r
+                $this->mError = "Code-128C is numeric only";\r
+                       return false;\r
+                }\r
+        }\r
+\r
+        if (($len%2) != 0) {\r
+            $this->mError = "The length of barcode value must be even.  You must pad the number with zeros.";\r
+            __DEBUG__("GetSize: failed C128-C requiremente");\r
+               return false;\r
+               }                \r
+\r
+        for ($i=0;$i<$len;$i+=2) {\r
+          $id = $this->GetCharIndex($this->mValue[$i].$this->mValue[$i+1]);\r
+          $cset = $this->mCharSet[$id];\r
+          $ret += $this->GetBarSize($xres, $cset[0]);\r
+          $ret += $this->GetBarSize($xres, $cset[1]);\r
+          $ret += $this->GetBarSize($xres, $cset[2]);\r
+          $ret += $this->GetBarSize($xres, $cset[3]);\r
+          $ret += $this->GetBarSize($xres, $cset[4]);\r
+          $ret += $this->GetBarSize($xres, $cset[5]);\r
+        }   \r
+        /* length of Check character */\r
+        $cset = $this->GetCheckCharValue();\r
+         for ($i=0;$i<6;$i++) {\r
+          $CheckSize += $this->GetBarSize($cset[$i], $xres);\r
+        }\r
+                 \r
+        $StartSize = 2*BCD_C128_BAR_2*$xres + 3*BCD_C128_BAR_1*$xres + BCD_C128_BAR_4*$xres;\r
+        $StopSize  = 2*BCD_C128_BAR_2*$xres + 3*BCD_C128_BAR_1*$xres + 2*BCD_C128_BAR_3*$xres;\r
+         return $StartSize + $ret + $CheckSize + $StopSize;\r
+   }\r
+   \r
+   function GetCheckCharValue()\r
+   {\r
+     $len = strlen($this->mValue);\r
+         $sum = 105; // 'C' type;\r
+         $m = 0;\r
+         for ($i=0;$i<$len;$i+=2) {\r
+          $m++;\r
+          $sum +=  $this->GetCharIndex($this->mValue[$i].$this->mValue[$i+1]) * $m;\r
+        }\r
+        $check  = $sum % 103;\r
+        return $this->mCharSet[$check];\r
+    }\r
+   \r
+   function DrawStart($DrawPos, $yPos, $ySize, $xres)\r
+   {  /* Start code is '211232' */\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('2', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('2', $xres);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('1', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $DrawPos += $this->GetBarSize('2', $xres);\r
+          $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('3', $xres) , $ySize);\r
+          $DrawPos += $this->GetBarSize('3', $xres);\r
+          $DrawPos += $this->GetBarSize('2', $xres);\r
+         return $DrawPos;\r
+   }\r
+   \r
+   function DrawStop($DrawPos, $yPos, $ySize, $xres)\r
+   {  /* Stop code is '2331112' */\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('2', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('2', $xres);\r
+         $DrawPos += $this->GetBarSize('3', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('3', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('3', $xres);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('1', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $DrawPos += $this->GetBarSize('1', $xres);\r
+         $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize('2', $xres) , $ySize);\r
+         $DrawPos += $this->GetBarSize('2', $xres);\r
+         return $DrawPos;\r
+   }\r
+       \r
+   function DrawCheckChar($DrawPos, $yPos, $ySize, $xres)\r
+   {\r
+     $cset = $this->GetCheckCharValue();\r
+        $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[0], $xres) , $ySize);\r
+        $DrawPos += $this->GetBarSize($cset[0], $xres);\r
+        $DrawPos += $this->GetBarSize($cset[1], $xres);\r
+        $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[2], $xres) , $ySize);\r
+        $DrawPos += $this->GetBarSize($cset[2], $xres);\r
+        $DrawPos += $this->GetBarSize($cset[3], $xres);\r
+        $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[4], $xres) , $ySize);\r
+        $DrawPos += $this->GetBarSize($cset[4], $xres);\r
+        $DrawPos += $this->GetBarSize($cset[5], $xres); \r
+        return $DrawPos;\r
+    }\r
+        \r
+    function DrawObject ($xres)\r
+    {\r
+     $len = strlen($this->mValue);               \r
+        if (($size = $this->GetSize($xres))==0) {\r
+       __DEBUG__("GetSize: failed");\r
+           return false;\r
+        }    \r
+                 \r
+        if ($this->mStyle & BCS_ALIGN_CENTER) $sPos = (integer)(($this->mWidth - $size ) / 2);\r
+        else if ($this->mStyle & BCS_ALIGN_RIGHT) $sPos = $this->mWidth - $size;\r
+                 else $sPos = 0;               \r
+                                                               \r
+        /* Total height of bar code -Bars only- */                                     \r
+        if ($this->mStyle & BCS_DRAW_TEXT) $ysize = $this->mHeight - BCD_DEFAULT_MAR_Y1 - BCD_DEFAULT_MAR_Y2 - $this->GetFontHeight($this->mFont);\r
+        else $ysize = $this->mHeight - BCD_DEFAULT_MAR_Y1 - BCD_DEFAULT_MAR_Y2;\r
+                                                                                \r
+        /* Draw text */ \r
+        if ($this->mStyle & BCS_DRAW_TEXT) {\r
+                if ($this->mStyle & BCS_STRETCH_TEXT) {\r
+                       for ($i=0;$i<$len;$i++) {\r
+                               $this->DrawChar($this->mFont, $sPos+(2*BCD_C128_BAR_2*$xres + 3*BCD_C128_BAR_1*$xres + BCD_C128_BAR_4*$xres)+($size/$len)*$i,\r
+                                                $ysize + BCD_DEFAULT_MAR_Y1 + BCD_DEFAULT_TEXT_OFFSET, $this->mValue[$i]);\r
+                               }                                                                                  \r
+                } else {/* Center */\r
+                        $text_width = $this->GetFontWidth($this->mFont) * strlen($this->mValue);\r
+                        $this->DrawText($this->mFont, $sPos+(($size-$text_width)/2)+(2*BCD_C128_BAR_2*$xres + 3*BCD_C128_BAR_1*$xres + BCD_C128_BAR_4*$xres),\r
+                                                         $ysize + BCD_DEFAULT_MAR_Y1 + BCD_DEFAULT_TEXT_OFFSET, $this->mValue);\r
+             }  \r
+         }                      \r
+                                                                       \r
+        $cPos = 0;                                      \r
+        $DrawPos = $this->DrawStart($sPos, BCD_DEFAULT_MAR_Y1 , $ysize, $xres); \r
+        do {                                \r
+        $c     = $this->GetCharIndex($this->mValue[$cPos].$this->mValue[$cPos+1]);\r
+               $cset  = $this->mCharSet[$c];                   \r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[0], $xres) , $ysize);\r
+           $DrawPos += $this->GetBarSize($cset[0], $xres);\r
+           $DrawPos += $this->GetBarSize($cset[1], $xres);\r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[2], $xres) , $ysize);\r
+           $DrawPos += $this->GetBarSize($cset[2], $xres);\r
+           $DrawPos += $this->GetBarSize($cset[3], $xres);\r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, $this->GetBarSize($cset[4], $xres) , $ysize);\r
+           $DrawPos += $this->GetBarSize($cset[4], $xres);\r
+           $DrawPos += $this->GetBarSize($cset[5], $xres);\r
+                $cPos += 2; \r
+         } while ($cPos<$len);\r
+         $DrawPos = $this->DrawCheckChar($DrawPos, BCD_DEFAULT_MAR_Y1 , $ysize, $xres);\r
+         $DrawPos =  $this->DrawStop($DrawPos, BCD_DEFAULT_MAR_Y1 , $ysize, $xres);\r
+         return true;\r
+        }\r
+  }\r
+?>\r
diff --git a/Open-ILS/web/staff/php/barcode/c39object.php b/Open-ILS/web/staff/php/barcode/c39object.php
new file mode 100755 (executable)
index 0000000..726f656
--- /dev/null
@@ -0,0 +1,228 @@
+<?php\r
+/*\r
+Barcode Render Class for PHP using the GD graphics library \r
+Copyright (C) 2001  Karim Mribti\r
+                                                               \r
+   Version  0.0.7a  2001-04-01  \r
+                                                               \r
+This library is free software; you can redistribute it and/or\r
+modify it under the terms of the GNU General Public\r
+License as published by the Free Software Foundation; either\r
+version 2.1 of the License, or (at your option) any later version.\r
+                                                                                                                                 \r
+This library is distributed in the hope that it will be useful,\r
+but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
+General Public License for more details.\r
+                                                                                          \r
+You should have received a copy of the GNU General Public\r
+License along with this library; if not, write to the Free Software\r
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
+                                                                                                                                                \r
+Copy of GNU General Public License at: http://www.gnu.org/copyleft/gpl.txt\r
+                                                                                                        \r
+Source code home page: http://www.mribti.com/barcode/\r
+Contact author at: barcode@mribti.com\r
+*/\r
+  \r
+  /* \r
+    Render for Code 39    \r
+       Code 39 is an alphanumeric bar code that can encode decimal number, case alphabet and some special symbols.\r
+  */\r
+    \r
+    \r
+  class C39Object extends BarcodeObject {\r
+   var $mCharSet, $mChars;\r
+   function C39Object($Width, $Height, $Style, $Value)\r
+   {\r
+     $this->BarcodeObject($Width, $Height, $Style);\r
+        $this->mValue   = $Value;\r
+        $this->mChars   = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. *$/+%";\r
+        $this->mCharSet = array\r
+         (\r
+               /* 0  */ "000110100",\r
+               /* 1  */ "100100001",\r
+        /* 2  */ "001100001",\r
+           /* 3  */ "101100000",\r
+               /* 4  */ "000110001",\r
+               /* 5  */ "100110000",\r
+               /* 6  */ "001110000",\r
+           /* 7  */ "000100101",\r
+               /* 8  */ "100100100",\r
+               /* 9  */ "001100100",\r
+               /* A  */ "100001001",\r
+           /* B  */ "001001001",\r
+           /* C  */ "101001000",\r
+               /* D  */ "000011001",\r
+               /* E  */ "100011000",\r
+               /* F  */ "001011000",\r
+           /* G  */ "000001101",\r
+               /* H  */ "100001100",\r
+               /* I  */ "001001100",\r
+           /* J  */ "000011100",\r
+               /* K  */ "100000011",\r
+               /* L  */ "001000011",\r
+               /* M  */ "101000010",\r
+               /* N  */ "000010011",\r
+               /* O  */ "100010010",\r
+               /* P  */ "001010010",\r
+               /* Q  */ "000000111",\r
+               /* R  */ "100000110",\r
+               /* S  */ "001000110",\r
+               /* T  */ "000010110",\r
+               /* U  */ "110000001",\r
+               /* V  */ "011000001",\r
+               /* W  */ "111000000",\r
+               /* X  */ "010010001",\r
+               /* Y  */ "110010000",\r
+               /* Z  */ "011010000",\r
+               /* -  */ "010000101",\r
+               /* .  */ "110000100",\r
+               /* SP */ "011000100",\r
+               /* *  */ "010010100",\r
+               /* $  */ "010101000",\r
+               /* /  */ "010100010",\r
+               /* +  */ "010001010",\r
+               /* %  */ "000101010" \r
+       );\r
+   }\r
+   \r
+   function GetCharIndex ($char)\r
+   {\r
+    for ($i=0;$i<44;$i++) {\r
+         if ($this->mChars[$i] == $char)\r
+            return $i;\r
+        }\r
+        return -1;\r
+   }\r
+    \r
+   function GetSize($xres)\r
+   {\r
+     $len = strlen($this->mValue);\r
+        \r
+        if ($len == 0)  {\r
+          $this->mError = "Null value";\r
+          __DEBUG__("GetRealSize: null barcode value");\r
+          return false;\r
+          }\r
+        \r
+        for ($i=0;$i<$len;$i++) {\r
+         if ($this->GetCharIndex($this->mValue[$i]) == -1 || $this->mValue[$i] == '*') {\r
+                       /* The asterisk is only used as a start and stop code */\r
+               $this->mError = "C39 not include the char '".$this->mValue[$i]."'";\r
+                       return false;\r
+                }\r
+        }\r
+        \r
+        /* Start, Stop is 010010100 == '*'  */\r
+        $StartSize = BCD_C39_NARROW_BAR * $xres * 6 + BCD_C39_WIDE_BAR * $xres * 3;\r
+        $StopSize  = BCD_C39_NARROW_BAR * $xres * 6 + BCD_C39_WIDE_BAR * $xres * 3;\r
+        $CharSize  = BCD_C39_NARROW_BAR * $xres * 6 + BCD_C39_WIDE_BAR * $xres * 3; /* Same for all chars */\r
+         \r
+         return $CharSize * $len + $StarSize + $StopSize + /* Space between chars */ BCD_C39_NARROW_BAR * $xres * ($len-1);\r
+   }\r
+   \r
+   function DrawStart($DrawPos, $yPos, $ySize, $xres)\r
+   {  /* Start code is '*' */\r
+      $narrow = BCD_C39_NARROW_BAR * $xres;\r
+         $wide   = BCD_C39_WIDE_BAR * $xres;\r
+         $this->DrawSingleBar($DrawPos, $yPos, $narrow , $ySize);\r
+         $DrawPos += $narrow;\r
+         $DrawPos += $wide;\r
+         $this->DrawSingleBar($DrawPos, $yPos, $narrow , $ySize);\r
+         $DrawPos += $narrow;\r
+         $DrawPos += $narrow;\r
+         $this->DrawSingleBar($DrawPos, $yPos, $wide , $ySize);\r
+         $DrawPos += $wide;\r
+         $DrawPos += $narrow;\r
+         $this->DrawSingleBar($DrawPos, $yPos, $wide , $ySize);\r
+         $DrawPos += $wide;\r
+         $DrawPos += $narrow;\r
+         $this->DrawSingleBar($DrawPos, $yPos, $narrow, $ySize);\r
+         $DrawPos += $narrow;\r
+         $DrawPos += $narrow; /* Space between chars */\r
+         return $DrawPos;\r
+   }\r
+   \r
+   function DrawStop($DrawPos, $yPos, $ySize, $xres)\r
+   {  /* Stop code is '*' */\r
+      $narrow = BCD_C39_NARROW_BAR * $xres;\r
+         $wide   = BCD_C39_WIDE_BAR * $xres;\r
+         $this->DrawSingleBar($DrawPos, $yPos, $narrow , $ySize);\r
+         $DrawPos += $narrow;\r
+         $DrawPos += $wide;\r
+         $this->DrawSingleBar($DrawPos, $yPos, $narrow , $ySize);\r
+         $DrawPos += $narrow;\r
+         $DrawPos += $narrow;\r
+         $this->DrawSingleBar($DrawPos, $yPos, $wide , $ySize);\r
+         $DrawPos += $wide;\r
+         $DrawPos += $narrow;\r
+         $this->DrawSingleBar($DrawPos, $yPos, $wide , $ySize);\r
+         $DrawPos += $wide;\r
+         $DrawPos += $narrow;\r
+         $this->DrawSingleBar($DrawPos, $yPos, $narrow, $ySize);\r
+         $DrawPos += $narrow;\r
+         return $DrawPos;\r
+   }\r
+   \r
+   function DrawObject ($xres)\r
+   {\r
+     $len = strlen($this->mValue);\r
+                                                                 \r
+        $narrow = BCD_C39_NARROW_BAR * $xres;\r
+        $wide   = BCD_C39_WIDE_BAR * $xres; \r
+                                                                                \r
+        if (($size = $this->GetSize($xres))==0) {\r
+       __DEBUG__("GetSize: failed");\r
+           return false;\r
+        }    \r
+                 \r
+        $cPos = 0;\r
+        if ($this->mStyle & BCS_ALIGN_CENTER) $sPos = (integer)(($this->mWidth - $size ) / 2);\r
+        else if ($this->mStyle & BCS_ALIGN_RIGHT) $sPos = $this->mWidth - $size;\r
+                 else $sPos = 0;               \r
+                                                               \r
+        /* Total height of bar code -Bars only- */                                     \r
+        if ($this->mStyle & BCS_DRAW_TEXT) $ysize = $this->mHeight - BCD_DEFAULT_MAR_Y1 - BCD_DEFAULT_MAR_Y2 - $this->GetFontHeight($this->mFont);\r
+        else $ysize = $this->mHeight - BCD_DEFAULT_MAR_Y1 - BCD_DEFAULT_MAR_Y2;\r
+                                                                                \r
+        /* Draw text */ \r
+        if ($this->mStyle & BCS_DRAW_TEXT) {\r
+                if ($this->mStyle & BCS_STRETCH_TEXT) {\r
+                       for ($i=0;$i<$len;$i++) {\r
+                               $this->DrawChar($this->mFont, $sPos+($narrow*6+$wide*3)+($size/$len)*$i,\r
+                                                $ysize + BCD_DEFAULT_MAR_Y1 + BCD_DEFAULT_TEXT_OFFSET, $this->mValue[$i]);\r
+                               }                                                                                  \r
+                } else {/* Center */\r
+                        $text_width = $this->GetFontWidth($this->mFont) * strlen($this->mValue);\r
+                        $this->DrawText($this->mFont, $sPos+(($size-$text_width)/2)+($narrow*6+$wide*3),\r
+                                                         $ysize + BCD_DEFAULT_MAR_Y1 + BCD_DEFAULT_TEXT_OFFSET, $this->mValue);\r
+             }  \r
+         }                      \r
+                                        \r
+        $DrawPos = $this->DrawStart($sPos, BCD_DEFAULT_MAR_Y1 , $ysize, $xres); \r
+        do {                                \r
+               $c     = $this->GetCharIndex($this->mValue[$cPos]);\r
+               $cset  = $this->mCharSet[$c];                   \r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, ($cset[0] == '0') ? $narrow : $wide , $ysize);\r
+           $DrawPos += ($cset[0] == '0') ? $narrow : $wide;\r
+           $DrawPos += ($cset[1] == '0') ? $narrow : $wide;\r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, ($cset[2] == '0') ? $narrow : $wide , $ysize);\r
+           $DrawPos += ($cset[2] == '0') ? $narrow : $wide;\r
+           $DrawPos += ($cset[3] == '0') ? $narrow : $wide;\r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, ($cset[4] == '0') ? $narrow : $wide , $ysize);\r
+           $DrawPos += ($cset[4] == '0') ? $narrow : $wide;\r
+           $DrawPos += ($cset[5] == '0') ? $narrow : $wide;\r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, ($cset[6] == '0') ? $narrow : $wide , $ysize);\r
+           $DrawPos += ($cset[6] == '0') ? $narrow : $wide;\r
+           $DrawPos += ($cset[7] == '0') ? $narrow : $wide;\r
+           $this->DrawSingleBar($DrawPos, BCD_DEFAULT_MAR_Y1, ($cset[8] == '0') ? $narrow : $wide , $ysize);\r
+           $DrawPos += ($cset[8] == '0') ? $narrow : $wide;\r
+           $DrawPos += $narrow; /* Space between chars */\r
+               $cPos++; \r
+         } while ($cPos<$len);\r
+         $DrawPos =  $this->DrawStop($DrawPos, BCD_DEFAULT_MAR_Y1 , $ysize, $xres);\r
+         return true;\r
+        }\r
+  }\r
+?>\r
diff --git a/Open-ILS/web/staff/php/barcode/debug.php b/Open-ILS/web/staff/php/barcode/debug.php
new file mode 100755 (executable)
index 0000000..3337a9c
--- /dev/null
@@ -0,0 +1,64 @@
+<?\r
+/*\r
+Barcode Render Class for PHP using the GD graphics library \r
+Copyright (C) 2001  Karim Mribti\r
+                                                               \r
+   Version  0.0.7a  2001-04-01  \r
+                                                               \r
+This library is free software; you can redistribute it and/or\r
+modify it under the terms of the GNU General Public\r
+License as published by the Free Software Foundation; either\r
+version 2.1 of the License, or (at your option) any later version.\r
+                                                                                                                                 \r
+This library is distributed in the hope that it will be useful,\r
+but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
+General Public License for more details.\r
+                                                                                          \r
+You should have received a copy of the GNU General Public\r
+License along with this library; if not, write to the Free Software\r
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
+                                                                                                                                                \r
+Copy of GNU General Public License at: http://www.gnu.org/copyleft/gpl.txt\r
+                                                                                                        \r
+Source code home page: http://www.mribti.com/barcode/\r
+Contact author at: barcode@mribti.com\r
+*/\r
+\r
+define(__DEBUG_HOST__, "localhost");\r
+define(__DEBUG_PORT__, "9999");\r
+\r
+define(__TRACE_HOST__, "localhost");\r
+define(__TRACE_PORT__, "9999");\r
+\r
+define(__TIMEOUT__, 3);\r
+                                                  \r
+function __TRACE__ ($str) {\r
+if (__TRACE_ENABLED__) {\r
+       $errno  = 0;\r
+       $errstr = "no error";\r
+       \r
+       $fp = @fsockopen(__TRACE_HOST__, __TRACE_PORT__, &$errno, &$errstr, __TIMEOUT__);\r
+       \r
+       if ($fp)\r
+       {\r
+          @fputs($fp, $str);\r
+          @fclose($fp);\r
+       }\r
+ }\r
+}      \r
+       \r
+function __DEBUG__ ($str) {\r
+if (__DEBUG_ENABLED__) {\r
+       $errno  = 0;\r
+       $errstr = "no error";\r
+       \r
+       $fp = @fsockopen(__DEBUG_HOST__, __DEGUB_PORT__, &$errno, &$errstr, __TIMEOUT__);\r
+       \r
+       if ($fp)\r
+       {\r
+          @fputs($fp, $str);\r
+          @fclose($fp);\r
+       } \r
+ }\r
+}\r
diff --git a/Open-ILS/web/staff/php/barcode/download.php b/Open-ILS/web/staff/php/barcode/download.php
new file mode 100755 (executable)
index 0000000..7b29cc1
--- /dev/null
@@ -0,0 +1,75 @@
+<html>\r
+<head>\r
+       <title>Barcode Download</title>\r
+</head>\r
+<body bgcolor="#FFFFCC">\r
+<table align='center'>\r
+ <tr>\r
+  <td><a href="home.php"><img src="home.png" border="0"></a></td>\r
+  <td><a href="sample.php"><img src="sample.png" border="0"></a></td>\r
+  <td><img src="download.png" border="1"></td>\r
+ </tr>\r
+</table>\r
+<br><br>\r
+<table align="center" width="640">\r
+ <tr><td> This is alpha release, report any problem to <a href="mailto:barcode@mribti.com">barcode@mribti.com</a></td></tr>\r
+ <tr><td><br></td></tr>\r
+ <tr><td align="left">Main site</td></tr>\r
+ <tr><td align="left"><a href="barcode-0.0.8a.tar.gz">barcode-0.0.8a.tar.gz(45kb)</a></td></tr>\r
+ <tr><td align="left"><a href="barcode-0.0.8a.zip">barcode-0.0.8a.zip(47kb)</a></td></tr>\r
+ <tr><td align="left">Mirror site (fast)</td></tr>\r
+ <tr><td align="left"><a href="http://www.aramsoft.com/barcode/barcode-0.0.8a.tar.gz">barcode-0.0.8a.tar.gz(45kb)</a></td></tr>\r
+ <tr><td align="left"><a href="http://www.aramsoft.com/barcode/barcode-0.0.8a.zip">barcode-0.0.8a.zip(47kb)</a></td></tr>\r
+ <tr><td><br></td></tr>\r
+</table>\r
+<br><br>\r
+<table align="center" width="640">\r
+ <tr>\r
+   <td colspan="3" align="center">CHANGES LOG</td>\r
+ </tr>\r
+ <tr>\r
+  <td colspan="3"><br></td>\r
+ </tr>\r
+ <tr>\r
+  <td width="20%" align="left">2001-03-25</td>\r
+  <td width="20%" align="left">v0.0.1a</td>\r
+  <td width="60%">Initial release.</td>\r
+ </tr>\r
+ <tr>\r
+  <td width="20%" align="left">2001-03-26</td>\r
+  <td width="20%" align="left">v0.0.2a</td>\r
+  <td width="60%">Error checking has been added, and there are minor bugfixes</td>\r
+ </tr>\r
+ <tr>\r
+  <td width="20%" align="left">2001-03-26</td>\r
+  <td width="20%" align="left">v0.0.3a</td>\r
+  <td width="60%">Add Code 39 support</td>\r
+ </tr>\r
+ <tr>\r
+  <td width="20%" align="left">2001-03-27</td>\r
+  <td width="20%" align="left">v0.0.4a</td>\r
+  <td width="60%">Minor feature enhancements, new output styles. </td>\r
+ </tr>\r
+ <tr>\r
+  <td width="20%" align="left">2001-03-28</td>\r
+  <td width="20%" align="left">v0.0.5a</td>\r
+  <td width="60%">Add font control, minor bugfixes. </td>\r
+ </tr>\r
+ <tr>\r
+  <td width="20%" align="left">2001-03-29</td>\r
+  <td width="20%" align="left">v0.0.6a</td>\r
+  <td width="60%">Bugfix in Code 39 render, thanks to Henry Bland. </td>\r
+ </tr>\r
+ <tr>\r
+  <td width="20%" align="left">2001-04-01</td>\r
+  <td width="20%" align="left">v0.0.7a</td>\r
+  <td width="60%">Add support for Code 128-A and Code 128-B</td>\r
+ </tr>\r
+ <tr>\r
+  <td width="20%" align="left">2001-08-03</td>\r
+  <td width="20%" align="left">v0.0.8a</td>\r
+  <td width="60%">Now support Code 128-C, thanks to Sam Michaels. </td>\r
+ </tr>\r
+</table>\r
+</body>\r
+</html>\r
diff --git a/Open-ILS/web/staff/php/barcode/download.png b/Open-ILS/web/staff/php/barcode/download.png
new file mode 100755 (executable)
index 0000000..ace23c4
Binary files /dev/null and b/Open-ILS/web/staff/php/barcode/download.png differ
diff --git a/Open-ILS/web/staff/php/barcode/home.php b/Open-ILS/web/staff/php/barcode/home.php
new file mode 100755 (executable)
index 0000000..40636c5
--- /dev/null
@@ -0,0 +1,30 @@
+<html>\r
+<head>\r
+       <title>Barcode home page</title>\r
+</head>\r
+<body bgcolor="#FFFFCC">\r
+<table align='center'>\r
+ <tr>\r
+  <td><img src="home.png" border="1"></td>\r
+  <td><a href="sample.php"><img src="sample.png" border="0"></a></td>\r
+  <td><a href="download.php"><img src="download.png" border="0"></a></td>\r
+ </tr>\r
+</table>\r
+<br><br>\r
+<table align="center" width="640">\r
+ <tr><td> Barcode is a small implementation of a barcode rendering class using the <a target="_blank" href="http://www.php.net">PHP</a> language and <a target="_blank" href="http://www.boutell.com/gd/">GD graphics library</a>.</td></tr>\r
+ <tr><td><br></td></tr>\r
+ <tr><td><br></td></tr>\r
+ <tr><td>For any question, please send an email to <a href="mailto:barcode@mribti.com">barcode@mribti.com</a></td></tr>\r
+</table>\r
+<br><br>\r
+<table align="center">\r
+<tr>\r
+ <td align="center"><A href="http://sourceforge.net"><IMG src="http://sourceforge.net/sflogo.php?group_id=25228" width="88" height="31" border="0" alt="SourceForge Logo"></A></td>
+</tr>\r
+<tr>\r
+ <td></td>\r
+</tr>\r
+</table>\r
+</body>\r
+</html>
diff --git a/Open-ILS/web/staff/php/barcode/home.png b/Open-ILS/web/staff/php/barcode/home.png
new file mode 100755 (executable)
index 0000000..2f2fcbe
Binary files /dev/null and b/Open-ILS/web/staff/php/barcode/home.png differ
diff --git a/Open-ILS/web/staff/php/barcode/i25object.php b/Open-ILS/web/staff/php/barcode/i25object.php
new file mode 100755 (executable)
index 0000000..69a4fe7
--- /dev/null
@@ -0,0 +1,169 @@
+<?php\r
+/*\r
+Barcode Render Class for PHP using the GD graphics library \r
+Copyright (C) 2001  Karim Mribti\r
+                                                               \r
+   Version  0.0.7a  2001-04-01  \r
+                                                               \r
+This library is free software; you can redistribute it and/or\r
+modify it under the terms of the GNU General Public\r
+License as published by the Free Software Foundation; either\r
+version 2.1 of the License, or (at your option) any later version.\r
+                                                                                                                                 \r
+This library is distributed in the hope that it will be useful,\r
+but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
+General Public License for more details.\r
+                                                                                          \r
+You should have received a copy of the GNU General Public\r
+License along with this library; if not, write to the Free Software\r
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
+                                                                                                                                                \r
+Copy of GNU General Public License at: http://www.gnu.org/copyleft/gpl.txt\r
+                                                                                                        \r
+Source code home page: http://www.mribti.com/barcode/\r
+Contact author at: barcode@mribti.com\r
+*/\r
+  \r
+  /* \r
+    render for Interleaved 2 of 5     \r
+       Interleaved 2 of 5 is a numeric only bar code with a optional check number.\r
+  */\r
+    \r
+  class I25Object extends BarcodeObject {\r
+   var $mCharSet;\r
+   function I25Object($Width, $Height, $Style, $Value)\r
+   {\r
+     $this->BarcodeObject($Width, $Height, $Style);\r
+        $this->mValue   = $Value;\r
+        $this->mCharSet = array\r
+        (                                              \r
+          /* 0 */ "00110",\r
+          /* 1 */ "10001",\r
+          /* 2 */ "01001",\r
+          /* 3 */ "11000",\r
+          /* 4 */ "00101",\r
+          /* 5 */ "10100",\r
+          /* 6 */ "01100",\r
+          /* 7 */ "00011",\r
+          /* 8 */ "10010",\r
+          /* 9 */ "01010" \r
+        );\r
+   }\r
+   \r
+   function GetSize($xres)\r
+   {\r
+     $len = strlen($this->mValue);\r
+        \r
+        if ($len == 0)  {\r
+          $this->mError = "Null value";\r
+          __DEBUG__("GetRealSize: null barcode value");\r
+          return false;\r
+          }\r
+        \r
+        for ($i=0;$i<$len;$i++) {\r
+         if ((ord($this->mValue[$i])<48) || (ord($this->mValue[$i])>57)) {\r
+               $this->mError = "I25 is numeric only";\r
+                       return false;\r
+                }\r
+        }\r
+        \r
+        if (($len%2) != 0) {\r
+           $this->mError = "The length of barcode value must be even";\r
+           __DEBUG__("GetSize: failed I25 requiremente");\r
+               return false;\r
+               }                \r
+        $StartSize = BCD_I25_NARROW_BAR * 4  * $xres;\r
+        $StopSize  = BCD_I25_WIDE_BAR * $xres + 2 * BCD_I25_NARROW_BAR * $xres;\r
+        $cPos = 0;\r
+        $sPos = 0;\r
+        do {      \r
+               $c1    = $this->mValue[$cPos];\r
+               $c2    = $this->mValue[$cPos+1];\r
+               $cset1 = $this->mCharSet[$c1];\r
+               $cset2 = $this->mCharSet[$c2];\r
+               \r
+               for ($i=0;$i<5;$i++) {\r
+                 $type1 = ($cset1[$i]==0) ? (BCD_I25_NARROW_BAR  * $xres) : (BCD_I25_WIDE_BAR * $xres);\r
+                 $type2 = ($cset2[$i]==0) ? (BCD_I25_NARROW_BAR  * $xres) : (BCD_I25_WIDE_BAR * $xres);\r
+                 $sPos += ($type1 + $type2);\r
+               }\r
+               $cPos+=2;\r
+         } while ($cPos<$len);\r
+         \r
+         return $sPos + $StarSize + $StopSize;\r
+   }\r
+   \r
+   function DrawStart($DrawPos, $yPos, $ySize, $xres)\r
+   {  /* Start code is "0000" */\r
+         $this->DrawSingleBar($DrawPos, $yPos, BCD_I25_NARROW_BAR  * $xres , $ySize);\r
+         $DrawPos += BCD_I25_NARROW_BAR  * $xres;\r
+         $DrawPos += BCD_I25_NARROW_BAR  * $xres;\r
+         $this->DrawSingleBar($DrawPos, $yPos, BCD_I25_NARROW_BAR  * $xres , $ySize);\r
+         $DrawPos += BCD_I25_NARROW_BAR  * $xres;\r
+         $DrawPos += BCD_I25_NARROW_BAR  * $xres;\r
+         return $DrawPos;\r
+   }\r
+   \r
+   function DrawStop($DrawPos, $yPos, $ySize, $xres)\r
+   {  /* Stop code is "100" */\r
+         $this->DrawSingleBar($DrawPos, $yPos, BCD_I25_WIDE_BAR * $xres , $ySize);\r
+         $DrawPos += BCD_I25_WIDE_BAR  * $xres;\r
+         $DrawPos += BCD_I25_NARROW_BAR  * $xres;\r
+         $this->DrawSingleBar($DrawPos, $yPos, BCD_I25_NARROW_BAR  * $xres , $ySize);\r
+         $DrawPos += BCD_I25_NARROW_BAR  * $xres; \r
+         return $DrawPos;\r
+   }\r
+   \r
+   function DrawObject ($xres)\r
+   {\r
+     $len = strlen($this->mValue);\r
+                                                                 \r
+        if (($size = $this->GetSize($xres))==0) {\r
+       __DEBUG__("GetSize: failed");\r
+           return false;\r
+        }    \r
+                 \r
+        $cPos  = 0;\r
+                               \r
+        if ($this->mStyle & BCS_DRAW_TEXT) $ysize = $this->mHeight - BCD_DEFAULT_MAR_Y1 - BCD_DEFAULT_MAR_Y2 - $this->GetFontHeight($this->mFont);\r
+        else $ysize = $this->mHeight - BCD_DEFAULT_MAR_Y1 - BCD_DEFAULT_MAR_Y2;\r
+                                                                                                                                                       \r
+        if ($this->mStyle & BCS_ALIGN_CENTER) $sPos = (integer)(($this->mWidth - $size ) / 2);\r
+        else if ($this->mStyle & BCS_ALIGN_RIGHT) $sPos = $this->mWidth - $size;\r
+                 else $sPos = 0;\r
+                                                \r
+         if ($this->mStyle & BCS_DRAW_TEXT) {\r
+                if ($this->mStyle & BCS_STRETCH_TEXT) {\r
+                  /* Stretch */\r
+              for ($i=0;$i<$len;$i++) {\r
+                         $this->DrawChar($this->mFont, $sPos+BCD_I25_NARROW_BAR*4*$xres+($size/$len)*$i, \r
+                                        $ysize + BCD_DEFAULT_MAR_Y1 + BCD_DEFAULT_TEXT_OFFSET , $this->mValue[$i]);\r
+                }                               \r
+            }else {/* Center */\r
+                        $text_width = $this->GetFontWidth($this->mFont) * strlen($this->mValue);\r
+                        $this->DrawText($this->mFont, $sPos+(($size-$text_width)/2)+(BCD_I25_NARROW_BAR*4*$xres),\r
+                                                         $ysize + BCD_DEFAULT_MAR_Y1 + BCD_DEFAULT_TEXT_OFFSET, $this->mValue);\r
+             }                                  \r
+        }                                               \r
+                                        \r
+        $sPos = $this->DrawStart($sPos, BCD_DEFAULT_MAR_Y1, $ysize, $xres); \r
+        do {                                                     \r
+               $c1    = $this->mValue[$cPos];\r
+               $c2    = $this->mValue[$cPos+1];\r
+               $cset1 = $this->mCharSet[$c1];\r
+               $cset2 = $this->mCharSet[$c2];\r
+                                                         \r
+               for ($i=0;$i<5;$i++) {\r
+                 $type1 = ($cset1[$i]==0) ? (BCD_I25_NARROW_BAR * $xres) : (BCD_I25_WIDE_BAR * $xres);\r
+                 $type2 = ($cset2[$i]==0) ? (BCD_I25_NARROW_BAR * $xres) : (BCD_I25_WIDE_BAR * $xres);\r
+                 $this->DrawSingleBar($sPos, BCD_DEFAULT_MAR_Y1, $type1 , $ysize);\r
+                 $sPos += ($type1 + $type2);\r
+               }                \r
+               $cPos+=2;\r
+         } while ($cPos<$len);\r
+         $sPos =  $this->DrawStop($sPos, BCD_DEFAULT_MAR_Y1, $ysize, $xres);\r
+         return true;\r
+        }\r
+  }\r
+?>\r
diff --git a/Open-ILS/web/staff/php/barcode/image.php b/Open-ILS/web/staff/php/barcode/image.php
new file mode 100755 (executable)
index 0000000..0284195
--- /dev/null
@@ -0,0 +1,73 @@
+<?php\r
+/*\r
+Barcode Render Class for PHP using the GD graphics library \r
+Copyright (C) 2001  Karim Mribti\r
+                                                               \r
+   Version  0.0.7a  2001-04-01  \r
+                                                               \r
+This library is free software; you can redistribute it and/or\r
+modify it under the terms of the GNU General Public\r
+License as published by the Free Software Foundation; either\r
+version 2.1 of the License, or (at your option) any later version.\r
+                                                                                                                                 \r
+This library is distributed in the hope that it will be useful,\r
+but WITHOUT ANY WARRANTY; without even the implied warranty of\r
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
+General Public License for more details.\r
+                                                                                          \r
+You should have received a copy of the GNU General Public\r
+License along with this library; if not, write to the Free Software\r
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA\r
+                                                                                                                                                \r
+Copy of GNU General Public License at: http://www.gnu.org/copyleft/gpl.txt\r
+                                                                                                        \r
+Source code home page: http://www.mribti.com/barcode/\r
+Contact author at: barcode@mribti.com\r
+*/\r
+  \r
+  define (__TRACE_ENABLED__, false);\r
+  define (__DEBUG_ENABLED__, false);\r
+  \r
+  require("barcode.php");  \r
+  require("i25object.php");\r
+  require("c39object.php");\r
+  require("c128aobject.php");\r
+  require("c128bobject.php");\r
+  require("c128cobject.php");\r
+                                  \r
+  if (!isset($style))  $style   = BCD_DEFAULT_STYLE;\r
+  if (!isset($width))  $width   = BCD_DEFAULT_WIDTH;\r
+  if (!isset($height)) $height  = BCD_DEFAULT_HEIGHT;\r
+  if (!isset($xres))   $xres    = BCD_DEFAULT_XRES;\r
+  if (!isset($font))   $font    = BCD_DEFAULT_FONT;\r
+                           \r
+  switch ($type)\r
+  {\r
+    case "I25":\r
+                         $obj = new I25Object($width, $height, $style, $code);\r
+                         break;\r
+    case "C39":\r
+                         $obj = new C39Object($width, $height, $style, $code);\r
+                         break;\r
+    case "C128A":\r
+                         $obj = new C128AObject($width, $height, $style, $code);\r
+                         break;\r
+    case "C128B":\r
+                         $obj = new C128BObject($width, $height, $style, $code);\r
+                         break;\r
+    case "C128C":\r
+              $obj = new C128CObject($width, $height, $style, $code);\r
+                         break;\r
+       default:\r
+                       echo "Need bar code type ex. C39";\r
+                       $obj = false;\r
+  }\r
+   \r
+  if ($obj) {\r
+      $obj->SetFont($font);   \r
+      $obj->DrawObject($xres);\r
+         $obj->FlushObject();\r
+         $obj->DestroyObject();\r
+         unset($obj);  /* clean */\r
+  }\r
+?>\r
diff --git a/Open-ILS/web/staff/php/barcode/image.png b/Open-ILS/web/staff/php/barcode/image.png
new file mode 100755 (executable)
index 0000000..36c4fab
Binary files /dev/null and b/Open-ILS/web/staff/php/barcode/image.png differ
diff --git a/Open-ILS/web/staff/php/barcode/index.php b/Open-ILS/web/staff/php/barcode/index.php
new file mode 100755 (executable)
index 0000000..0e8f4db
--- /dev/null
@@ -0,0 +1,3 @@
+<?\r
+include("home.php");\r
+?>
\ No newline at end of file
diff --git a/Open-ILS/web/staff/php/barcode/linux.gif b/Open-ILS/web/staff/php/barcode/linux.gif
new file mode 100755 (executable)
index 0000000..2540ad3
Binary files /dev/null and b/Open-ILS/web/staff/php/barcode/linux.gif differ
diff --git a/Open-ILS/web/staff/php/barcode/php_logo.gif b/Open-ILS/web/staff/php/barcode/php_logo.gif
new file mode 100755 (executable)
index 0000000..7beda43
Binary files /dev/null and b/Open-ILS/web/staff/php/barcode/php_logo.gif differ
diff --git a/Open-ILS/web/staff/php/barcode/sample.php b/Open-ILS/web/staff/php/barcode/sample.php
new file mode 100755 (executable)
index 0000000..f82132c
--- /dev/null
@@ -0,0 +1,139 @@
+<html>\r
+<head>\r
+       <title>Barcode Sample</title>\r
+</head>\r
+<body bgcolor="#FFFFCC">\r
+<table align='center'>\r
+ <tr>\r
+  <td><a href="home.php"><img src="home.png" border="0"></a></td>\r
+  <td><img src="sample.png" border="1"></td>\r
+  <td><a href="download.php"><img src="download.png" border="0"></a></td>\r
+ </tr>\r
+</table>\r
+<br><br>\r
+<? \r
+ define (__TRACE_ENABLED__, false);\r
+ define (__DEBUG_ENABLED__, false);\r
+                                                                  \r
+ require("barcode.php");                  \r
+ require("i25object.php");\r
+ require("c39object.php");\r
+ require("c128aobject.php");\r
+ require("c128bobject.php");\r
+ require("c128cobject.php");\r
+                                                 \r
+/* Default value */\r
+if (!isset($output))  $output   = "png"; \r
+if (!isset($barcode)) $barcode  = "0123456789";\r
+if (!isset($type))    $type     = "I25";\r
+if (!isset($width))   $width    = "460";\r
+if (!isset($height))  $height   = "120";\r
+if (!isset($xres))    $xres     = "2";\r
+if (!isset($font))    $font     = "5";\r
+/*********************************/ \r
+                                                                       \r
+if (isset($barcode) && strlen($barcode)>0) {    \r
+  $style  = BCS_ALIGN_CENTER;                                         \r
+  $style |= ($output  == "png" ) ? BCS_IMAGE_PNG  : 0; \r
+  $style |= ($output  == "jpeg") ? BCS_IMAGE_JPEG : 0; \r
+  $style |= ($border  == "on"  ) ? BCS_BORDER    : 0; \r
+  $style |= ($drawtext== "on"  ) ? BCS_DRAW_TEXT  : 0; \r
+  $style |= ($stretchtext== "on" ) ? BCS_STRETCH_TEXT  : 0; \r
+  $style |= ($negative== "on"  ) ? BCS_REVERSE_COLOR  : 0; \r
+  \r
+  switch ($type)\r
+  {\r
+    case "I25":\r
+                         $obj = new I25Object(250, 120, $style, $barcode);\r
+                         break;\r
+    case "C39":\r
+                         $obj = new C39Object(250, 120, $style, $barcode);\r
+                         break;\r
+    case "C128A":\r
+                         $obj = new C128AObject(250, 120, $style, $barcode);\r
+                         break;\r
+    case "C128B":\r
+                         $obj = new C128BObject(250, 120, $style, $barcode);\r
+                         break;\r
+    case "C128C":\r
+                          $obj = new C128CObject(250, 120, $style, $barcode);\r
+                         break;\r
+       default:\r
+                       $obj = false;\r
+  }\r
+  if ($obj) {\r
+     if ($obj->DrawObject($xres)) {\r
+         echo "<table align='center'><tr><td><img src='./image.php?code=".$barcode."&style=".$style."&type=".$type."&width=".$width."&height=".$height."&xres=".$xres."&font=".$font."'></td></tr></table>";\r
+     } else echo "<table align='center'><tr><td><font color='#FF0000'>".($obj->GetError())."</font></td></tr></table>";\r
+  }\r
+}\r
+?>\r
+<br>\r
+<form method="post" action="sample.php">\r
+<table align="center" border="1" cellpadding="1" cellspacing="1">\r
+ <tr>\r
+  <td bgcolor="#EFEFEF"><b>Type</b></td>\r
+  <td><select name="type" style="WIDTH: 260px" size="1">\r
+               <option value="I25" <?=($type=="I25" ? "selected" : " ")?>>Interleaved 2 of 5\r
+               <option value="C39" <?=($type=="C39" ? "selected" : " ")?>>Code 39\r
+               <option value="C128A" <?=($type=="C128A" ? "selected" : " ")?>>Code 128-A\r
+               <option value="C128B" <?=($type=="C128B" ? "selected" : " ")?>>Code 128-B\r
+        <option value="C128C" <?=($type=="C128C" ? "selected" : " ")?>>Code 128-C</select></td>\r
+ </tr>\r
+ <tr>\r
+  <td bgcolor="#EFEFEF"><b>Output</b></td>\r
+  <td><select name="output" style="WIDTH: 260px" size="1">\r
+               <option value="png" <?=($output=="png" ? "selected" : " ")?>>Portable Network Graphics (PNG)\r
+               <option value="jpeg" <?=($output=="jpeg" ? "selected" : " ")?>>Joint Photographic Experts Group(JPEG)</select></td>\r
+ </tr>\r
+ <tr>\r
+  <td rowspan="4" bgcolor="#EFEFEF"><b>Styles</b></td>\r
+  <td rowspan="1"><input type="Checkbox" name="border" <?=($border=="on" ? "CHECKED" : " ")?>>Draw border</td>\r
+ </tr>\r
+ <tr>\r
+  <td><input type="Checkbox" name="drawtext" <?=($drawtext=="on" ? "CHECKED" : " ")?>>Draw value text</td>\r
+ </tr>\r
+ <tr>\r
+  <td><input type="Checkbox" name="stretchtext" <?=($stretchtext=="on" ? "CHECKED" : " ")?>>Stretch text</td>\r
+ </tr>\r
+ <tr>\r
+  <td><input type="Checkbox" name="negative" <?=($negative=="on" ? "CHECKED" : " ")?>>Negative (White on black)</td>\r
+ </tr>\r
+ <tr>\r
+  <td rowspan="2" bgcolor="#EFEFEF"><b>Size</b></td>\r
+  <td rowspan="1">Width: <input type="text" size="6" maxlength="3" name="width" value="<?=$width?>"></td>\r
+ </tr>\r
+ <tr>\r
+  <td>Height: <input type="text" size="6" maxlength="3" name="height" value="<?=$height?>"></td>\r
+ </tr>\r
+ <tr>\r
+  <td bgcolor="#EFEFEF"><b>Xres</b></td>\r
+  <td>\r
+      <input type="Radio" name="xres" value="1" <?=($xres=="1" ? "CHECKED" : " ")?>>1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\r
+      <input type="Radio" name="xres" value="2" <?=($xres=="2" ? "CHECKED" : " ")?>>2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\r
+      <input type="Radio" name="xres" value="3" <?=($xres=="3" ? "CHECKED" : " ")?>>3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\r
+  </td>\r
+ </tr>\r
+ <tr>\r
+  <td bgcolor="#EFEFEF"><b>Text Font</b></td>\r
+  <td>\r
+      <input type="Radio" name="font" value="1" <?=($font=="1" ? "CHECKED" : " ")?>>1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\r
+      <input type="Radio" name="font" value="2" <?=($font=="2" ? "CHECKED" : " ")?>>2&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\r
+      <input type="Radio" name="font" value="3" <?=($font=="3" ? "CHECKED" : " ")?>>3&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\r
+      <input type="Radio" name="font" value="4" <?=($font=="4" ? "CHECKED" : " ")?>>4&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\r
+      <input type="Radio" name="font" value="5" <?=($font=="5" ? "CHECKED" : " ")?>>5&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;\r
+  </td>\r
+ </tr>\r
+ <tr>\r
+  <td bgcolor="#EFEFEF"><b>Value</b></td>\r
+  <td><input type="Text" size="24" name="barcode" style="WIDTH: 260px" value="<?=$barcode?>"></td>\r
+ </tr>\r
+ <tr>\r
+ </tr>\r
+ <tr>\r
+  <td colspan="2" align="center"><input type="Submit" name="Submit" value="Show"></td>\r
+ </tr>\r
+</table>\r
+</form>\r
+</body>\r
+</html>\r
diff --git a/Open-ILS/web/staff/php/barcode/sample.png b/Open-ILS/web/staff/php/barcode/sample.png
new file mode 100755 (executable)
index 0000000..1fdbbf6
Binary files /dev/null and b/Open-ILS/web/staff/php/barcode/sample.png differ
diff --git a/Open-ILS/web/staff/php/barcode/spain.png b/Open-ILS/web/staff/php/barcode/spain.png
new file mode 100755 (executable)
index 0000000..d50f1be
Binary files /dev/null and b/Open-ILS/web/staff/php/barcode/spain.png differ
diff --git a/Open-ILS/web/staff/xml/circ_tracker.xml b/Open-ILS/web/staff/xml/circ_tracker.xml
new file mode 100644 (file)
index 0000000..0f35a2b
--- /dev/null
@@ -0,0 +1,267 @@
+<html>
+<head>
+       <style type="text/css">
+        @import '/js/dojo/dojo/resources/dojo.css';
+        @import '/js/dojo/dijit/themes/tundra/tundra.css';
+               @import '/js/dojo/dojox/grid/resources/Grid.css';
+
+        #mainhead{
+            height:120px;
+            background-color:#1d57aa;
+        }
+
+        #wrap{
+            width:950px;
+            margin-left: auto;
+            margin-right: auto;
+            border:1px solid #8396d3;
+            min-height:750px;
+            background-color:white;
+            margin-top:0px;
+
+        }
+
+        .mainNav{
+            text-decoration:none;
+            color:#8396d3;
+            padding-right:1em;
+        }
+
+        .thispage{
+            color:white;
+        }
+
+        a.mainNav:hover{
+            color:white;
+            text-decoration:none;
+        }
+
+        #subhead{
+            background-color:#00396a;
+            padding-left:30px;
+            height:30px;
+            line-height:30px;
+            font-size:1em;
+        }
+
+       </style>
+
+    <script language='javascript' type="text/javascript">
+
+         var djConfig = {
+             AutoIDL: ['aou','aout','pgt','ahr','au','ac','acp','acn','ahtc','atc'],
+             parseOnLoad: true,
+             isDebug: false
+         }, lang, bidi;
+
+    </script>
+
+    <script type="text/javascript" src='/opac/common/js/CGI.js'></script>
+    <script type="text/javascript" src='/js/dojo/dojo/dojo.js'></script>
+    <script type="text/javascript" src='/js/dojo/dojo/openils_dojo.js'></script>
+    <script type="text/javascript" src='/js/dojo/opensrf/opensrf.js'></script>
+    <script type="text/javascript" src='/js/dojo/opensrf/md5.js'></script>
+       <script type='text/javascript' src='../js/circ_tracker.js'></script>
+
+</head>
+<body class="tundra" style="background-color:#b7bbc3;">
+
+    <div id="wrap">
+        <div id="mainhead">
+            <a href="http://fulfillment-ill.org" target="_blank"><img src="/images/FulfillmentHomePageBanner.png" border="0" alt="Open Source Integrated Interlibrary Lending System" /></a>
+        </div>
+        <div id="subhead">
+                <a href="hold_tracker.xml" class="mainNav">Manage ILL Requests</a>
+                <a href="circ_tracker.xml" class="thispage mainNav">Manage ILL Transactions</a>
+                <a href="record_mgmt.xml" class="mainNav">Bibliographic Record Management</a>
+        </div>
+
+    <div style="height: 20px;"/>
+
+    <span style="margin-left: 10px;">Choose a location</span>
+    <div dojoType='openils.widget.OrgUnitFilteringSelect'
+         onChange="resetLocation(this.getValue())"
+         jsId='circLocation'
+         searchAttr='name'
+         labelAttr='name'>
+    </div>
+    <br/>
+    <br/>
+    <br/>
+    <div dojoType="dijit.layout.ContentPane" layoutAlign="client" style='min-height:600px; height:80%'>
+        <div dojoType="dijit.layout.TabContainer">
+            <div dojoType="dijit.layout.ContentPane"
+                 class='oils-acq-detail-content-pane'
+                 title="Single Scan" style="height:100%"
+                 selected='true'>
+                <script type='dojo/connect' event='onShow'>itemScanBox.focus()</script>
+
+                <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+                    <h1>Check In or Recall ILLs</h1>
+                    <br/>
+                    Scan Item: <input type="text" dojoType="dijit.form.TextBox" jsId="itemScanBox" trim="true" id="itemScanBox">
+                        <script type='dojo/connect' event='startup'>
+                            dojo.connect(
+                                itemScanBox,
+                                'onKeyDown', 
+                                function (e) {
+                                    if(e.keyCode != dojo.keys.ENTER) return;
+                                    processCopyBarcodeAction(itemScanBox.getValue(),{x:false});
+                                    itemScanBox.setValue('');
+                                }
+                            );  
+                        </script>
+                    </input>
+                    <br/>
+                    <br/>
+                    <br/>
+
+                    <table class="dojoxGridRowTable dojoxGridHeader dojoxMasterGridHeader" style="border-collapse:collapse; width:100%;">
+                        <caption style="text-align:right; caption-side:bottom;"><a href="javascript:printAllCircStatDetail()">Print ALL</a></caption>
+                        <tbody id="statusTarget">
+                            <tr style="height:2em;">
+                                <th style="font-weight:bold;" class="action dojoxGridCell">Action</th>
+                                <th style="font-weight:bold;" class="item dojoxGridCell">Item</th>
+                                <th style="font-weight:bold;" class="patron dojoxGridCell">Patron</th>
+                                <th style="font-weight:bold;" class="location dojoxGridCell">Location</th>
+                                <th style="font-weight:bold;" class="receipt dojoxGridCell">Print...</th>
+                            </tr>
+                        </tbody>
+                        <tbody style="display:none; visiblility:hidden;">
+                            <tr class="statusTemplate">
+                                <td class="action dojoxGridCell"></td>
+                                <td class="item dojoxGridCell"></td>
+                                <td class="patron dojoxGridCell"></td>
+                                <td class="location dojoxGridCell"></td>
+                                <td class="dojoxGridCell">
+                                    <a class="receipt">ILL Receipt</a>
+                                </td>
+                            </tr>
+                        </tbody>
+                    </table>
+
+                </div>
+            </div>
+
+            <div dojoType="dijit.layout.ContentPane"
+                 class='oils-acq-detail-content-pane'
+                 title="ILLs To My patrons" style="height:100%">
+                <script type='dojo/connect' event='onShow'>resetLocation(circLocation.getValue(),{tmg:true})</script>
+
+                <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+                    <table  jsId="toMeGrid"
+                            dojoType="openils.widget.AutoGrid"
+                            autoHeight='true'
+                            fieldOrder="['id', 'target_copy', 'xact_start','due_date', 'usr','duration','renewal_remaining']"
+                            suppressFields="['xact_finish','unrecovered','circ_lib','circ_staff','checkin_staff','checkin_lib','stop_fines_time','checkin_time','phone_renewal','desk_renewal','opac_renewal','duration_rule','recurring_fine_rule','max_fine_rule','stop_fines','workstation','checkin_workstation','checkin_scan_time','parent_circ','create_time','fine_interval','max_fine','recurring_fine']"
+                            query="{id: '*'}"
+                            defaultCellWidth='"auto"'
+                            fmClass='circ'
+                            showPaginator='false'
+                            showSequenceFields="true"
+                            editOnEnter='false'>
+                        <thead>
+                            <tr>
+                                <th field="target_copy" get="fetchCopy" formatter="formatCopyBarcode"></th>
+                                <th field="usr" get="fetchCard" formatter="formatBarcode"></th>
+                                <th name="Actions" field="id" get="fetchCirc" formatter="formatLocalActions"></th>
+                            </tr>
+                        </thead>
+                    </table>
+                </div>
+            </div>
+    
+            <div dojoType="dijit.layout.ContentPane"
+                 class='oils-acq-detail-content-pane'
+                 title="ILLs To Other locations" style="height:100%">
+                <script type='dojo/connect' event='onShow'>resetLocation(circLocation.getValue(),{fmg:true})</script>
+    
+                <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+                    <table  jsId="fromMeGrid"
+                            dojoType="openils.widget.AutoGrid"
+                            autoHeight='true'
+                            fieldOrder="['id', 'target_copy', 'xact_start','due_date', 'circ_lib','duration','renewal_remaining']"
+                            suppressFields="['xact_finish','unrecovered','usr','circ_staff','checkin_staff','checkin_lib','stop_fines_time','checkin_time','phone_renewal','desk_renewal','opac_renewal','duration_rule','recurring_fine_rule','max_fine_rule','stop_fines','workstation','checkin_workstation','checkin_scan_time','parent_circ','create_time','fine_interval','max_fine','recurring_fine']"
+                            query="{id: '*'}"
+                            defaultCellWidth='"auto"'
+                            fmClass='circ'
+                            showPaginator='false'
+                            showSequenceFields="true"
+                            editOnEnter='false'>
+                        <thead>
+                            <tr>
+                                <th field="target_copy" get="fetchCopy" formatter="formatCopyBarcode"></th>
+                                <th field="circ_lib" get="fetchCirclib" formatter="formatCirclib"></th>
+                                <th name="Actions" field="id" get="fetchCirc" formatter="formatRemoteActions"></th>
+                            </tr>
+                        </thead>
+                    </table>
+                </div>
+          </div>
+        
+            <div dojoType="dijit.layout.ContentPane"
+                 class='oils-acq-detail-content-pane'
+                 title="Return Transits To Me" style="height:100%">
+                <script type='dojo/connect' event='onShow'>resetLocation(circLocation.getValue(),{nhttmg:true})</script>
+
+                <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+                    <table  jsId="nonHoldTransitsToMeGrid"
+                            dojoType="openils.widget.AutoGrid"
+                            autoHeight='true'
+                            fieldOrder="['id','target_copy', 'source', 'source_send_time']"
+                            suppressFields="['persistant_transfer','dest','prev_hop','copy_status','prev_dest','dest_recv_time']"
+                            query="{id: '*'}"
+                            defaultCellWidth='"auto"'
+                            fmClass='atc'
+                            showSequenceFields="true"
+                            showLoadFilter="true"
+                            editOnEnter='false'>
+                        <thead>
+                            <tr>
+                                <th name="Actions" field="id" get="fetchTransit" formatter="formatIncomingTransitActions"></th>
+                                <th field="target_copy" get="fetchCopy" formatter="formatCopyBarcode"></th>
+                                <th field="source" get="fetchSource" formatter="formatCirclib"></th>
+                            </tr>
+                        </thead>
+                    </table>
+                </div>
+            </div>
+
+            <div dojoType="dijit.layout.ContentPane"
+                 class='oils-acq-detail-content-pane'
+                 title="Return Transits To Other Locations" style="height:100%">
+                <script type='dojo/connect' event='onShow'>resetLocation(circLocation.getValue(),{nhtfmg:true})</script>
+
+                <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+                    <table  jsId="nonHoldTransitsFromMeGrid"
+                            dojoType="openils.widget.AutoGrid"
+                            autoHeight='true'
+                            fieldOrder="['target_copy', 'dest', 'source_send_time']"
+                            suppressFields="['id','persistant_transfer','source','prev_hop','copy_status','prev_dest','dest_recv_time']"
+                            query="{id: '*'}"
+                            defaultCellWidth='"auto"'
+                            fmClass='atc'
+                            showSequenceFields="true"
+                            showLoadFilter="true"
+                            editOnEnter='false'>
+                        <thead>
+                            <tr>
+                                <!-- <th name="Actions" field="id" get="fetchTransit" formatter="formatOutgoingTransitActions"></th> -->
+                                <th field="target_copy" get="fetchCopy" formatter="formatCopyBarcode"></th>
+                                <th field="dest" get="fetchDest" formatter="formatCirclib"></th>
+                            </tr>
+                        </thead>
+                    </table>
+                </div>
+            </div>
+
+
+       </div>
+    </div>
+
+    </div>
+    <br/>
+    <br/>
+    <a href="javascript:dojo.cookie('ses',  null, {path:'/',expires:-1});location.href=location.href;">Log Out</a>
+</body>
+</html>
diff --git a/Open-ILS/web/staff/xml/hold_tracker.xml b/Open-ILS/web/staff/xml/hold_tracker.xml
new file mode 100644 (file)
index 0000000..beb5ee0
--- /dev/null
@@ -0,0 +1,276 @@
+<html>
+<head>
+    <style type="text/css">
+        @import '/js/dojo/dojo/resources/dojo.css';
+        @import '/js/dojo/dijit/themes/tundra/tundra.css';
+        @import '/js/dojo/dojox/grid/resources/Grid.css';
+
+        #mainhead{
+            height:120px;
+            background-color:#1d57aa;
+        }
+
+        #wrap{
+            width:950px;
+            margin-left: auto;
+            margin-right: auto;
+            border:1px solid #8396d3;
+            min-height:750px;
+            background-color:white;
+            margin-top:0px;
+        
+        }
+
+        .mainNav{
+            text-decoration:none;
+            color:#8396d3;
+            padding-right:1em;
+        }
+
+        .thispage{
+            color:white;
+        }
+
+        a.mainNav:hover{
+            color:white;
+            text-decoration:none;
+        }
+
+        #subhead{
+            background-color:#00396a;
+            padding-left:30px;
+            height:30px;
+            line-height:30px;
+            font-size:1em;
+        }
+
+    </style>
+
+    <script language='javascript' type="text/javascript">
+
+         var djConfig = {
+             AutoIDL: ['aou','aout','pgt','ahr','au','ac','acp','acn','ahtc','atc'],
+             parseOnLoad: true,
+             isDebug: false
+         }, lang, bidi;
+
+    </script>
+
+    <script type="text/javascript" src='/opac/common/js/CGI.js'></script>
+    <script type="text/javascript" src='/js/dojo/dojo/dojo.js'></script>
+    <script type="text/javascript" src='/js/dojo/dojo/openils_dojo.js'></script>
+    <script type="text/javascript" src='/js/dojo/opensrf/opensrf.js'></script>
+    <script type="text/javascript" src='/js/dojo/opensrf/md5.js'></script>
+    <script type='text/javascript' src='../js/hold_tracker.js'></script>
+
+</head>
+<body class="tundra" style="background-color:#b7bbc3;">
+
+    <div id="wrap"> 
+        <div id="mainhead"> 
+            <a href="http://fulfillment-ill.org" target="_blank"><img src="/images/FulfillmentHomePageBanner.png" border="0" alt="Open Source Integrated Interlibrary Lending System" /></a> 
+        </div> 
+        <div id="subhead"> 
+                <a href="hold_tracker.xml" class="thispage mainNav">Manage ILL Requests</a> 
+                <a href="circ_tracker.xml" class="mainNav">Manage ILL Transactions</a> 
+                <a href="record_mgmt.xml" class="mainNav">Bibliographic Record Management</a> 
+        </div> 
+
+    <div style="height: 20px;"/>
+
+    <span style="margin-left: 10px;">Choose a location</span>
+    <div dojoType='openils.widget.OrgUnitFilteringSelect'
+         onChange="resetLocation(this.getValue())"
+         jsId='holdLocation'
+         searchAttr='name'
+         labelAttr='name'>
+    </div>
+    <br/>
+    <br/>
+    <br/>
+
+    <div dojoType="dijit.layout.ContentPane" layoutAlign="client" style='min-height:600px; height:80%'>
+        <div dojoType="dijit.layout.TabContainer" style="height:100%">
+            <div dojoType="dijit.layout.ContentPane"
+                 class='oils-acq-detail-content-pane'
+                 title="Single Scan"
+                 selected='true' style="height:100%">
+                <script type='dojo/connect' event='onShow'>itemScanBox.focus()</script>
+
+                <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+                    <h1>Capture, Receive or Circulate ILL Requests</h1>
+                    <br/>
+                    Scan Item: <input type="text" dojoType="dijit.form.TextBox" jsId="itemScanBox" trim="true" id="itemScanBox">
+                        <script type='dojo/connect' event='startup'>
+                            dojo.connect(
+                                itemScanBox,
+                                'onKeyDown',
+                                function (e) {
+                                    if(e.keyCode != dojo.keys.ENTER) return;
+                                    processCopyBarcodeAction(itemScanBox.getValue());
+                                    itemScanBox.setValue('');
+                                }
+                            );
+                        </script>
+                    </input>
+                    <br/>
+                    <br/>
+                    <br/>
+
+                    <table class="dojoxGridRowTable dojoxGridHeader dojoxMasterGridHeader" style="border-collapse:collapse; width:100%;">
+                        <caption style="text-align:right; caption-side:bottom;"><a href="javascript:printAllHoldStatDetail()">Print ALL</a></caption>
+                        <tbody id="statusTarget">
+                            <tr style="height:2em;">
+                                <th style="font-weight:bold;" class="direction dojoxGridCell">Direction</th>
+                                <th style="font-weight:bold;" class="action dojoxGridCell">Action</th>
+                                <th style="font-weight:bold;" class="item dojoxGridCell">Item</th>
+                                <th style="font-weight:bold;" class="patron dojoxGridCell">Patron</th>
+                                <th style="font-weight:bold;" class="location dojoxGridCell">Location</th>
+                                <th style="font-weight:bold;" class="receipt dojoxGridCell">Print...</th>
+                            </tr>
+                        </tbody>
+                        <tbody style="display:none; visiblility:hidden;">
+                            <tr class="statusTemplate">
+                                <td class="direction dojoxGridCell"></td>
+                                <td class="action dojoxGridCell"></td>
+                                <td class="item dojoxGridCell"></td>
+                                <td class="patron dojoxGridCell"></td>
+                                <td class="location dojoxGridCell"></td>
+                                <td class="dojoxGridCell">
+                                    <a class="receipt">ILL Request Receipt</a>
+                                </td>
+                            </tr>
+                        </tbody>
+                    </table>
+
+                </div>
+            </div>
+
+            <div dojoType="dijit.layout.ContentPane"
+                 class='oils-acq-detail-content-pane'
+                 title="ILL Requests By My Patrons" style="height:100%">
+                <script type='dojo/connect' event='onShow'>resetLocation(holdLocation.getValue(),{tmg:true})</script>
+
+                <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+                    <table  jsId="toMeGrid"
+                            dojoType="openils.widget.AutoGrid"
+                            autoHeight='true'
+                            fieldOrder="['id', 'request_time', 'capture_time', 'usr','frozen','thaw_date']"
+                            suppressFields="['cancel_cause','cancel_note','cancel_time','fulfillment_staff','fulfillment_lib','request_lib','selection_ou','selection_depth','holdable_formats','phone_notify','email_notify','shelf_time','shelf_expire_time','fulfillment_time','return_time','checkin_time','target','cut_in_line','expire_time','prev_check_time','mint_condition','hold_type','requestor','pickup_lib']"
+                            query="{id: '*'}"
+                            defaultCellWidth='"auto"'
+                            fmClass='ahr'
+                            showSequenceFields="true"
+                            showLoadFilter="true"
+                            editOnEnter='false'>
+                        <thead>
+                            <tr>
+                                <th field="current_copy" get="fetchCopy" formatter="formatCopyBarcode"></th>
+                                <th field="usr" get="fetchCard" formatter="formatBarcode"></th>
+                                <th field="requestor" get="fetchCard" formatter="formatBarcode"></th>
+                                <th name="Actions" field="id" get="fetchHold" formatter="formatLocalActions"></th>
+                            </tr>
+                        </thead>
+                    </table>
+                </div>
+            </div>
+    
+            
+
+       <div dojoType="dijit.layout.ContentPane"
+                 class='oils-acq-detail-content-pane'
+                 title="ILL Requests From Other Locations" style="height:100%">
+                <script type='dojo/connect' event='onShow'>resetLocation(holdLocation.getValue(),{fmg:true})</script>
+    
+                <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+                    <table  jsId="fromMeGrid"
+                            dojoType="openils.widget.AutoGrid"
+                            autoHeight='true'
+                            fieldOrder="['id', 'request_time', 'capture_time', 'pickup_lib','frozen','thaw_date']"
+                            suppressFields="['cancel_cause','cancel_note','cancel_time','fulfillment_staff','fulfillment_lib','request_lib','selection_ou','selection_depth','holdable_formats','phone_notify','email_notify','shelf_time','shelf_expire_time','fulfillment_time','return_time','checkin_time','target','cut_in_line','expire_time','prev_check_time','mint_condition','hold_type','requestor','usr']"
+                            query="{id: '*'}"
+                            defaultCellWidth='"auto"'
+                            fmClass='ahr'
+                            showSequenceFields="true"
+                            showLoadFilter="true"
+                            editOnEnter='false'>
+                        <thead>
+                            <tr>
+                                <th field="pickup_lib" get="fetchCirclib" formatter="formatCirclib"></th>
+                                <th field="requestor" get="fetchCard" formatter="formatBarcode"></th>
+                                <th field="usr" get="fetchCard" formatter="formatBarcode"></th>
+                                <th name="Actions" field="id" get="fetchHold" formatter="formatRemoteActions"></th>
+                                <th field="current_copy" get="fetchCopy" formatter="formatCopyBarcode"></th>
+                            </tr>
+                        </thead>
+                    </table>
+                </div>
+           </div>
+        
+       
+            <div dojoType="dijit.layout.ContentPane"
+                 class='oils-acq-detail-content-pane'
+                 title="ILL Transits From Me" style="height:100%">
+                <script type='dojo/connect' event='onShow'>resetLocation(holdLocation.getValue(),{htfmg:true})</script>
+
+                <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+                    <table  jsId="holdTransitFromMeGrid"
+                            dojoType="openils.widget.AutoGrid"
+                            autoHeight='true'
+                            fieldOrder="['id','target_copy', 'dest', 'source_send_time']"
+                            suppressFields="['persistant_transfer','source','prev_hop','copy_status','prev_dest','hold', 'dest_recv_time']"
+                            query="{id: '*'}"
+                            defaultCellWidth='"auto"'
+                            fmClass='ahtc'
+                            showSequenceFields="true"
+                            showLoadFilter="true"
+                            editOnEnter='false'>
+                        <thead>
+                            <tr>
+                                <th name="Actions" field="id" get="fetchTransit" formatter="formatOutgoingTransitActions"></th>
+                                <th field="target_copy" get="fetchTargetCopy" formatter="formatCopyBarcode"></th>
+                                <th field="dest" get="fetchDest" formatter="formatCirclib"></th>
+                            </tr>
+                        </thead>
+                    </table>
+                </div>
+            </div>
+
+
+            <div dojoType="dijit.layout.ContentPane"
+                 class='oils-acq-detail-content-pane'
+                 title="ILL Transits From Other Locations">
+                <script type='dojo/connect' event='onShow'>resetLocation(holdLocation.getValue(),{httmg:true})</script>
+
+                <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+                    <table  jsId="holdTransitToMeGrid"
+                            dojoType="openils.widget.AutoGrid"
+                            autoHeight='true'
+                            fieldOrder="['id','target_copy', 'source', 'source_send_time']"
+                            suppressFields="['persistant_transfer','dest','prev_hop','copy_status','prev_dest','hold', 'dest_recv_time']"
+                            query="{id: '*'}"
+                            defaultCellWidth='"auto"'
+                            fmClass='ahtc'
+                            showSequenceFields="true"
+                            showLoadFilter="true"
+                            editOnEnter='false'>
+                        <thead>
+                            <tr>
+                                <th name="Actions" field="id" get="fetchTransit" formatter="formatIncomingTransitActions"></th>
+                                <th field="target_copy" get="fetchTargetCopy" formatter="formatCopyBarcode"></th>
+                                <th field="source" get="fetchSource" formatter="formatCirclib"></th>
+                            </tr>
+                        </thead>
+                    </table>
+                </div>
+            </div>
+
+           </div>
+    </div>
+                
+    </div>
+    <br/>
+    <br/>
+    <a href="javascript:dojo.cookie('ses', null, {path:'/',expires:-1});location.href=location.href;">Log Out</a>
+</body>
+</html>
diff --git a/Open-ILS/web/staff/xml/record_mgmt.xml b/Open-ILS/web/staff/xml/record_mgmt.xml
new file mode 100644 (file)
index 0000000..c1c9a29
--- /dev/null
@@ -0,0 +1,154 @@
+<html>
+<head>
+    <style type="text/css">
+
+        @import '/js/dojo/dojo/resources/dojo.css';
+        @import '/js/dojo/dijit/themes/tundra/tundra.css';
+        @import '/js/dojo/dojox/grid/resources/Grid.css';
+
+        #mainhead{
+            height:120px;
+            background-color:#1d57aa;
+        }
+
+        #wrap{
+            width:950px;
+            margin-left: auto;
+            margin-right: auto;
+            border:1px solid #8396d3;
+            min-height:750px;
+            background-color:white;
+            margin-top:0px;
+
+        }
+
+        .mainNav{
+            text-decoration:none;
+            color:#8396d3;
+            padding-right:1em;
+        }
+
+        .thispage{
+            color:white;
+        }
+
+        a.mainNav:hover{
+            color:white;
+            text-decoration:none;
+        }
+
+        #subhead{
+            background-color:#00396a;
+            padding-left:30px;
+            height:30px;
+            line-height:30px;
+            font-size:1em;
+        }
+
+    </style>
+
+    <script language='javascript' type="text/javascript">
+
+         var djConfig = {
+             AutoIDL: ['aou','aout','pgt','ahr','au','ac','acp','acn'],
+             parseOnLoad: true,
+             isDebug: false
+         }, lang, bidi;
+
+    </script>
+
+    <script type="text/javascript" src='/opac/common/js/CGI.js'></script>
+    <script type="text/javascript" src='/js/dojo/dojo/dojo.js'></script>
+    <script type="text/javascript" src='/js/dojo/dojo/openils_dojo.js'></script>
+    <script type="text/javascript" src='/js/dojo/opensrf/opensrf.js'></script>
+    <script type="text/javascript" src='/js/dojo/opensrf/md5.js'></script>
+    <script type='text/javascript' src='../js/record_mgmt.js'></script>
+
+</head>
+<body class="tundra" style="background-color:#b7bbc3;">
+
+    <div id="wrap">
+        <div id="mainhead">
+            <a href="http://fulfillment-ill.org" target="_blank"><img src="/images/FulfillmentHomePageBanner.png" border="0" alt="Open Source Integrated Interlibrary Lending System" /></a>
+        </div>
+        <div id="subhead">
+                <a href="hold_tracker.xml" class="mainNav">Manage ILL Requests</a>
+                <a href="circ_tracker.xml" class="mainNav">Manage ILL Transactions</a>
+                <a href="record_mgmt.xml" class="thispage mainNav">Bibliographic Record Management</a>
+        </div>
+
+        <div style="height: 20px;"/>
+    
+        <span style="margin-left: 10px;">Choose a location</span>
+        <select dojoType='openils.widget.OrgUnitFilteringSelect'
+             jsId='loc'
+             name='loc'
+             searchAttr='name'
+             labelAttr='name'
+            >
+        </select>
+        <br/>
+        <br/>
+        <br/>
+        <div dojoType="dijit.layout.ContentPane" layoutAlign="client" style='height:80%'>
+            <div dojoType="dijit.layout.TabContainer">
+
+                <div dojoType="dijit.layout.ContentPane"
+                     class='oils-acq-detail-content-pane'
+                     title="Ingest batch record file"
+                     selected='true'>
+
+                    <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+                
+                        <form action="/staff/postBibs/" method="POST" enctype="multipart/form-data" name="localMarcForm" id="localMarcForm">
+                            <label for="loadFile">Load File:</label>
+                            <input type="file" name="loadFile" id="loadFile" />
+                            <input type="hidden" name="uploadLocation" id="uploadLocation" />
+                            <br/>
+                            <br/>
+                            <table><tr><td>Status:</td><td><div id="response"></div></td></tr></table>
+                            <div class="dojo-FormUploadProgress"
+                                background="#333"
+                                color="#ccc"
+                                    ready="upload:progress.ready"
+                                connecting="upload:progress.connecting"
+                                attr="ready connecting"/>
+                            <br/>
+                            <input type="submit" onclick="dojo.attr('uploadLocation', 'value', loc.getValue()); return true" value="Submit" />
+                        </form>
+
+                    </div>
+                </div>
+
+                <div dojoType="dijit.layout.ContentPane"
+                     class='oils-acq-detail-content-pane'
+                     title="Purge bibliographic records"
+                     selected='true'>
+
+                    <div dojoType="dijit.layout.ContentPane" layoutAlign="client">
+                
+                        <button dojoType='dijit.form.Button'>
+                            <span>Purge all current bibliographic records</span>
+                            <script type='dojo/connect' event='onClick'>
+                               if (confirm('This will completely remove all bibliographic data for the currently selected location!')) {
+                                       var success = fieldmapper.standardRequest(
+                                           ['open-ils.cat','open-ils.cat.biblio.record.purge_by_owner'],
+                                           [user.authtoken, loc.getValue()]
+                                       );
+                               }
+                            </script>
+                        </button>
+
+                    </div>
+                </div>
+
+            </div>
+        </div>
+
+    </div>
+    <br/>
+    <br/>
+    <a href="javascript:dojo.cookie('ses',  null, {path:'/',expires:-1});location.href=location.href;">Log Out</a>
+</body>
+</html>
+