From 08bf4f1843ae813d5cbc7bbb03cd4652663e97ce Mon Sep 17 00:00:00 2001 From: miker Date: Wed, 13 Jan 2010 16:35:03 +0000 Subject: [PATCH] Turn away! Avert your eyes! Herein lies the initial 1.6 backport of changes* implementing the booking module for Evergreen. 1.6.1, we look forward to you! *) Changesets included: 14921, 14925, 15019, 15068, 15071, 15072, 15076, 15077, 15092, 15096, 15099, 15100, 15103, 15104, 15108, 15110, 15113, 15128, 15133, 15161, 15164, 15188, 15207, 15211, 15215, 15223, 15224, 15228, 15236, 15241, 15247, 15264, 15285, 15288, 15289, 15309 git-svn-id: svn://svn.open-ils.org/ILS/branches/rel_1_6@15312 dcc99617-32d9-48b4-a31d-7c20da2025e4 --- Open-ILS/examples/fm_IDL.xml | 265 +++++ Open-ILS/examples/opensrf.xml.example | 23 + Open-ILS/examples/opensrf_core.xml.example | 1 + Open-ILS/src/extras/ils_events.xml | 12 + .../src/perlmods/OpenILS/Application/AppUtils.pm | 91 ++ .../src/perlmods/OpenILS/Application/Booking.pm | 1148 ++++++++++++++++++++ Open-ILS/src/perlmods/OpenILS/Application/Circ.pm | 138 +++ .../perlmods/OpenILS/Application/Circ/Circulate.pm | 341 +++++- .../src/perlmods/OpenILS/Application/Circ/Money.pm | 18 +- .../perlmods/OpenILS/Application/Collections.pm | 52 +- .../src/perlmods/OpenILS/Application/CreditCard.pm | 5 +- .../perlmods/OpenILS/Application/Storage/CDBI.pm | 16 +- .../OpenILS/Application/Storage/CDBI/action.pm | 10 + .../OpenILS/Application/Storage/CDBI/booking.pm | 57 + .../OpenILS/Application/Storage/Driver/Pg/dbi.pm | 37 + .../Application/Storage/Publisher/action.pm | 265 ++++- .../OpenILS/Application/Storage/Publisher/money.pm | 85 +- Open-ILS/src/perlmods/OpenILS/Const.pm | 1 + .../src/perlmods/OpenILS/Utils/CStoreEditor.pm | 6 +- Open-ILS/src/perlmods/OpenILS/WWW/BadDebt.pm | 14 +- Open-ILS/src/sql/Pg/002.schema.config.sql | 2 +- Open-ILS/src/sql/Pg/095.schema.booking.sql | 168 +++ Open-ILS/src/sql/Pg/100.circ_matrix.sql | 12 + Open-ILS/src/sql/Pg/500.view.cross-schema.sql | 3 +- Open-ILS/src/sql/Pg/950.data.seed-values.sql | 14 +- Open-ILS/src/sql/Pg/build-db.sh | 3 + .../sql/Pg/upgrade/0086.schema.booking-tables.sql | 135 +++ .../upgrade/0090.schema.booking.bib-base-types.sql | 9 + .../Pg/upgrade/0103.schema.booking.max_fine.sql | 8 + .../Pg/upgrade/0105.schema.booking-integration.sql | 339 ++++++ .../Pg/upgrade/0106.booking.admin_permissions.sql | 15 + ...09.data.org-settings-booking_alter_due_date.sql | 20 + ...110.schema.booking_resource_type.elbow_room.sql | 8 + .../upgrade/0122.data.reservation-shelf-status.sql | 7 + .../0130.booking.resource_constraint_and_perms.sql | 12 + .../sql/Pg/upgrade/119.schema.booking.transits.sql | 15 + .../support-scripts/test-scripts/collections.pl | 3 +- Open-ILS/web/css/skin/default/booking.css | 89 ++ .../web/js/dojo/openils/booking/nls/capture.js | 24 + .../dojo/openils/booking/nls/pickup_and_return.js | 38 + .../web/js/dojo/openils/booking/nls/pull_list.js | 20 + .../web/js/dojo/openils/booking/nls/reservation.js | 82 ++ Open-ILS/web/js/ui/default/booking/capture.js | 174 +++ Open-ILS/web/js/ui/default/booking/common.js | 71 ++ Open-ILS/web/js/ui/default/booking/pickup.js | 32 + Open-ILS/web/js/ui/default/booking/populator.js | 283 +++++ Open-ILS/web/js/ui/default/booking/pull_list.js | 203 ++++ Open-ILS/web/js/ui/default/booking/reservation.js | 768 +++++++++++++ Open-ILS/web/js/ui/default/booking/return.js | 41 + Open-ILS/web/opac/locale/en-US/lang.dtd | 47 + Open-ILS/web/opac/skin/default/js/myopac.js | 6 + Open-ILS/web/templates/default/booking/capture.tt2 | 21 + Open-ILS/web/templates/default/booking/pickup.tt2 | 77 ++ .../web/templates/default/booking/pull_list.tt2 | 50 + .../web/templates/default/booking/reservation.tt2 | 117 ++ Open-ILS/web/templates/default/booking/return.tt2 | 87 ++ .../default/conify/global/booking/reservation.tt2 | 39 + .../global/booking/reservation_attr_value_map.tt2 | 39 + .../default/conify/global/booking/resource.tt2 | 43 + .../conify/global/booking/resource_attr.tt2 | 39 + .../conify/global/booking/resource_attr_map.tt2 | 39 + .../conify/global/booking/resource_attr_value.tt2 | 39 + .../conify/global/booking/resource_type.tt2 | 39 + .../staff_client/chrome/content/main/constants.js | 5 + .../xul/staff_client/chrome/content/main/menu.js | 97 +- .../chrome/content/main/menu_frame_menus.xul | 30 + .../chrome/content/main/menu_frame_overlay.xul | 1 + .../staff_client/chrome/content/util/functional.js | 10 +- .../chrome/locale/en-US/offline.properties | 6 + Open-ILS/xul/staff_client/server/admin/index.xhtml | 34 +- .../xul/staff_client/server/cat/copy_browser.js | 69 +- .../xul/staff_client/server/cat/copy_browser.xul | 7 + Open-ILS/xul/staff_client/server/cat/util.js | 73 ++ .../xul/staff_client/server/circ/copy_status.js | 54 + .../xul/staff_client/server/circ/copy_status.xul | 2 + .../server/circ/copy_status_overlay.xul | 6 + .../server/locale/en-US/cat.properties | 4 + .../server/locale/en-US/patron.properties | 3 + Open-ILS/xul/staff_client/server/patron/bills.js | 44 + Open-ILS/xul/staff_client/server/patron/display.js | 63 ++ .../xul/staff_client/server/patron/display.xul | 3 + .../staff_client/server/patron/display_horiz.xul | 3 + .../server/patron/display_horiz_overlay.xul | 7 + .../staff_client/server/patron/display_overlay.xul | 7 + 84 files changed, 6345 insertions(+), 48 deletions(-) create mode 100644 Open-ILS/src/perlmods/OpenILS/Application/Booking.pm create mode 100644 Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI/booking.pm create mode 100644 Open-ILS/src/sql/Pg/095.schema.booking.sql create mode 100644 Open-ILS/src/sql/Pg/upgrade/0086.schema.booking-tables.sql create mode 100644 Open-ILS/src/sql/Pg/upgrade/0090.schema.booking.bib-base-types.sql create mode 100644 Open-ILS/src/sql/Pg/upgrade/0103.schema.booking.max_fine.sql create mode 100644 Open-ILS/src/sql/Pg/upgrade/0105.schema.booking-integration.sql create mode 100644 Open-ILS/src/sql/Pg/upgrade/0106.booking.admin_permissions.sql create mode 100644 Open-ILS/src/sql/Pg/upgrade/0109.data.org-settings-booking_alter_due_date.sql create mode 100644 Open-ILS/src/sql/Pg/upgrade/0110.schema.booking_resource_type.elbow_room.sql create mode 100644 Open-ILS/src/sql/Pg/upgrade/0122.data.reservation-shelf-status.sql create mode 100644 Open-ILS/src/sql/Pg/upgrade/0130.booking.resource_constraint_and_perms.sql create mode 100644 Open-ILS/src/sql/Pg/upgrade/119.schema.booking.transits.sql create mode 100644 Open-ILS/web/css/skin/default/booking.css create mode 100644 Open-ILS/web/js/dojo/openils/booking/nls/capture.js create mode 100644 Open-ILS/web/js/dojo/openils/booking/nls/pickup_and_return.js create mode 100644 Open-ILS/web/js/dojo/openils/booking/nls/pull_list.js create mode 100644 Open-ILS/web/js/dojo/openils/booking/nls/reservation.js create mode 100644 Open-ILS/web/js/ui/default/booking/capture.js create mode 100644 Open-ILS/web/js/ui/default/booking/common.js create mode 100644 Open-ILS/web/js/ui/default/booking/pickup.js create mode 100644 Open-ILS/web/js/ui/default/booking/populator.js create mode 100644 Open-ILS/web/js/ui/default/booking/pull_list.js create mode 100644 Open-ILS/web/js/ui/default/booking/reservation.js create mode 100644 Open-ILS/web/js/ui/default/booking/return.js create mode 100644 Open-ILS/web/templates/default/booking/capture.tt2 create mode 100644 Open-ILS/web/templates/default/booking/pickup.tt2 create mode 100644 Open-ILS/web/templates/default/booking/pull_list.tt2 create mode 100644 Open-ILS/web/templates/default/booking/reservation.tt2 create mode 100644 Open-ILS/web/templates/default/booking/return.tt2 create mode 100644 Open-ILS/web/templates/default/conify/global/booking/reservation.tt2 create mode 100644 Open-ILS/web/templates/default/conify/global/booking/reservation_attr_value_map.tt2 create mode 100644 Open-ILS/web/templates/default/conify/global/booking/resource.tt2 create mode 100644 Open-ILS/web/templates/default/conify/global/booking/resource_attr.tt2 create mode 100644 Open-ILS/web/templates/default/conify/global/booking/resource_attr_map.tt2 create mode 100644 Open-ILS/web/templates/default/conify/global/booking/resource_attr_value.tt2 create mode 100644 Open-ILS/web/templates/default/conify/global/booking/resource_type.tt2 diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index aa28b280e9..0a96b069a6 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -1649,6 +1649,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + @@ -1678,6 +1679,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + @@ -2208,6 +2210,221 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2676,6 +2893,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + + + + + + @@ -2694,6 +2918,12 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + + + + + @@ -3616,6 +3846,41 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/examples/opensrf.xml.example b/Open-ILS/examples/opensrf.xml.example index 6d5824700e..552ec3241b 100644 --- a/Open-ILS/examples/opensrf.xml.example +++ b/Open-ILS/examples/opensrf.xml.example @@ -430,6 +430,28 @@ vim:et:ts=4:sw=4: + + 5 + 1 + perl + OpenILS::Application::Booking + 199 + + open-ils.booking_unix.sock + open-ils.booking_unix.pid + 1000 + open-ils.booking_unix.log + 1 + 15 + 1 + 5 + + + + LOCALSTATEDIR/templates/marc/k_book.xml + + + 5 @@ -946,6 +968,7 @@ vim:et:ts=4:sw=4: opensrf.math opensrf.dbmath open-ils.acq + open-ils.booking open-ils.cat open-ils.supercat open-ils.search diff --git a/Open-ILS/examples/opensrf_core.xml.example b/Open-ILS/examples/opensrf_core.xml.example index 695ab4a8bb..3097ce053f 100644 --- a/Open-ILS/examples/opensrf_core.xml.example +++ b/Open-ILS/examples/opensrf_core.xml.example @@ -22,6 +22,7 @@ Example OpenSRF bootstrap configuration file for Evergreen open-ils.actor open-ils.acq open-ils.auth + open-ils.booking open-ils.cat open-ils.circ open-ils.collections diff --git a/Open-ILS/src/extras/ils_events.xml b/Open-ILS/src/extras/ils_events.xml index 1d31af54d5..47b43b7bfb 100644 --- a/Open-ILS/src/extras/ils_events.xml +++ b/Open-ILS/src/extras/ils_events.xml @@ -814,6 +814,18 @@ Hold capture was delayed for this item + + Item reserved for booking request + + + Booking reservation not found + + + Booking reservation capture failed + + + Provided parameters describe unacceptable reservation. + diff --git a/Open-ILS/src/perlmods/OpenILS/Application/AppUtils.pm b/Open-ILS/src/perlmods/OpenILS/Application/AppUtils.pm index 6281e4e63e..496222c659 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/AppUtils.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/AppUtils.pm @@ -811,6 +811,64 @@ sub DB_UPDATE_FAILED { payload => ($payload) ? $payload : undef ); } +sub fetch_booking_reservation { + my( $self, $id ) = @_; + my( $res, $evt ); + + $res = $self->simplereq( + 'open-ils.cstore', + 'open-ils.cstore.direct.booking.reservation.retrieve', $id + ); + + # simplereq doesn't know how to flesh so ... + if ($res) { + $res->usr( + $self->simplereq( + 'open-ils.cstore', + 'open-ils.cstore.direct.actor.user.retrieve', $res->usr + ) + ); + + $res->target_resource_type( + $self->simplereq( + 'open-ils.cstore', + 'open-ils.cstore.direct.booking.resource_type.retrieve', $res->target_resource_type + ) + ); + + if ($res->current_resource) { + $res->current_resource( + $self->simplereq( + 'open-ils.cstore', + 'open-ils.cstore.direct.booking.resource.retrieve', $res->current_resource + ) + ); + + if ($self->is_true( $res->target_resource_type->catalog_item )) { + $res->current_resource->catalog_item( $self->fetch_copy_by_barcode( $res->current_resource->barcode ) ); + } + } + + if ($res->target_resource) { + $res->target_resource( + $self->simplereq( + 'open-ils.cstore', + 'open-ils.cstore.direct.booking.resource.retrieve', $res->target_resource + ) + ); + + if ($self->is_true( $res->target_resource_type->catalog_item )) { + $res->target_resource->catalog_item( $self->fetch_copy_by_barcode( $res->target_resource->barcode ) ); + } + } + + } else { + $evt = OpenILS::Event->new('RESERVATION_NOT_FOUND'); + } + + return ($res, $evt); +} + sub fetch_circ_duration_by_name { my( $self, $name ) = @_; my( $dur, $evt ); @@ -951,6 +1009,16 @@ sub unflesh_copy { return $copy; } +sub unflesh_reservation { + my( $self, $reservation ) = @_; + return undef unless $reservation; + $reservation->usr( $reservation->usr->id ) if ref($reservation->usr); + $reservation->target_resource_type( $reservation->target_resource_type->id ) if ref($reservation->target_resource_type); + $reservation->target_resource( $reservation->target_resource->id ) if ref($reservation->target_resource); + $reservation->current_resource( $reservation->current_resource->id ) if ref($reservation->current_resource); + return $reservation; +} + # un-fleshes a copy and updates it in the DB # returns a DB_UPDATE_FAILED event on error # returns undef on success @@ -979,6 +1047,29 @@ sub update_copy { return undef; } +sub update_reservation { + my( $self, %params ) = @_; + + my $reservation = $params{reservation} || die "update_reservation(): reservation required"; + my $editor = $params{editor} || die "update_reservation(): copy editor required"; + my $session = $params{session}; + + $logger->debug("Updating copy in the database: " . $reservation->id); + + $self->unflesh_reservation($reservation); + + my $s; + my $meth = 'open-ils.cstore.direct.booking.reservation.update'; + + $s = $session->request( $meth, $reservation )->gather(1) if $session; + $s = $self->cstorereq( $meth, $reservation ) unless $session; + + $logger->debug("Update of copy ".$reservation->id." returned: $s"); + + return $self->DB_UPDATE_FAILED($reservation) unless $s; + return undef; +} + sub fetch_billable_xact { my( $self, $id ) = @_; my($xact, $evt); diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Booking.pm b/Open-ILS/src/perlmods/OpenILS/Application/Booking.pm new file mode 100644 index 0000000000..dc6ab3bd79 --- /dev/null +++ b/Open-ILS/src/perlmods/OpenILS/Application/Booking.pm @@ -0,0 +1,1148 @@ +package OpenILS::Application::Booking; + +use strict; +use warnings; + +use POSIX qw/strftime/; +use OpenILS::Application; +use base qw/OpenILS::Application/; + +use OpenILS::Utils::CStoreEditor qw/:funcs/; +use OpenILS::Utils::Fieldmapper; +use OpenILS::Application::AppUtils; +my $U = "OpenILS::Application::AppUtils"; + +use OpenSRF::Utils::Logger qw/$logger/; + +sub prepare_new_brt { + my ($record_id, $owning_lib, $mvr) = @_; + + my $brt = new Fieldmapper::booking::resource_type; + $brt->isnew(1); + $brt->name($mvr->title); + $brt->record($record_id); + $brt->catalog_item('t'); + $brt->transferable('t'); + $brt->owner($owning_lib); + + return $brt; +} + +sub get_existing_brt { + my ($e, $record_id, $owning_lib, $mvr) = @_; + my $results = $e->search_booking_resource_type( + {name => $mvr->title, owner => $owning_lib, record => $record_id} + ); + + return $results->[0] if scalar(@$results) > 0; + return undef; +} + +sub get_mvr { + return $U->simplereq( + 'open-ils.search', + 'open-ils.search.biblio.record.mods_slim.retrieve.authoritative', + shift # record id + ); +} + +sub get_unique_owning_libs { + my %hash = (); + $hash{$_->call_number->owning_lib} = 1 foreach (@_); # @_ are copies + return keys %hash; +} + +sub fetch_copies_by_ids { + my ($e, $copy_ids) = @_; + my $results = $e->search_asset_copy([ + {id => $copy_ids}, + {flesh => 1, flesh_fields => {acp => ['call_number']}} + ]); + return $results if ref($results) eq 'ARRAY'; + return []; +} + +sub get_single_record_id { + my $record_id = undef; + foreach (@_) { # @_ are copies + return undef if + (defined $record_id && $record_id != $_->call_number->record); + $record_id = $_->call_number->record; + } + return $record_id; +} + +# This function generates the correct json_query clause for determining +# whether two given ranges overlap. Each range is composed of a start +# and an end point. All four points should be the same type (could be int, +# date, time, timestamp, or perhaps other types). +# +# The first range (or the first two points) should be specified as +# literal values. The second range (or the last two points) should be +# specified as the names of columns, the values of which in a given row +# will constitute the second range in the comparison. +# +# ALSO: PostgreSQL includes an OVERLAPS operator which provides the same +# functionality in a much more concise way, but json_query does not (yet). +sub json_query_ranges_overlap { + +{ '-or' => [ + { '-and' => [{$_[2] => {'>=', $_[0]}}, {$_[2] => {'<', $_[1]}}]}, + { '-and' => [{$_[3] => {'>', $_[0]}}, {$_[3] => {'<', $_[1]}}]}, + { '-and' => { $_[3] => {'>', $_[0]}, $_[2] => {'<=', $_[0]}}}, + { '-and' => { $_[3] => {'>', $_[1]}, $_[2] => {'<', $_[1]}}}, + ]}; +} + +sub create_brt_and_brsrc { + my ($self, $conn, $authtoken, $copy_ids) = @_; + my (@created_brt, @created_brsrc); + my %brt_table = (); + + my $e = new_editor(xact => 1, authtoken => $authtoken); + return $e->die_event unless $e->checkauth; + + my @copies = @{fetch_copies_by_ids($e, $copy_ids)}; + my $record_id = get_single_record_id(@copies) or return $e->die_event; + my $mvr = get_mvr($record_id) or return $e->die_event; + + foreach (get_unique_owning_libs(@copies)) { + $brt_table{$_} = get_existing_brt($e, $record_id, $_, $mvr) || + prepare_new_brt($record_id, $_, $mvr); + } + + while (my ($owning_lib, $brt) = each %brt_table) { + my $pre_existing = 1; + if ($brt->isnew) { + if ($e->allowed('ADMIN_BOOKING_RESOURCE_TYPE', $owning_lib)) { + $pre_existing = 0; + return $e->die_event unless ( + # v-- Important: assignment modifies original hash + $brt = $e->create_booking_resource_type($brt) + ); + } + } + push @created_brt, [$brt->id, $brt->record, $pre_existing]; + } + + foreach (@copies) { + if ($e->allowed( + 'ADMIN_BOOKING_RESOURCE', $_->call_number->owning_lib + )) { + # This block needs to disregard any cstore failures and just + # return what results it can. + my $brsrc = new Fieldmapper::booking::resource; + $brsrc->isnew(1); + $brsrc->type($brt_table{$_->call_number->owning_lib}->id); + $brsrc->owner($_->call_number->owning_lib); + $brsrc->barcode($_->barcode); + + $e->set_savepoint("alpha"); + my $pre_existing = 0; + my $usable_result = undef; + if (!($usable_result = $e->create_booking_resource($brsrc))) { + $e->rollback_savepoint("alpha"); + if (($usable_result = $e->search_booking_resource( + +{ map { ($_, $brsrc->$_()) } qw/type owner barcode/ } + ))) { + $usable_result = $usable_result->[0]; + $pre_existing = 1; + } else { + # So we failed to create a booking resource for this copy. + # For now, let's just keep going. If the calling app wants + # to consider this an error, it can notice the absence + # of a booking resource for the copy in the returned + # results. + $logger->warn( + "Couldn't create or find brsrc for acp #" . $_->id + ); + } + } else { + $e->release_savepoint("alpha"); + } + + if ($usable_result) { + push @created_brsrc, + [$usable_result->id, $_->id, $pre_existing]; + } + } + } + + $e->commit and + return {brt => \@created_brt, brsrc => \@created_brsrc} or + return $e->die_event; +} +__PACKAGE__->register_method( + method => "create_brt_and_brsrc", + api_name => "open-ils.booking.resources.create_from_copies", + signature => { + params => [ + {type => 'string', desc => 'Authentication token'}, + {type => 'array', desc => 'Copy IDs'}, + ], + return => { desc => "A two-element hash. The 'brt' element " . + "is a list of created booking resource types described by " . + "3-tuples (id, copy id, was pre-existing). The 'brsrc' " . + "element is a similar list of created booking resources " . + "described by (id, record id, was pre-existing) 3-tuples."} + } +); + + +sub create_bresv { + my ($self, $client, $authtoken, + $target_user_barcode, $datetime_range, $pickup_lib, + $brt, $brsrc_list, $attr_values) = @_; + + $brsrc_list = [ undef ] if not defined $brsrc_list; + return undef if scalar(@$brsrc_list) < 1; # Empty list not ok. + + my $e = new_editor(xact => 1, authtoken => $authtoken); + return $e->die_event unless $e->checkauth; + return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION"); + + my $usr = $U->fetch_user_by_barcode($target_user_barcode); + return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"}); + + my $results = []; + foreach my $brsrc (@$brsrc_list) { + my $bresv = new Fieldmapper::booking::reservation; + $bresv->usr($usr->id); + $bresv->request_lib($e->requestor->ws_ou); + $bresv->pickup_lib($pickup_lib); + $bresv->start_time($datetime_range->[0]); + $bresv->end_time($datetime_range->[1]); + + # A little sanity checking: don't agree to put a reservation on a + # brsrc and a brt when they don't match. In fact, bomb out of + # this transaction entirely. + if ($brsrc) { + my $brsrc_itself = $e->retrieve_booking_resource([ + $brsrc, { + "flesh" => 1, + "flesh_fields" => {"brsrc" => ["type"]} + } + ]); + + if (not $brsrc_itself) { + my $ev = new OpenILS::Event( + "RESERVATION_BAD_PARAMS", + desc => "brsrc $brsrc doesn't exist" + ); + $e->disconnect; + return $ev; + } + elsif ($brsrc_itself->type->id != $brt) { + my $ev = new OpenILS::Event( + "RESERVATION_BAD_PARAMS", + desc => "brsrc $brsrc doesn't match given brt $brt" + ); + $e->disconnect; + return $ev; + } + + # Also bail if the user is trying to create a reservation at + # a pickup lib to which our resource won't go. + if ( + $brsrc_itself->owner != $pickup_lib and + not $brsrc_itself->type->transferable + ) { + my $ev = new OpenILS::Event( + "RESERVATION_BAD_PARAMS", + desc => "brsrc $brsrc doesn't belong to $pickup_lib and " . + "is not transferable" + ); + $e->disconnect; + return $ev; + } + } + $bresv->target_resource($brsrc); # undef is ok here + $bresv->target_resource_type($brt); + + ($bresv = $e->create_booking_reservation($bresv)) or + return $e->die_event; + + # We could/should do some sanity checking on this too: namely, on + # whether the attribute values given actually apply to the relevant + # brt. Not seeing any grievous side effects of not checking, though. + my @bravm = (); + foreach my $value (@$attr_values) { + my $bravm = new Fieldmapper::booking::reservation_attr_value_map; + $bravm->reservation($bresv->id); + $bravm->attr_value($value); + $bravm = $e->create_booking_reservation_attr_value_map($bravm) or + return $e->die_event; + push @bravm, $bravm; + } + push @$results, { + "bresv" => $bresv->id, + "bravm" => \@bravm, + }; + } + + $e->commit or return $e->die_event; + + # Targeting must be tacked on _after_ committing the transaction where the + # reservations are actually created. + foreach (@$results) { + $_->{"targeting"} = $U->storagereq( + "open-ils.storage.booking.reservation.resource_targeter", + $_->{"bresv"} + )->[0]; + } + return $results; +} +__PACKAGE__->register_method( + method => "create_bresv", + api_name => "open-ils.booking.reservations.create", + signature => { + params => [ + {type => 'string', desc => 'Authentication token'}, + {type => 'string', desc => 'Barcode of user for whom to reserve'}, + {type => 'array', desc => 'Two elements: start and end timestamp'}, + {type => 'int', desc => 'Desired reservation pickup lib'}, + {type => 'int', desc => 'Booking resource type'}, + {type => 'list', desc => 'Booking resource (undef ok; empty not ok)'}, + {type => 'array', desc => 'Attribute values selected'}, + ], + return => { desc => "A hash containing the new bresv and a list " . + "of new bravm"} + } +); + + +sub resource_list_by_attrs { + my $self = shift; + my $client = shift; + my $auth = shift; # Keep as argument, though not used just now. + my $filters = shift; + + return undef unless ($filters->{type} || $filters->{attribute_values}); + + my $query = { + "select" => {brsrc => ["id"]}, + "from" => {brsrc => {"brt" => {}}}, + "where" => {}, + "distinct" => 1 + }; + + $query->{where} = {"-and" => []}; + if ($filters->{type}) { + push @{$query->{where}->{"-and"}}, {"type" => $filters->{type}}; + } + + if ($filters->{pickup_lib}) { + push @{$query->{where}->{"-and"}}, + {"-or" => [ + {"owner" => $filters->{pickup_lib}}, + {"+brt" => {"transferable" => "t"}} + ]}; + } + + if ($filters->{attribute_values}) { + + $query->{from}->{brsrc}->{bram} = { field => 'resource' }; + + $filters->{attribute_values} = [$filters->{attribute_values}] + if (!ref($filters->{attribute_values})); + + $query->{having}->{'+bram'}->{value}->{'@>'} = { + transform => 'array_accum', + value => '$_' . $$ . '${' . + join(',', @{$filters->{attribute_values}}) . + '}$_' . $$ . '$' + }; + } + + if ($filters->{available}) { + # If only one timestamp has been provided, make it into a range. + if (!ref($filters->{available})) { + $filters->{available} = [($filters->{available}) x 2]; + } + + push @{$query->{where}->{"-and"}}, { + "-or" => [ + {"overbook" => "t"}, + {"-not-exists" => { + "select" => {"bresv" => ["id"]}, + "from" => "bresv", + "where" => {"-and" => [ + json_query_ranges_overlap( + $filters->{available}->[0], + $filters->{available}->[1], + "start_time", + "end_time" + ), + {"cancel_time" => undef}, + {"current_resource" => {"=" => {"+brsrc" => "id"}}} + ]}, + }} + ] + }; + } + if ($filters->{booked}) { + # If only one timestamp has been provided, make it into a range. + if (!ref($filters->{booked})) { + $filters->{booked} = [($filters->{booked}) x 2]; + } + + push @{$query->{where}->{"-and"}}, { + "-exists" => { + "select" => {"bresv" => ["id"]}, + "from" => "bresv", + "where" => {"-and" => [ + json_query_ranges_overlap( + $filters->{booked}->[0], + $filters->{booked}->[1], + "start_time", + "end_time" + ), + {"cancel_time" => undef}, + {"current_resource" => { "=" => {"+brsrc" => "id"}}} + ]}, + } + }; + # I think that the "booked" case could be done with a JOIN instead of + # an EXISTS, but I'm leaving it this way for symmetry with the + # "available" case for now. The available case cannot be done with a + # join. + } + + my $cstore = OpenSRF::AppSession->connect('open-ils.cstore'); + my $rows = $cstore->request( 'open-ils.cstore.json_query.atomic', $query )->gather(1); + $cstore->disconnect; + + return @$rows ? [map { $_->{id} } @$rows] : []; +} +__PACKAGE__->register_method( + method => "resource_list_by_attrs", + api_name => "open-ils.booking.resources.filtered_id_list", + argc => 3, + signature=> { + params => [ + {type => 'string', desc => 'Authentication token (unused for now,' . + ' but at least pass undef here)'}, + {type => 'object', desc => 'Filter object: see notes for details'}, + {type => 'bool', desc => 'Return whole objects instead of IDs?'} + ], + return => { desc => "An array of brsrc ids matching the requested filters." }, + }, + notes => <<'NOTES' + +The filter object parameter can contain the following keys: + * type => The id of a booking resource type (brt) + * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav) + * available => Either: + A timestamp during which the resources are not reserved. If the resource is overbookable, this is ignored. + A range of two timestamps which do not overlap any reservations for the resources. If the resource is overbookable, this is ignored. + * booked => Either: + A timestamp during which the resources are reserved. + A range of two timestamps which overlap a reservation of the resources. + +Note that at least one of 'type' or 'attribute_values' is required. + +NOTES +); + + +sub reservation_list_by_filters { + my $self = shift; + my $client = shift; + my $auth = shift; + my $filters = shift; + my $whole_obj = shift; + + return undef unless ($filters->{user} || $filters->{user_barcode} || $filters->{resource} || $filters->{type} || $filters->{attribute_values}); + + my $e = new_editor(authtoken=>$auth); + return $e->event unless $e->checkauth; + return $e->event unless $e->allowed('VIEW_TRANSACTION'); + + my $query = { + 'select' => { bresv => [ 'id', 'start_time' ] }, + 'from' => { bresv => {} }, + 'where' => {}, + 'order_by' => [{ class => bresv => field => start_time => direction => 'asc' }], + 'distinct' => 1 + }; + + if ($filters->{fields}) { + $query->{where} = $filters->{fields}; + } + + + if ($filters->{user}) { + $query->{where}->{usr} = $filters->{user}; + } + elsif ($filters->{user_barcode}) { # just one of user and user_barcode + my $usr = $U->fetch_user_by_barcode($filters->{user_barcode}); + return $usr if ref($usr) eq 'HASH' and exists($usr->{"ilsevent"}); + $query->{where}->{usr} = $usr->id; + } + + + if ($filters->{type}) { + $query->{where}->{target_resource_type} = $filters->{type}; + } + + if ($filters->{resource}) { + $query->{where}->{target_resource} = $filters->{resource}; + } + + if ($filters->{attribute_values}) { + + $query->{from}->{bresv}->{bravm} = { field => 'reservation' }; + + $filters->{attribute_values} = [$filters->{attribute_values}] + if (!ref($filters->{attribute_values})); + + $query->{having}->{'+bravm'}->{attr_value}->{'@>'} = { + transform => 'array_accum', + value => '$_' . $$ . '${' . + join(',', @{$filters->{attribute_values}}) . + '}$_' . $$ . '$' + }; + } + + if ($filters->{search_start} || $filters->{search_end}) { + $query->{where}->{'-or'} = {}; + + $query->{where}->{'-or'}->{start_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] } + if ($filters->{search_start}); + + $query->{where}->{'-or'}->{end_time} = { 'between' => [ $filters->{search_start}, $filters->{search_end} ] } + if ($filters->{search_end}); + } + + my $cstore = OpenSRF::AppSession->connect('open-ils.cstore'); + my $ids = [ map { $_->{id} } @{ + $cstore->request( + 'open-ils.cstore.json_query.atomic', $query + )->gather(1) + } ]; + $cstore->disconnect; + + if (not $whole_obj or @$ids < 1) { + $e->disconnect; + return $ids; + } + + my $bresv_list = $e->search_booking_reservation([ + {"id" => $ids}, + {"flesh" => 1, + "flesh_fields" => { + "bresv" => + [qw/target_resource current_resource target_resource_type/] + } + }] + ); + $e->disconnect; + return $bresv_list ? $bresv_list : []; +} +__PACKAGE__->register_method( + method => "reservation_list_by_filters", + api_name => "open-ils.booking.reservations.filtered_id_list", + argc => 2, + signature=> { + params => [ + {type => 'string', desc => 'Authentication token'}, + {type => 'object', desc => 'Filter object -- see notes for details'} + ], + return => { desc => "An array of bresv ids matching the requested filters." }, + }, + notes => <<'NOTES' + +The filter object parameter can contain the following keys: + * user => The id of a user that has requested a bookable item -- filters on bresv.usr + * barcode => The barcode of a user that has requested a bookable item + * type => The id of a booking resource type (brt) -- filters on bresv.target_resource_type + * resource => The id of a booking resource (brsrc) -- filters on bresv.target_resource + * attribute_values => The ids of booking resource type attribute values that the resource must have assigned to it (brav) + * search_start => If search_end is not specified, booking interval (start_time to end_time) must contain this timestamp. + * search_end => If search_start is not specified, booking interval (start_time to end_time) must contain this timestamp. + * fields => An object containing any combination of bresv search filters in standard cstore/pcrud search format. + +Note that at least one of 'user', 'type', 'resource' or 'attribute_values' is required. If both search_start and search_end are specified, +then the result includes any reservations that overlap with that time range. Any filter fields supplied in 'fields' are overridden +by the top-level filters ('user', 'type', 'resource'). + +NOTES +); + + +sub naive_ts_string {strftime("%F %T", localtime($_[0] || time));} +sub naive_start_of_day {strftime("%F", localtime($_[0] || time))." 00:00:00";} + +# Return a list of bresv or an ilsevent on failure. +sub get_uncaptured_bresv_for_brsrc { + my ($e, $o) = @_; # o's keys (all optional): owning_lib, barcode, range + + my $from_clause = { + "bresv" => { + "brsrc" => {"field" => "id", "fkey" => "current_resource"} + } + }; + + my $query = { + "select" => { + "bresv" => [ + "current_resource", + { + "column" => "start_time", + "transform" => "min", + "aggregate" => 1 + } + ] + }, + "from" => $from_clause, + "where" => { + "-and" => [ + {"current_resource" => {"!=" => undef}}, + {"capture_time" => undef}, + {"cancel_time" => undef}, + {"return_time" => undef}, + {"pickup_time" => undef} + ] + } + }; + if ($o->{"owning_lib"}) { + push @{$query->{"where"}->{"-and"}}, + {"+brsrc" => {"owner" => $o->{"owning_lib"}}}; + } + if ($o->{"range"}) { + push @{$query->{"where"}->{"-and"}}, + json_query_ranges_overlap( + $o->{"range"}->[0], $o->{"range"}->[1], + "start_time", "end_time" + ); + } + if ($o->{"barcode"}) { + push @{$query->{"where"}->{"-and"}}, + {"+brsrc" => {"barcode" => $o->{"barcode"}}}; + } + + my $rows = $e->json_query($query); + my $current_resource_bresv_map = {}; + if (@$rows) { + my $id_query = { + "select" => {"bresv" => ["id"]}, + "from" => $from_clause, + "where" => { + "-and" => [ + {"current_resource" => "PLACEHOLDER"}, + {"start_time" => "PLACEHOLDER"}, + ] + } + }; + if ($o->{"owning_lib"}) { + push @{$id_query->{"where"}->{"-and"}}, + {"+brsrc" => {"owner" => $o->{"owning_lib"}}}; + } + + foreach (@$rows) { + $id_query->{"where"}->{"-and"}->[0]->{"current_resource"} = + $_->{"current_resource"}; + $id_query->{"where"}->{"-and"}->[1]->{"start_time"} = + $_->{"start_time"}; + + my $results = $e->json_query($id_query); + if ($results && @$results) { + $current_resource_bresv_map->{$_->{"current_resource"}} = + [map { $_->{"id"} } @$results]; + } + } + } + return $current_resource_bresv_map; +} + +sub get_pull_list { + my ($self, $client, $auth, $range, $interval_secs, $owning_lib) = @_; + + my $e = new_editor(xact => 1, authtoken => $auth); + return $e->die_event unless $e->checkauth; + return $e->die_event unless $e->allowed("RETRIEVE_RESERVATION_PULL_LIST"); + return $e->die_event unless ( + ref($range) eq "ARRAY" or + ($interval_secs = int($interval_secs)) > 0 + ); + + $owning_lib = $e->requestor->ws_ou if not $owning_lib; + $range = [ naive_ts_string(time), naive_ts_string(time + $interval_secs) ] + if not $range; + + my $uncaptured = get_uncaptured_bresv_for_brsrc( + $e, {"range" => $range, "owning_lib" => $owning_lib} + ); + + if (keys(%$uncaptured)) { + my @all_bresv_ids = map { @{$_} } values %$uncaptured; + my %bresv_lookup = ( + map { $_->id => $_ } @{ + $e->search_booking_reservation([{"id" => [@all_bresv_ids]}, { + flesh => 1, + flesh_fields => { bresv => [ + "usr", "target_resource_type", "current_resource" + ]} + }]) + } + ); + $e->disconnect; + return [ map { + my $key = $_; + my $one = $bresv_lookup{$uncaptured->{$key}->[0]}; + my $result = { + "current_resource" => $one->current_resource, + "target_resource_type" => $one->target_resource_type, + "reservations" => [ + map { $bresv_lookup{$_} } @{$uncaptured->{$key}} + ] + }; + foreach (@{$result->{"reservations"}}) { # deflesh + $_->current_resource($_->current_resource->id); + $_->target_resource_type($_->target_resource_type->id); + } + $result; + } keys %$uncaptured ]; + } else { + $e->disconnect; + return []; + } +} +__PACKAGE__->register_method( + method => "get_pull_list", + api_name => "open-ils.booking.reservations.get_pull_list", + argc => 4, + signature=> { + params => [ + {type => "string", desc => "Authentication token"}, + {type => "array", desc => + "range: Date/time range for reservations (opt)"}, + {type => "int", desc => + "interval: Seconds from now (instead of range)"}, + {type => "number", desc => "(Optional) Owning library"} + ], + return => { desc => "An array of hashes, each containing key/value " . + "pairs describing resource, resource type, and a list of " . + "reservations that claim the given resource." } + } +); + + +sub get_copy_fleshed_just_right { + my ($self, $client, $auth, $barcode) = @_; + + return undef if not defined $barcode; + return {} if ref($barcode) eq "ARRAY" and not @$barcode; + + my $e = new_editor(authtoken => $auth); + my $results = $e->search_asset_copy([ + {"barcode" => $barcode}, + { + "flesh" => 1, + "flesh_fields" => {"acp" => [qw/call_number location/]} + } + ]); + + if (ref($results) eq "ARRAY") { + $e->disconnect; + return $results->[0] unless ref $barcode; + return +{ map { $_->barcode => $_ } @$results }; + } else { + return $e->die_event; + } +} +__PACKAGE__->register_method( + method => "get_copy_fleshed_just_right", + api_name => "open-ils.booking.asset.get_copy_fleshed_just_right", + argc => 2, + signature=> { + params => [ + {type => "string", desc => "Authentication token"}, + {type => "mixed", desc => "One barcode or an array of them"}, + ], + return => { desc => + "A copy, or a hash of copies keyed by barcode if an array of " . + "barcodes was given" + } + } +); + + +sub best_bresv_candidate { + my ($e, $id_list) = @_; + + # This will almost always be the case. + return $id_list->[0] if @$id_list == 1; + + my @here = (); + my $this_ou = $e->requestor->ws_ou; + my $results = $e->json_query({ + "select" => {"brsrc" => ["pickup_lib"], "bresv" => ["id"]}, + "from" => { + "bresv" => { + "brsrc" => {"field" => "id", "fkey" => "current_resource"} + } + }, + "where" => { + {"+bresv" => {"id" => $id_list}} + } + }); + + foreach (@$results) { + push @here, $_->{"id"} if $_->{"pickup_lib"} == $this_ou; + } + + if (@here > 0) { + return pop @here if @here == 1; + return (sort @here)[0]; + } else { + return (sort @$id_list)[0]; + } +} + + +sub capture_resource_for_reservation { + my ($self, $client, $auth, $barcode) = @_; + + my $e = new_editor(xact => 1, authtoken => $auth); + return $e->die_event unless $e->checkauth; + return $e->die_event unless $e->allowed("CAPTURE_RESERVATION"); + + my $uncaptured = get_uncaptured_bresv_for_brsrc( + $e, {"barcode" => $barcode} + ); + $e->disconnect; + + if (keys %$uncaptured) { + # Note this will only capture one reservation at a time, even in + # cases with overbooking (multiple "soonest" bresv's on a resource). + my $key = (sort(keys %$uncaptured))[0]; + return capture_reservation( + $self, $client, $auth, best_bresv_candidate($e, $uncaptured->{$key}) + ); + } else { + return new OpenILS::Event( + "RESERVATION_NOT_FOUND", + desc => "No capturable reservation found pertaining " . + "to a resource with barcode $barcode", + payload => {fail_cause => 'no-reservation', captured => 0} + ); + } +} +__PACKAGE__->register_method( + method => "capture_resource_for_reservation", + api_name => "open-ils.booking.resources.capture_for_reservation", + argc => 3, + signature=> { + params => [ + {type => "string", desc => "Authentication token"}, + {type => "string", desc => "Barcode of booked & targeted resource"}, + {type => "int", desc => "Pickup library (default to client ws_ou)"}, + ], + return => { desc => "An OpenILS event describing the capture outcome" } + } +); + + +sub capture_reservation { + my ($self, $client, $auth, $res_id) = @_; + + my $e = new_editor(xact => 1, authtoken => $auth); + return $e->event unless $e->checkauth; + return $e->event unless $e->allowed('CAPTURE_RESERVATION'); + my $here = $e->requestor->ws_ou; + + my $reservation = $e->retrieve_booking_reservation([ + $res_id, { + flesh => 2, + flesh_fields => {"bresv" => ["usr"], "au" => ["card"]} + } + ]); + return OpenILS::Event->new('RESERVATION_NOT_FOUND') unless $reservation; + + return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'no-resource' }) + if (!$reservation->current_resource); # no resource + + return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'cancelled' }) + if ($reservation->cancel_time); # canceled + + my $resource = $e->retrieve_booking_resource( $reservation->current_resource ); + my $type = $e->retrieve_booking_resource_type( $resource->type ); + + $reservation->capture_staff( $e->requestor->id ); + $reservation->capture_time( 'now' ); + + my $reservation_id = undef; + return $e->event unless ( $e->update_booking_reservation( $reservation ) and $reservation_id = $e->data ); + + $reservation->id($reservation_id); + + my $ret = { captured => 1, reservation => $reservation }; + + if ($here <> $reservation->pickup_lib) { + return OpenILS::Event->new('RESERVATION_CAPTURE_FAILED', payload => { captured => 0, fail_cause => 'not-transferable' }) + if (!$U->is_true($type->transferable)); # non-transferable resource + + # need to transit the item ... is it already in transit? + my $transit = $e->search_action_reservation_transit_copy( { reservation => $res_id, dest_recv_time => undef } )->[0]; + + if (!$transit) { # not yet in transit + $transit = new Fieldmapper::action::reservation_transit_copy; + + $transit->reservation($reservation->id); + $transit->target_copy($resource->id); + $transit->copy_status(15); + $transit->source_send_time('now'); + $transit->source($here); + $transit->dest($reservation->pickup_lib); + + $e->create_action_reservation_transit_copy( $transit ); + + if ($U->is_true($type->catalog_item)) { + my $copy = $e->search_asset_copy( { barcode => $resource->barcode, deleted => 'f' } )->[0]; + + if ($copy) { + return new OpenILS::Event( + "OPEN_CIRCULATION_EXISTS", + payload => { captured => 0, copy => $copy } + ) if $copy->status == 1; + $copy->status(6); + $e->update_asset_copy( $copy ); + $$ret{catalog_item} = $copy; # $e->data is just id (int) + } + } + } + + $$ret{transit} = $transit; + } elsif ($U->is_true($type->catalog_item)) { + my $copy = $e->search_asset_copy( { barcode => $resource->barcode, deleted => 'f' } )->[0]; + + if ($copy) { + return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => { captured => 0, copy => $copy }) if ($copy->status == 1); + $copy->status(15); + $e->update_asset_copy( $copy ); + $$ret{catalog_item} = $copy; # $e->data is just id (int) + } + } + + $e->commit; + + return OpenILS::Event->new('SUCCESS', payload => $ret); +} +__PACKAGE__->register_method( + method => "capture_reservation", + api_name => "open-ils.booking.reservations.capture", + argc => 2, + signature=> { + params => [ + {type => 'string', desc => 'Authentication token'}, + {type => 'mixed', desc => + 'Reservation ID (number) or array of resource barcodes'} + ], + return => { desc => "An OpenILS Event object describing the outcome of the capture, with relevant payload." }, + } +); + + +sub cancel_reservation { + my ($self, $client, $auth, $id_list) = @_; + + my $e = new_editor(xact => 1, authtoken => $auth); + return $e->die_event unless $e->checkauth; + # Should the following permission really be checked as relates to each + # individual reservation's request_lib? Hrmm... + return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION"); + + my $bresv_list = $e->search_booking_reservation([ + {"id" => $id_list}, + {"flesh" => 1, "flesh_fields" => {"bresv" => [ + "current_resource", "target_resource_type" + ]}} + ]); + return $e->die_event if not $bresv_list; + + my $circ = OpenSRF::AppSession->connect("open-ils.circ") or + return $e->die_event; + my @results = (); + foreach my $bresv (@$bresv_list) { + if ( + $bresv->target_resource_type->catalog_item == "t" && + $bresv->current_resource + ) { + $logger->info("result of no-op checkin (upon cxl bresv) is " . + $circ->request( + "open-ils.circ.checkin", $auth, + {"barcode" => $bresv->current_resource->barcode, + "noop" => 1} + )->gather(1)->{"textcode"}); + } + $bresv->cancel_time("now"); + $e->update_booking_reservation($bresv) or do { + $circ->disconnect; + return $e->die_event; + }; + + push @results, $bresv->id; + } + + $e->commit; + $circ->disconnect; + + return \@results; +} +__PACKAGE__->register_method( + method => "cancel_reservation", + api_name => "open-ils.booking.reservations.cancel", + argc => 2, + signature=> { + params => [ + {type => "string", desc => "Authentication token"}, + {type => "array", desc => "List of reservation IDs"} + ], + return => { desc => "A list of canceled reservation IDs" }, + } +); + + +sub get_captured_reservations { + my ($self, $client, $auth, $barcode, $which) = @_; + + my $e = new_editor(xact => 1, authtoken => $auth); + return $e->die_event unless $e->checkauth; + return $e->die_event unless $e->allowed("VIEW_USER"); + return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION"); + + # fetch the patron for our uses in any case... + my $patron = $U->fetch_user_by_barcode($barcode); + return $patron if ref($patron) eq "HASH" and exists $patron->{"ilsevent"}; + + my $bresv_flesh = { + "flesh" => 1, + "flesh_fields" => {"bresv" => [ + qw/target_resource_type current_resource/ + ]} + }; + + my $dispatch = { + "patron" => sub { + return $patron; + }, + "ready" => sub { + return $e->search_booking_reservation([ + { + "usr" => $patron->id, + "capture_time" => {"!=" => undef}, + "pickup_time" => undef, + "start_time" => {">=" => naive_start_of_day()}, + "cancel_time" => undef + }, + $bresv_flesh + ]) or $e->die_event; + }, + "out" => sub { + return $e->search_booking_reservation([ + { + "usr" => $patron->id, + "pickup_time" => {"!=" => undef}, + "return_time" => undef, + "cancel_time" => undef + }, + $bresv_flesh + ]) or $e->die_event; + }, + "in" => sub { + return $e->search_booking_reservation([ + { + "usr" => $patron->id, + "return_time" => {">=" => naive_start_of_day()}, + "cancel_time" => undef + }, + $bresv_flesh + ]) or $e->die_event; + } + }; + + my $result = {}; + foreach (@$which) { + my $f = $dispatch->{$_}; + if ($f) { + my $r = &{$f}(); + return $r if (ref($r) eq "HASH" and exists $r->{"ilsevent"}); + $result->{$_} = $r; + } + } + + return $result; +} +__PACKAGE__->register_method( + method => "get_captured_reservations", + api_name => "open-ils.booking.reservations.get_captured", + argc => 3, + signature=> { + params => [ + {type => "string", desc => "Authentication token"}, + {type => "string", desc => "Patron barcode"}, + {type => "array", desc => "Parts wanted (patron, ready, out, in?)"} + ], + return => { desc => "A hash of parts." } # XXX describe more fully + } +); + + +sub get_bresv_by_returnable_resource_barcode { + my ($self, $client, $auth, $barcode) = @_; + + my $e = new_editor(xact => 1, authtoken => $auth); + return $e->die_event unless $e->checkauth; + return $e->die_event unless $e->allowed("VIEW_USER"); + return $e->die_event unless $e->allowed("ADMIN_BOOKING_RESERVATION"); + + my $rows = $e->json_query({ + "select" => {"bresv" => ["id"]}, + "from" => { + "bresv" => { + "brsrc" => {"field" => "id", "fkey" => "current_resource"} + } + }, + "where" => { + "+brsrc" => {"barcode" => $barcode}, + "-and" => { + "pickup_time" => {"!=" => undef}, + "cancel_time" => undef, + "return_time" => undef + } + } + }) or return $e->die_event; + + if (@$rows < 1) { + return $rows; + } else { + # More than one result might be possible, but we don't want to return + # more than one at this time. + my $id = $rows->[0]->{"id"}; + return $e->retrieve_booking_reservation([ + $id, { + "flesh" => 2, + "flesh_fields" => { + "bresv" => [qw/usr target_resource_type current_resource/], + "au" => ["card"] + } + } + ]) or $e->die_event; + } +} + +__PACKAGE__->register_method( + method => "get_bresv_by_returnable_resource_barcode", + api_name => "open-ils.booking.reservations.by_returnable_resource_barcode", + argc => 2, + signature=> { + params => [ + {type => "string", desc => "Authentication token"}, + {type => "string", desc => "Resource barcode"}, + ], + return => { desc => "A fleshed bresv or an ilsevent on error" } + } +); + + +1; diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Circ.pm b/Open-ILS/src/perlmods/OpenILS/Application/Circ.pm index e9867b1dd4..8aae114a72 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Circ.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Circ.pm @@ -1105,6 +1105,144 @@ sub test_batch_circ_events { } +# XXX +# XXX !!! HERE through line 1245 !!! +# XXX Backported from trunk aprox 15310, may not be supported or required by 1.6 +# XXX + +__PACKAGE__->register_method( + method => "user_payments_list", + api_name => "open-ils.circ.user_payments.filtered.batch", + stream => 1, + signature => { + desc => q/Returns a fleshed, date-limited set of all payments a user + has made. By default, ordered by payment date. Optionally + ordered by other columns in the top-level "mp" object/, + params => [ + {desc => 'Authentication token', type => 'string'}, + {desc => 'User ID', type => 'number'}, + {desc => 'Order by column(s), optional. Array of "mp" class columns', type => 'array'} + ], + return => {desc => q/List of "mp" objects, fleshed with the billable transaction + and the related fully-realized payment object (e.g money.cash_payment)/} + } +); + +sub user_payments_list { + my($self, $conn, $auth, $user_id, $start_date, $end_date, $order_by) = @_; + + my $e = new_editor(authtoken => $auth); + return $e->event unless $e->checkauth; + + my $user = $e->retrieve_actor_user($user_id) or return $e->event; + return $e->event unless $e->allowed('VIEW_CIRCULATIONS', $user->home_ou); + + $order_by ||= ['payment_ts']; + + # all payments by user, between start_date and end_date + my $payments = $e->json_query({ + select => {mp => ['id']}, + from => { + mp => { + mbt => { + fkey => 'xact', field => 'id'} + } + }, + where => { + '+mbt' => {usr => $user_id}, + '+mp' => {payment_ts => {between => [$start_date, $end_date]}} + }, + order_by => {mp => $order_by} + }); + + for my $payment_id (@$payments) { + my $payment = $e->retrieve_money_payment([ + $payment_id->{id}, + { + flesh => 2, + flesh_fields => { + mp => [ + 'xact', + 'cash_payment', + 'credit_card_payment', + 'credit_payment', + 'check_payment', + 'work_payment', + 'forgive_payment', + 'goods_payment' + ], + mbt => [ + 'circulation', + 'grocery', + 'reservation' + ] + } + } + ]); + $conn->respond($payment); + } + + return undef; +} + + +__PACKAGE__->register_method( + method => "retrieve_circ_chain", + api_name => "open-ils.circ.renewal_chain.retrieve_by_circ", + stream => 1, + signature => { + desc => q/Given a circulation, this returns all circulation objects + that are part of the same chain of renewals./, + params => [ + {desc => 'Authentication token', type => 'string'}, + {desc => 'Circ ID', type => 'number'}, + ], + return => {desc => q/List of circ objects, orderd by oldest circ first/} + } +); + +__PACKAGE__->register_method( + method => "retrieve_circ_chain", + api_name => "open-ils.circ.renewal_chain.retrieve_by_circ.summary", + signature => { + desc => q/Given a circulation, this returns all circulation objects + that are part of the same chain of renewals./, + params => [ + {desc => 'Authentication token', type => 'string'}, + {desc => 'Circ ID', type => 'number'}, + ], + return => {desc => q/List of circ objects, orderd by oldest circ first/} + } +); + +sub retrieve_circ_chain { + my($self, $conn, $auth, $circ_id) = @_; + + my $e = new_editor(authtoken => $auth); + return $e->event unless $e->checkauth; + return $e->event unless $e->allowed('VIEW_CIRCULATIONS'); + + if($self->api_name =~ /summary/) { + my $sum = $e->json_query({from => ['action.summarize_circ_chain', $circ_id]})->[0]; + return undef unless $sum; + my $obj = Fieldmapper::action::circ_chain_summary->new; + $obj->$_($sum->{$_}) for keys %$sum; + return $obj; + + } else { + + my $chain = $e->json_query({from => ['action.circ_chain', $circ_id]}); + + for my $circ_info (@$chain) { + my $circ = Fieldmapper::action::circulation->new; + $circ->$_($circ_info->{$_}) for keys %$circ_info; + $conn->respond($circ); + } + } + + return undef; +} + # {"select":{"acp":["id"],"circ":[{"aggregate":true,"transform":"count","alias":"count","column":"id"}]},"from":{"acp":{"circ":{"field":"target_copy","fkey":"id","type":"left"},"acn"{"field":"id","fkey":"call_number"}}},"where":{"+acn":{"record":200057}} diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm b/Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm index 6b56bbb928..13061457a1 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm @@ -2,10 +2,12 @@ package OpenILS::Application::Circ::Circulate; use strict; use warnings; use base 'OpenILS::Application'; use OpenSRF::EX qw(:try); +use OpenSRF::AppSession; use OpenSRF::Utils::SettingsClient; use OpenSRF::Utils::Logger qw(:logger); use OpenILS::Const qw/:const/; use OpenILS::Application::AppUtils; +use DateTime; my $U = "OpenILS::Application::AppUtils"; my %scripts; @@ -148,6 +150,13 @@ __PACKAGE__->register_method( __PACKAGE__->register_method( method => "run_method", + api_name => "open-ils.circ.reservation.pickup"); +__PACKAGE__->register_method( + method => "run_method", + api_name => "open-ils.circ.reservation.return"); + +__PACKAGE__->register_method( + method => "run_method", api_name => "open-ils.circ.checkout.inspect", desc => q/ Returns the circ matrix test result and, on success, the rule set and matrix test object @@ -167,9 +176,98 @@ sub run_method { return circ_events($circulator) if $circulator->bail_out; # -------------------------------------------------------------------------- + # First, check for a booking transit, as the barcode may not be a copy + # barcode, but a resource barcode, and nothing else in here will work + # -------------------------------------------------------------------------- + + if ((my $bc = $circulator->copy_barcode) && $api !~ /checkout|inspect/) { # do we have a barcode? + my $resources = $circulator->editor->search_booking_resource( { barcode => $bc } ); # any resources by this barcode? + if (@$resources) { # yes! + + my $res_id_list = [ map { $_->id } @$resources ]; + my $transit = $circulator->editor->search_action_reservation_transit_copy( + [ + { target_copy => $res_id_list, dest => $circulator->circ_lib }, + { order_by => { artc => 'source_send_time' }, limit => 1 } + ] + )->[0]; # Any transit for this barcode? + + if ($transit) { # yes! unwrap it. + + my $reservation = $circulator->editor->retrieve_booking_reservation( $transit->reservation ); + my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type ); + + if ($U->is_true($res_type->catalog_item)) { # is there a copy to be had here? + if (my $copy = $circulator->editor->search_asset_copy({ barcode => $bc, deleted => 'f' })->[0]) { # got a copy + $copy->status( $transit->copy_status ); + $copy->editor($circulator->editor->requestor->id); + $copy->edit_date('now'); + $circulator->editor->update_asset_copy( $copy ); + } + } + + $transit->dest_recv_time('now'); + $circulator->editor->update_action_reservation_transit_copy( $transit ); + + $circulator->editor->commit; + + #XXX need to return here, with info about the resource/copy and the "put it on the booking shelf" message + + } else { # no transit, look for an upcoming reservation to capture for + + my $reservation = $circulator->editor->search_booking_reservation( + [ + { current_resource => $res_id_list, + pickup_lib => $circulator->circ_lib, + cancel_time => undef, + capture_time => undef + }, + { order_by => { bresv => 'start_time' }, limit => 1 } + ] + )->[0]; + + if ($reservation) { # we have a reservation for which we could capture this resource. wheee! + my $res_type = $circulator->editor->retrieve_booking_resource_type( $reservation->target_resource_type ); + my $elbow_room = $res_type->elbow_room || + $U->ou_ancestor_setting_value( $circulator->circ_lib, 'circ.booking_reservation.default_elbow_room', $circulator->editor ); + + if ($elbow_room) { + $reservation = $circulator->editor->search_booking_reservation( + [ + { id => $reservation->id, start_time => { '<=' => DateTime->now->add( seconds => interval_to_seconds($elbow_room) )->strftime('%FT%T%z') } }, + { order_by => { bresv => 'start_time' }, limit => 1 } + ] + )->[0]; + } + + if ($reservation) { # no elbow room specified, or we still have a reservation within the elbow_room time + my $b_ses = OpenSRF::AppSession->create('open-ils.booking'); + my $result = $b_ses->request( + 'open-ils.booking.reservations.capture', + $auth => $reservation->id + )->gather(1); + + if (ref($result) && $result->{ilsevent} == 0) { # captured! + #XXX what to return here??? + return $result; # the booking capture success + } else { + #XXX how to fail??? Probably, just move on. + } + } + } + } + } + } + + + + # -------------------------------------------------------------------------- # Go ahead and load the script runner to make sure we have all # of the objects we need # -------------------------------------------------------------------------- + $circulator->is_res_checkin($circulator->is_checkin(1)) if $api =~ /reservation.return/; + $circulator->is_res_checkout(1) if $api =~ /reservation.pickup/; + $circulator->is_renewal(1) if $api =~ /renew/; $circulator->is_checkin(1) if $api =~ /checkin/; @@ -180,7 +278,7 @@ sub run_method { $circulator->circ_permit_copy($scripts{circ_permit_copy}); $circulator->circ_duration($scripts{circ_duration}); $circulator->circ_permit_renew($scripts{circ_permit_renew}); - } else { + } elsif (not $circulator->is_res_checkin) { # mk_env cannot work w/ reservation.return $circulator->mk_env(); } return circ_events($circulator) if $circulator->bail_out; @@ -203,6 +301,9 @@ sub run_method { $circulator->do_checkout(); } + } elsif( $circulator->is_res_checkout ) { + $circulator->do_reservation_pickup(); + } elsif( $api =~ /inspect/ ) { my $data = $circulator->do_inspect(); $circulator->editor->rollback; @@ -212,6 +313,9 @@ sub run_method { $circulator->is_checkout(1); $circulator->do_checkout(); + } elsif( $circulator->is_res_checkin ) { + $circulator->do_reservation_return(); + $circulator->do_checkin() if ($circulator->copy()); } elsif( $api =~ /checkin/ ) { $circulator->do_checkin(); @@ -335,6 +439,7 @@ my @AUTOLOAD_FIELDS = qw/ notify_hold remote_hold backdate + reservation copy copy_id copy_barcode @@ -346,10 +451,12 @@ my @AUTOLOAD_FIELDS = qw/ title is_renewal is_checkout + is_res_checkout is_noncat is_precat request_precat is_checkin + is_res_checkin noncat_type editor events @@ -1163,7 +1270,10 @@ sub do_checkout { $self->build_checkout_circ_object(); return if $self->bail_out; - $self->apply_modified_due_date(); + my $modify_to_start = $self->booking_adjusted_due_date(); + return if $self->bail_out; + + $self->apply_modified_due_date($modify_to_start); return if $self->bail_out; return $self->bail_on_events($self->editor->event) @@ -1258,6 +1368,28 @@ sub update_copy { $copy->circ_lib($circ_lib) if $circ_lib; } +sub update_reservation { + my $self = shift; + my $reservation = $self->reservation; + + my $usr = $reservation->usr; + my $target_rt = $reservation->target_resource_type; + my $target_r = $reservation->target_resource; + my $current_r = $reservation->current_resource; + + $reservation->usr($usr->id) if ref $usr; + $reservation->target_resource_type($target_rt->id) if ref $target_rt; + $reservation->target_resource($target_r->id) if ref $target_r; + $reservation->current_resource($current_r->id) if ref $current_r; + + return $self->bail_on_events($self->editor->event) + unless $self->editor->update_booking_reservation($self->reservation); + + my $evt; + ($reservation, $evt) = $U->fetch_booking_reservation($reservation->id); + $self->reservation($reservation); +} + sub bail_on_events { my( $self, @evts ) = @_; @@ -1340,6 +1472,7 @@ sub handle_checkout_holds { sub run_checkout_scripts { my $self = shift; + my $nobail = shift; my $evt; my $runner = $self->script_runner; @@ -1374,13 +1507,13 @@ sub run_checkout_scripts { unless($duration) { ($duration, $evt) = $U->fetch_circ_duration_by_name($duration_name); - return $self->bail_on_events($evt) if $evt; + return $self->bail_on_events($evt) if ($evt && !$nobail); ($recurring, $evt) = $U->fetch_recurring_fine_by_name($recurring_name); - return $self->bail_on_events($evt) if $evt; + return $self->bail_on_events($evt) if ($evt && !$nobail); ($max_fine, $evt) = $U->fetch_max_fine_by_name($max_fine_name); - return $self->bail_on_events($evt) if $evt; + return $self->bail_on_events($evt) if ($evt && !$nobail); } } else { @@ -1462,9 +1595,151 @@ sub build_checkout_circ_object { $self->circ($circ); } +sub do_reservation_pickup { + my $self = shift; + + $self->log_me("do_reservation_pickup()"); + + $self->reservation->pickup_time('now'); + + if ( + $self->reservation->current_resource && + $self->reservation->current_resource->catalog_item + ) { + $self->copy( $self->reservation->current_resource->catalog_item ); + $self->patron( $self->reservation->usr ); + $self->run_checkout_scripts(1); + + my $duration = $self->duration_rule; + my $max = $self->max_fine_rule; + my $recurring = $self->recurring_fines_rule; + + if ($duration && $max && $recurring) { + my $policy = $self->get_circ_policy($duration, $recurring, $max); + + my $dname = $duration->name; + my $mname = $max->name; + my $rname = $recurring->name; + + $logger->debug("circulator: building reservation ". + "with duration=$dname, maxfine=$mname, recurring=$rname"); + + $self->reservation->fine_amount($policy->{recurring_fine}); + $self->reservation->max_fine($policy->{max_fine}); + $self->reservation->fine_interval($recurring->recurrence_interval); + } + + $self->copy->status(OILS_COPY_STATUS_CHECKED_OUT); + $self->update_copy(); + + } else { + $self->reservation->fine_amount($self->reservation->fine_amount); + $self->reservation->max_fine($self->reservation->max_fine); + $self->reservation->fine_interval($self->reservation->fine_interval); + } + + $self->update_reservation(); +} + +sub do_reservation_return { + my $self = shift; + my $request = shift; + + $self->log_me("do_reservation_return()"); + + my ($reservation, $evt) = $U->fetch_booking_reservation($self->reservation); + return $self->bail_on_events($evt) if $evt; + + $self->reservation( $reservation ); + $self->generate_fines(1); + $self->reservation->return_time('now'); + $self->update_reservation(); + + if ( $self->reservation->current_resource && $self->reservation->current_resource->catalog_item ) { + $self->copy( $self->reservation->current_resource->catalog_item ); + } +} + +sub booking_adjusted_due_date { + my $self = shift; + my $circ = $self->circ; + my $copy = $self->copy; + + + my $changed; + + if( $self->due_date ) { + + return $self->bail_on_events($self->editor->event) + unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib); + + $circ->due_date(clense_ISO8601($self->due_date)); + + } else { + + return unless $copy and $circ->due_date; + } + + my $booking_items = $self->editor->search_booking_resource( { barcode => $copy->barcode } ); + if (@$booking_items) { + my $booking_item = $booking_items->[0]; + my $resource_type = $self->editor->retrieve_booking_resource_type( $booking_item->type ); + + my $stop_circ_setting = $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.stop_circ', $self->editor ); + my $shorten_circ_setting = $resource_type->elbow_room || + $U->ou_ancestor_setting_value( $self->circ_lib, 'circ.booking_reservation.default_elbow_room', $self->editor ) || + '0 seconds'; + + my $booking_ses = OpenSRF::AppSession->create( 'open-ils.booking' ); + my $bookings = $booking_ses->request( + 'open-ils.booking.reservations.filtered_id_list', $self->editor->authtoken, + { resource => $booking_item->id, search_start => 'now', search_end => $circ->due_date } + )->gather(1); + $booking_ses->disconnect; + + my $dt_parser = DateTime::Format::ISO8601->new; + my $due_date = $dt_parser->parse_datetime( clense_ISO8601($circ->due_date) ); + + for my $bid (@$bookings) { + + my $booking = $self->editor->retrieve_booking_reservation( $bid ); + + my $booking_start = $dt_parser->parse_datetime( clense_ISO8601($booking->start_time) ); + my $booking_end = $dt_parser->parse_datetime( clense_ISO8601($booking->end_time) ); + + return $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) + if ($booking_start < DateTime->now); + + + if ($U->is_true($stop_circ_setting)) { + $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ); + } else { + $due_date = $booking_start->subtract( seconds => interval_to_seconds($shorten_circ_setting) ); + $self->bail_on_events( OpenILS::Event->new('COPY_RESERVED') ) if ($due_date < DateTime->now); + } + + # We set the circ duration here only to affect the logic that will + # later (in a DB trigger) mangle the time part of the due date to + # 11:59pm. Having any circ duration that is not a whole number of + # days is enough to prevent the "correction." + my $new_circ_duration = $due_date->epoch - time; + $new_circ_duration++ if $new_circ_duration % 86400 == 0; + $circ->duration("$new_circ_duration seconds"); + + $circ->due_date(clense_ISO8601($due_date->strftime('%FT%T%z'))); + $changed = 1; + } + + return $self->bail_on_events($self->editor->event) + unless $self->editor->allowed('CIRC_OVERRIDE_DUE_DATE', $self->circ_lib); + } + + return $changed; +} sub apply_modified_due_date { my $self = shift; + my $shift_earlier = shift; my $circ = $self->circ; my $copy = $self->copy; @@ -1499,7 +1774,11 @@ sub apply_modified_due_date { # XXX make the behavior more dynamic # for now, we just push the due date to after the close date - $circ->due_date($dateinfo->{end}); + if ($shift_earlier) { + $circ->due_date($dateinfo->{start}); + } else { + $circ->due_date($dateinfo->{end}); + } } } } @@ -1721,7 +2000,7 @@ sub do_checkin { # this copy can fulfill a hold or needs to be routed to a different location # ------------------------------------------------------------------------------ - unless($self->noop) { # no-op checkins to not capture holds or put items into transit + if(!$self->noop) { # /not/ a no-op checkin, capture for hold or put item into transit my $needed_for_hold = (!$self->remote_hold and $self->attempt_checkin_hold_capture()); return if $self->bail_out; @@ -2110,6 +2389,53 @@ sub process_received_transit { } +# ------------------------------------------------------------------ +# Sets the shelf_time and shelf_expire_time for a newly shelved hold +# ------------------------------------------------------------------ +sub put_hold_on_shelf { + my($self, $hold) = @_; + + $hold->shelf_time('now'); + + my $shelf_expire = $U->ou_ancestor_setting_value( + $self->circ_lib, 'circ.holds.default_shelf_expire_interval', $self->editor); + + if($shelf_expire) { + my $seconds = OpenSRF::Utils->interval_to_seconds($shelf_expire); + my $expire_time = DateTime->now->add(seconds => $seconds); + $hold->shelf_expire_time($expire_time->strftime('%FT%T%z')); + } + + return undef; +} + + + +sub generate_fines { + my $self = shift; + my $reservation = shift; + my $evt; + my $obt; + + my $id = $reservation ? $self->reservation->id : $self->circ->id; + + my $st = OpenSRF::AppSession->connect('open-ils.storage'); + + $st->request( + 'open-ils.storage.action.circulation.overdue.generate_fines', + undef, + $id + )->wait_complete; + + $st->disconnect; + + # refresh the circ in case the fine generator set the stop_fines field + $self->reservation($self->editor->retrieve_booking_reservation($id)) if $reservation; + $self->circ($self->editor->retrieve_action_circulation($id)) if !$reservation; + + return undef; +} + sub checkin_handle_circ { my $self = shift; my $circ = $self->circ; @@ -2292,6 +2618,7 @@ sub check_checkin_copy_status { $status == OILS_COPY_STATUS_ON_HOLDS_SHELF || $status == OILS_COPY_STATUS_IN_TRANSIT || $status == OILS_COPY_STATUS_CATALOGING || + $status == OILS_COPY_STATUS_ON_RESV_SHELF || $status == OILS_COPY_STATUS_RESHELVING ); return OpenILS::Event->new('COPY_STATUS_LOST', payload => $copy ) diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Circ/Money.pm b/Open-ILS/src/perlmods/OpenILS/Application/Circ/Money.pm index ad8da67695..e971c47450 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Circ/Money.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Circ/Money.pm @@ -270,8 +270,22 @@ sub create_grocery_bill { __PACKAGE__->register_method( - method => 'fetch_grocery', - api_name => 'open-ils.circ.money.grocery.retrieve' + method => 'fetch_reservation', + api_name => 'open-ils.circ.booking.reservation.retrieve' +); +sub fetch_reservation { + my( $self, $conn, $auth, $id ) = @_; + my $e = new_editor(authtoken=>$auth); + return $e->event unless $e->checkauth; + return $e->event unless $e->allowed('VIEW_TRANSACTION'); # eh.. basically the same permission + my $g = $e->retrieve_booking_reservation($id) + or return $e->event; + return $g; +} + +__PACKAGE__->register_method( + method => 'fetch_grocery', + api_name => 'open-ils.circ.money.grocery.retrieve' ); sub fetch_grocery { diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Collections.pm b/Open-ILS/src/perlmods/OpenILS/Application/Collections.pm index 050a9d157e..8b72ae9a31 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Collections.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Collections.pm @@ -621,7 +621,9 @@ sub transaction_details { circulations => fetch_circ_xacts($e, $uid, $org, $start_date, $end_date), grocery => - fetch_grocery_xacts($e, $uid, $org, $start_date, $end_date) + fetch_grocery_xacts($e, $uid, $org, $start_date, $end_date), + reservations => + fetch_reservation_xacts($e, $uid, $org, $start_date, $end_date) }; # for each transaction, flesh the workstatoin on any attached payment @@ -629,6 +631,7 @@ sub transaction_details { # not just a generic payment object for my $xact ( @{$blob->{transactions}->{circulations}}, + @{$blob->{transactions}->{reservations}}, @{$blob->{transactions}->{grocery}} ) { my $ps; @@ -778,6 +781,53 @@ sub fetch_grocery_xacts { return \@data; } +sub fetch_reservation_xacts { + my $e = shift; + my $uid = shift; + my $org = shift; + my $start_date = shift; + my $end_date = shift; + + my @xacts; + $U->walk_org_tree( $org, + sub { + my $n = shift; + $logger->debug("collect: searching for open grocery xacts at " . $n->shortname); + push( @xacts, + @{ + $e->search_booking_reservation( + { + usr => $uid, + pickup_lib => $n->id, + }, + {idlist => 1} + ) + } + ); + } + ); + + my @data; + my $active_ids = fetch_active($e, \@xacts, $start_date, $end_date); + + for my $id (@$active_ids) { + push( @data, + $e->retrieve_booking_reservation( + [ + $id, + { + flesh => 1, + flesh_fields => { + bresv => [ "billings", "payments", "pickup_lib" ] } + } + ] + ) + ); + } + + return \@data; +} + # -------------------------------------------------------------- diff --git a/Open-ILS/src/perlmods/OpenILS/Application/CreditCard.pm b/Open-ILS/src/perlmods/OpenILS/Application/CreditCard.pm index 6cd94b2f5d..b7c8ea7821 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/CreditCard.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/CreditCard.pm @@ -291,9 +291,12 @@ sub retrieve_payable_balance { my $circ = $e->retrieve_action_circulation($xact->id) or return $e->event; next unless grep { $_ == $circ->circ_lib } @credit_orgs; - } else { + } elsif ($xact->xact_type eq 'grocery') { my $bill = $e->retrieve_money_grocery($xact->id) or return $e->event; next unless grep { $_ == $bill->billing_location } @credit_orgs; + } elsif ($xact->xact_type eq 'reservation') { + my $bill = $e->retrieve_booking_reservation($xact->id) or return $e->event; + next unless grep { $_ == $bill->pickup_lib } @credit_orgs; } $sum += $xact->balance_owed(); } diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI.pm b/Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI.pm index 1388d816c4..419179c9f1 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI.pm @@ -5,6 +5,7 @@ use Class::DBI::AbstractSearch; use OpenILS::Application::Storage::CDBI::actor; use OpenILS::Application::Storage::CDBI::action; +use OpenILS::Application::Storage::CDBI::booking; use OpenILS::Application::Storage::CDBI::asset; use OpenILS::Application::Storage::CDBI::authority; use OpenILS::Application::Storage::CDBI::biblio; @@ -555,6 +556,15 @@ sub modify_from_fieldmapper { action::circulation->has_a( usr => 'actor::user' ); actor::user->has_many( circulations => 'action::circulation' => 'usr' ); + + booking::resource_attr_map->has_a( resource => 'booking::resource' ); + + booking::resource->has_a( owner => 'actor::org_unit' ); + booking::resource->has_a( type => 'booking::resource_type' ); + booking::resource_type->has_a( owner => 'actor::org_unit' ); + + booking::reservation->has_a( usr => 'actor::user' ); + actor::user->has_many( reservations => 'booking::reservation' => 'usr' ); action::circulation->has_a( circ_staff => 'actor::user' ); actor::user->has_many( performed_circulations => 'action::circulation' => 'circ_staff' ); @@ -565,6 +575,8 @@ sub modify_from_fieldmapper { action::circulation->has_a( target_copy => 'asset::copy' ); asset::copy->has_many( circulations => 'action::circulation' => 'target_copy' ); + booking::reservation->has_a( pickup_lib => 'actor::org_unit' ); + action::circulation->has_a( circ_lib => 'actor::org_unit' ); actor::org_unit->has_many( circulations => 'action::circulation' => 'circ_lib' ); @@ -621,9 +633,11 @@ sub modify_from_fieldmapper { action::circulation->has_many( billings => 'money::billing' => 'xact' ); action::circulation->has_many( payments => 'money::payment' => 'xact' ); #action::circulation->might_have( billable_transaction => 'money::billable_transaction' ); - #action::open_circulation->might_have( circulation => 'action::circulation' ); + booking::reservation->has_many( billings => 'money::billing' => 'xact' ); + booking::reservation->has_many( payments => 'money::payment' => 'xact' ); + action::in_house_use->has_a( org_unit => 'actor::org_unit' ); action::in_house_use->has_a( staff => 'actor::user' ); action::in_house_use->has_a( item => 'asset::copy' ); diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI/action.pm b/Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI/action.pm index c8335b4217..0d3dceb4bf 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI/action.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI/action.pm @@ -127,6 +127,16 @@ __PACKAGE__->columns(Essential => qw/source dest persistant_transfer target_copy #------------------------------------------------------------------------------- +package action::reservation_transit_copy; +use base qw/action/; +__PACKAGE__->table('action_reservation_transit_copy'); +__PACKAGE__->columns(Primary => 'id'); +__PACKAGE__->columns(Essential => qw/source dest persistant_transfer target_copy + source_send_time dest_recv_time prev_hop prev_dest + copy_status reservation/); + +#------------------------------------------------------------------------------- + package action::transit_copy; use base qw/action/; __PACKAGE__->table('action_transit_copy'); diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI/booking.pm b/Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI/booking.pm new file mode 100644 index 0000000000..e2b60fa5e1 --- /dev/null +++ b/Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI/booking.pm @@ -0,0 +1,57 @@ +package OpenILS::Application::Storage::CDBI::booking; +our $VERSION = 1; + +#------------------------------------------------------------------------------- +package booking; +use base qw/OpenILS::Application::Storage::CDBI/; +#------------------------------------------------------------------------------- + +package booking::resource_type; +use base qw/booking/; +__PACKAGE__->table('booking_resource_type'); +__PACKAGE__->columns(Primary => 'id'); +__PACKAGE__->columns(Essential => qw/name fine_interval fine_amount + max_fine owner catalog_item record transferable elbow_room/); + +#------------------------------------------------------------------------------- + +package booking::resource; +use base qw/booking/; +__PACKAGE__->table('booking_resource'); +__PACKAGE__->columns(Primary => 'id'); +__PACKAGE__->columns(Essential => qw/owner type overbook barcode deposit + deposit_amount user_fee/); + +#------------------------------------------------------------------------------- + +package booking::reservation; +use base qw/booking/; +__PACKAGE__->table('booking_reservation'); +__PACKAGE__->columns(Primary => 'id'); +__PACKAGE__->columns(Essential => qw/xact_start usr current_resource + fine_amount max_fine fine_interval xact_finish + capture_staff pickup_lib request_time start_time end_time + capture_time cancel_time pickup_time return_time + booking_interval target_resource_type target_resource + current_resource request_lib/); + +#------------------------------------------------------------------------------- + +package booking::resource_attr_map; +use base qw/booking/; +__PACKAGE__->table('booking_resource_attr_map'); +__PACKAGE__->columns(Primary => 'id'); +__PACKAGE__->columns(Essential => qw/resource resource_attr value/); + +#------------------------------------------------------------------------------- + +package booking::reservation_attr_value_map; +use base qw/booking/; +__PACKAGE__->table('booking_reservation_attr_value_map'); +__PACKAGE__->columns(Primary => 'id'); +__PACKAGE__->columns(Essential => qw/reservation attr_value/); + +#------------------------------------------------------------------------------- + +1; + diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Storage/Driver/Pg/dbi.pm b/Open-ILS/src/perlmods/OpenILS/Application/Storage/Driver/Pg/dbi.pm index 3bb16d944a..12deac5ec4 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Storage/Driver/Pg/dbi.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Storage/Driver/Pg/dbi.pm @@ -161,6 +161,36 @@ action::circulation->sequence( 'money.billable_xact_id_seq' ); #--------------------------------------------------------------------- + package booking::resource_type; + + booking::resource_type->table( 'booking.resource_type' ); + booking::resource_type->sequence( 'booking.resource_type_id_seq' ); + + #--------------------------------------------------------------------- + package booking::resource; + + booking::resource->table( 'booking.resource' ); + booking::resource->sequence( 'booking.resource_id_seq' ); + + #--------------------------------------------------------------------- + package booking::reservation; + + booking::reservation->table( 'booking.reservation' ); + booking::reservation->sequence( 'money.billable_xact_id_seq' ); + + #--------------------------------------------------------------------- + package booking::reservation_attr_value_map; + + booking::reservation_attr_value_map->table( 'booking.reservation_attr_value_map' ); + booking::reservation_attr_value_map->sequence( 'booking.reservation_attr_value_map_id_seq' ); + + #--------------------------------------------------------------------- + package booking::resource_attr_map; + + booking::resource_attr_map->table( 'booking.resource_attr_map' ); + booking::resource_attr_map->sequence( 'booking.resource_attr_map_id_seq' ); + + #--------------------------------------------------------------------- package action::non_cat_in_house_use; action::non_cat_in_house_use->table( 'action.non_cat_in_house_use' ); @@ -653,6 +683,13 @@ #------------------------------------------------------------------------------- + package action::reservation_transit_copy; + + action::reservation_transit_copy->sequence( 'action.transit_copy_id_seq' ); + action::reservation_transit_copy->table('action.reservation_transit_copy'); + + #------------------------------------------------------------------------------- + package action::transit_copy; action::transit_copy->sequence( 'action.transit_copy_id_seq' ); diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/action.pm b/Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/action.pm index 8106dd97c7..9ac0252750 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/action.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/action.pm @@ -114,8 +114,24 @@ sub overdue_circs { my $sth = action::circulation->db_Main->prepare_cached($sql); $sth->execute($upper_interval); - return ( map { action::circulation->construct($_) } $sth->fetchall_hash ); + my @circs = map { action::circulation->construct($_) } $sth->fetchall_hash; + $c_t = booking::reservation->table; + $sql = <<" SQL"; + SELECT * + FROM $c_t + WHERE return_time IS NULL + AND end_time < ( CURRENT_TIMESTAMP $grace) + AND fine_interval IS NOT NULL + AND cancel_time IS NULL + SQL + + $sth = action::circulation->db_Main->prepare_cached($sql); + $sth->execute(); + + push @circs, map { booking::reservation->construct($_) } $sth->fetchall_hash; + + return @circs; } sub complete_reshelving { @@ -597,7 +613,9 @@ sub generate_fines { my @circs; if ($circ) { - push @circs, action::circulation->search_where( { id => $circ, stop_fines => undef } ); + push @circs, + action::circulation->search_where( { id => $circ, stop_fines => undef } ), + booking::reservation->search_where( { id => $circ, return_time => undef, cancel_time => undef } ); } else { push @circs, overdue_circs($grace); } @@ -606,7 +624,22 @@ sub generate_fines { my $penalty = OpenSRF::AppSession->create('open-ils.penalty'); for my $c (@circs) { + + my $ctype = ref($c); + $ctype =~ s/^.*([^:]+)$/$1/o; + my $due_date_method = 'due_date'; + my $target_copy_method = 'target_copy'; + my $circ_lib_method = 'circ_lib'; + my $recurring_fine_method = 'recurring_fine'; + if ($ctype eq 'reservation') { + $due_date_method = 'end_time'; + $target_copy_method = 'current_resource'; + $circ_lib_method = 'pickup_lib'; + $recurring_fine_method = 'fine_amount'; + next unless ($c->fine_interval); + } + try { if ($self->method_lookup('open-ils.storage.transaction.current')->run) { $log->debug("Cleaning up after previous transaction\n"); @@ -616,7 +649,7 @@ sub generate_fines { $log->info("Processing circ ".$c->id."...\n"); - my $due_dt = $parser->parse_datetime( clense_ISO8601( $c->due_date ) ); + my $due_dt = $parser->parse_datetime( clense_ISO8601( $c->$due_date_method ) ); my $due = $due_dt->epoch; my $now = time; @@ -636,15 +669,15 @@ sub generate_fines { } $client->respond( - "ARG! Overdue circulation ".$c->id. - " for item ".$c->target_copy. + "ARG! Overdue $ctype ".$c->id. + " for item ".$c->$target_copy_method. " (user ".$c->usr.").\n". "\tItem was due on or before: ".localtime($due)."\n"); my @fines = money::billing->search_where( { xact => $c->id, btype => 1, - billing_ts => { '>' => $c->due_date } }, + billing_ts => { '>' => $c->$due_date_method } }, { order_by => 'billing_ts DESC'} ); @@ -666,7 +699,7 @@ sub generate_fines { $last_fine = $due; if (0) { - if (my $h = $hoo{$c->circ_lib}) { + if (my $h = $hoo{$c->$circ_lib_method}) { $log->info( "Circ lib has an hours-of-operation entry" ); # find the day after the due date... @@ -714,17 +747,17 @@ sub generate_fines { $client->respond( "\t$pending_fine_count pending fine(s)\n" ); - my $recuring_fine = int($c->recuring_fine * 100); + my $recurring_fine = int($c->$recurring_fine_method * 100); my $max_fine = int($c->max_fine * 100); my ($latest_billing_ts, $latest_amount) = ('',0); for (my $bill = 1; $bill <= $pending_fine_count; $bill++) { if ($current_fine_total >= $max_fine) { - $c->update({stop_fines => 'MAXFINES', stop_fines_time => 'now'}); + $c->update({stop_fines => 'MAXFINES', stop_fines_time => 'now'}) if ($ctype eq 'circulation'); $client->respond( "\tMaximum fine level of ".$c->max_fine. - " reached for this circulation.\n". + " reached for this $ctype.\n". "\tNo more fines will be generated.\n" ); last; } @@ -735,7 +768,7 @@ sub generate_fines { my $dow_open = "dow_${dow}_open"; my $dow_close = "dow_${dow}_close"; - if (my $h = $hoo{$c->circ_lib}) { + if (my $h = $hoo{$c->$circ_lib_method}) { next if ( $h->$dow_open eq '00:00:00' and $h->$dow_close eq '00:00:00'); } @@ -743,7 +776,7 @@ sub generate_fines { my @cl = actor::org_unit::closed_date->search_where( { close_start => { '<=' => $timestamptz }, close_end => { '>=' => $timestamptz }, - org_unit => $c->circ_lib } + org_unit => $c->$circ_lib_method } ); next if (@cl); @@ -772,7 +805,7 @@ sub generate_fines { # Caluclate penalties inline OpenILS::Utils::Penalty->calculate_penalties( - undef, $c->usr->to_fieldmapper->id.'', $c->circ_lib->to_fieldmapper->id.''); + undef, $c->usr->to_fieldmapper->id.'', $c->$circ_lib_method->to_fieldmapper->id.''); } else { @@ -783,7 +816,7 @@ sub generate_fines { $penalty->request( 'open-ils.penalty.patron_penalty.calculate', { patronid => ''.$c->usr, - context_org => ''.$c->circ_lib, + context_org => ''.$c->$circ_lib_method, update => 1, background => 1, } @@ -792,8 +825,8 @@ sub generate_fines { } catch Error with { my $e = shift; - $client->respond( "Error processing overdue circulation [".$c->id."]:\n\n$e\n" ); - $log->error("Error processing overdue circulation [".$c->id."]:\n$e\n"); + $client->respond( "Error processing overdue $ctype [".$c->id."]:\n\n$e\n" ); + $log->error("Error processing overdue $ctype [".$c->id."]:\n$e\n"); $self->method_lookup('open-ils.storage.transaction.rollback')->run; throw $e if ($e =~ /IS NOT CONNECTED TO THE NETWORK/o); }; @@ -1129,6 +1162,206 @@ __PACKAGE__->register_method( method => 'new_hold_copy_targeter', ); +sub reservation_targeter { + my $self = shift; + my $client = shift; + my $one_reservation = shift; + + local $OpenILS::Application::Storage::WRITE = 1; + + my $reservations; + + try { + if ($one_reservation) { + $self->method_lookup('open-ils.storage.transaction.begin')->run( $client ); + $reservations = [ booking::reservation->search_where( { id => $one_reservation, capture_time => undef, cancel_time => undef } ) ]; + } else { + + # find all the reservations needing targeting + $reservations = [ + booking::reservation->search_where( + { current_resource => undef, + cancel_time => undef, + start_time => { '>' => 'now' } + }, + { order_by => 'start_time' } + ) + ]; + } + } catch Error with { + my $e = shift; + die "Could not retrieve reservation requests:\n\n$e\n"; + }; + + my @successes = (); + for my $bresv (@$reservations) { + try { + #start a transaction if needed + if ($self->method_lookup('open-ils.storage.transaction.current')->run) { + $log->debug("Cleaning up after previous transaction\n"); + $self->method_lookup('open-ils.storage.transaction.rollback')->run; + } + $self->method_lookup('open-ils.storage.transaction.begin')->run( $client ); + $log->info("Processing reservation ".$bresv->id."...\n"); + + #first, re-fetch the hold, to make sure it's not captured already + $bresv->remove_from_object_index(); + $bresv = booking::reservation->retrieve( $bresv->id ); + + die "OK\n" if (!$bresv or $bresv->capture_time or $bresv->cancel_time); + + my $end_time = $parser->parse_datetime( clense_ISO8601( $bresv->end_time ) ); + if (DateTime->compare($end_time, DateTime->now) < 0) { + + # cancel cause = un-targeted expiration + $bresv->update( { cancel_time => 'now' } ); + $self->method_lookup('open-ils.storage.transaction.commit')->run; + + # tell A/T the reservation was cancelled + my $fm_bresv = $bresv->to_fieldmapper; + my $ses = OpenSRF::AppSession->create('open-ils.trigger'); + $ses->request('open-ils.trigger.event.autocreate', + 'booking.reservation.cancel.expire_no_target', $fm_bresv, $fm_bresv->pickup_lib); + + die "OK\n"; + } + + my $possible_resources; + + # find all the potential resources + if (!$bresv->target_resource) { + my $filter = { type => $bresv->target_resource_type }; + my $attr_maps = [ booking::reservation_attr_value_map->search( reservation => $bresv->id) ]; + + $filter->{attribute_values} = [ map { $_->attr_value } @$attr_maps ] if (@$attr_maps); + + $filter->{available} = [$bresv->start_time, $bresv->end_time]; + my $ses = OpenSRF::AppSession->create('open-ils.booking'); + $possible_resources = $ses->request('open-ils.booking.resources.filtered_id_list', undef, $filter)->gather(1); + } else { + $possible_resources = $bresv->target_resource; + } + + my $all_resources = [ booking::resource->search( id => $possible_resources ) ]; + @$all_resources = grep { isTrue($_->type->transferable) || $_->owner.'' eq $bresv->pickup_lib.'' } @$all_resources; + + + my @good_resources = (); + for my $res (@$all_resources) { + unless (isTrue($res->type->catalog_item)) { + push @good_resources, $res; + next; + } + + my $copy = [ asset::copy->search( deleted => f, barcode => $res->barcode )]->[0]; + + unless ($copy) { + push @good_resources, $res; + next; + } + + if ($copy->status->id == 0 || $copy->status->id == 7) { + push @good_resources, $res; + next; + } + + if ($copy->status->id == 1) { + my $circs = action::circulation->search_where( + {target_copy => $copy->id, checkin_time => undef }, + { order_by => 'id DESC' } + ); + + if (@$circs) { + my $due_date = $circs->[0]->due_date; + $due_date = $parser->parse_datetime( clense_ISO8601( $due_date ) ); + my $start_time = $parser->parse_datetime( clense_ISO8601( $bresv->start_time ) ); + next if (DateTime->compare($start_time, $due_date) < 0); + push @good_resources, $res; + } + + next; + } + + push @good_resources, $res if (isTrue($copy->status->holdable)); + } + + # let 'em know we're still working + $client->status( new OpenSRF::DomainObject::oilsContinueStatus ); + + # if we have no copies ... + if (!@good_resources) { + $log->info("\tNo resources available for targeting at all!\n"); + push @successes, { reservation => $bresv->id, eligible_copies => 0, error => 'NO_COPIES' }; + + $self->method_lookup('open-ils.storage.transaction.commit')->run; + die "OK\n"; + } + + $log->debug("\t".scalar(@good_resources)." resources available for targeting..."); + + my $prox_list = []; + $$prox_list[0] = + [ + grep { + $_->owner == $bresv->pickup_lib + } @good_resources + ]; + + $all_resources = [grep {$_->owner != $bresv->pickup_lib } @good_resources]; + # $all_copies is now a list of copies not at the pickup library + + my $best = shift @good_resources; + $client->status( new OpenSRF::DomainObject::oilsContinueStatus ); + + if (!$best) { + $log->debug("\tNothing at the pickup lib, looking elsewhere among ".scalar(@$all_resources)." resources"); + + $prox_list = + map { $_->[1] } + sort { $a->[0] <> $b->[0] } + map { + [ actor::org_unit_proximity->search_where( + { from_org => $bresv->pickup_lib.'', to_org => $_=>owner.'' } + )->[0]->prox, + $_ + ] + } @$all_resources; + + $client->status( new OpenSRF::DomainObject::oilsContinueStatus ); + + $best = shift @$prox_list + } + + if ($best) { + $bresv->update( { current_resource => ''.$best->id } ); + $log->debug("\tUpdating reservation [".$bresv->id."] with new 'current_resource' [".$best->id."] for reservation fulfillment."); + } + + $self->method_lookup('open-ils.storage.transaction.commit')->run; + $log->info("\tProcessing of bresv ".$bresv->id." complete."); + + push @successes, + { reservation => $bresv->id, + current_resource => ($best ? $best->id : undef) }; + + } otherwise { + my $e = shift; + if ($e !~ /^OK/o) { + $log->error("Processing of bresv failed: $e"); + $self->method_lookup('open-ils.storage.transaction.rollback')->run; + throw $e if ($e =~ /IS NOT CONNECTED TO THE NETWORK/o); + } + }; + } + + return \@successes; +} +__PACKAGE__->register_method( + api_name => 'open-ils.storage.booking.reservation.resource_targeter', + api_level => 1, + method => 'reservation_targeter', +); + my $locations; my $statuses; my %cache = (titles => {}, cns => {}); diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/money.pm b/Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/money.pm index b049a0aae0..e1a2f0ae3c 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/money.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Storage/Publisher/money.pm @@ -51,8 +51,13 @@ sub _make_mbts { $s->balance_owed( sprintf('%0.2f', (($to) - ($tp)) / 100) ); #$log->debug( "balance of ".$x->id." == ".$s->balance_owed, DEBUG ); - $s->xact_type( 'grocery' ) if (money::grocery->retrieve($x->id)); - $s->xact_type( 'circulation' ) if (action::circulation->retrieve($x->id)); + if (action::circulation->retrieve($x->id)) { + $s->xact_type( 'circulation' ); + } elsif (money::grocery->retrieve($x->id)) { + $s->xact_type( 'grocery' ); + } elsif (booking::reservation->retrieve($x->id)) { + $s->xact_type( 'reservation' ); + } push @mbts, $s; } @@ -117,6 +122,7 @@ sub new_collections { my $mb = money::billing->table; my $circ = action::circulation->table; my $mg = money::grocery->table; + my $res = booking::reservation->table; my $descendants = "actor.org_unit_descendants((select id from actor.org_unit where shortname = ?))"; my $SQL = <<" SQL"; @@ -156,6 +162,23 @@ select and b.billing_ts < current_timestamp - ? * '1 day'::interval and not b.voided group by 1,2 + + union all + + select + x.id, + x.usr, + MAX(b.billing_ts) as last_billing, + SUM(b.amount) AS total_billing + from booking.reservation x + left join money.collections_tracker c ON (c.usr = x.usr AND c.location = ?) + join money.billing b on (b.xact = x.id) + where x.xact_finish is null + and c.id is null + and x.pickup_lib in (XX) + and b.billing_ts < current_timestamp - ? * '1 day'::interval + and not b.voided + group by 1,2 ) full_list left join money.payment p on (full_list.id = p.xact) group by 1 @@ -236,6 +259,20 @@ select and b.billing_ts between ? and ? and not b.voided group by 1,2 + + union all + + select + x.id, + x.usr, + SUM(b.amount) AS total_billing + from booking.reservation x + join money.billing b on (b.xact = x.id) + where x.xact_finish is null + and x.pickup_lib in (XX) + and b.billing_ts between ? and ? + and not b.voided + group by 1,2 ) full_list left join money.payment p on (full_list.id = p.xact) group by 1 @@ -292,6 +329,42 @@ SELECT usr, SELECT lt.usr, NULL::TIMESTAMPTZ AS last_pertinent_billing, NULL::TIMESTAMPTZ AS last_pertinent_payment + FROM booking.reservation lt + JOIN money.collections_tracker cl ON (lt.usr = cl.usr) + JOIN money.billing bl ON (lt.id = bl.xact) + WHERE cl.location = ? + AND lt.pickup_lib IN (XX) + AND bl.void_time BETWEEN ? AND ? + GROUP BY 1 + + UNION ALL + SELECT lt.usr, + MAX(bl.billing_ts) AS last_pertinent_billing, + NULL::TIMESTAMPTZ AS last_pertinent_payment + FROM booking.reservation lt + JOIN money.collections_tracker cl ON (lt.usr = cl.usr) + JOIN money.billing bl ON (lt.id = bl.xact) + WHERE cl.location = ? + AND lt.pickup_lib IN (XX) + AND bl.billing_ts BETWEEN ? AND ? + GROUP BY 1 + + UNION ALL + SELECT lt.usr, + NULL::TIMESTAMPTZ AS last_pertinent_billing, + MAX(pm.payment_ts) AS last_pertinent_payment + FROM booking.reservation lt + JOIN money.collections_tracker cl ON (lt.usr = cl.usr) + JOIN money.payment pm ON (lt.id = pm.xact) + WHERE cl.location = ? + AND lt.pickup_lib IN (XX) + AND pm.payment_ts BETWEEN ? AND ? + GROUP BY 1 + + UNION ALL + SELECT lt.usr, + NULL::TIMESTAMPTZ AS last_pertinent_billing, + NULL::TIMESTAMPTZ AS last_pertinent_payment FROM money.grocery lt JOIN money.collections_tracker cl ON (lt.usr = cl.usr) JOIN money.billing bl ON (lt.id = bl.xact) @@ -389,9 +462,17 @@ SELECT usr, my $sth = money::collections_tracker->db_Main->prepare($real_sql); $sth->execute( + # reservation queries $org->id, $startdate, $enddate, $org->id, $startdate, $enddate, $org->id, $startdate, $enddate, + + # grocery queries + $org->id, $startdate, $enddate, + $org->id, $startdate, $enddate, + $org->id, $startdate, $enddate, + + # circ queries $org->id, $startdate, $enddate, $org->id, $startdate, $enddate, $org->id, $startdate, $enddate, diff --git a/Open-ILS/src/perlmods/OpenILS/Const.pm b/Open-ILS/src/perlmods/OpenILS/Const.pm index 5d58ef63ee..624c9fd10f 100644 --- a/Open-ILS/src/perlmods/OpenILS/Const.pm +++ b/Open-ILS/src/perlmods/OpenILS/Const.pm @@ -41,6 +41,7 @@ econst OILS_COPY_STATUS_CATALOGING => 11; econst OILS_COPY_STATUS_RESERVES => 12; econst OILS_COPY_STATUS_DISCARD => 13; econst OILS_COPY_STATUS_DAMAGED => 14; +econst OILS_COPY_STATUS_ON_RESV_SHELF => 15; # --------------------------------------------------------------------- diff --git a/Open-ILS/src/perlmods/OpenILS/Utils/CStoreEditor.pm b/Open-ILS/src/perlmods/OpenILS/Utils/CStoreEditor.pm index 8881e2f7cc..782b085001 100644 --- a/Open-ILS/src/perlmods/OpenILS/Utils/CStoreEditor.pm +++ b/Open-ILS/src/perlmods/OpenILS/Utils/CStoreEditor.pm @@ -258,7 +258,7 @@ sub set_savepoint { my $name = shift || 'savepoint'; return unless $self->{session} and $self->{xact_id}; $self->log(I, "setting savepoint '$name'"); - my $stat = $self->request($self->app.".savepoint.set") + my $stat = $self->request($self->app.".savepoint.set", $name) or $self->log(E, "error setting savepoint '$name'"); return $stat; } @@ -268,7 +268,7 @@ sub release_savepoint { my $name = shift || 'savepoint'; return unless $self->{session} and $self->{xact_id}; $self->log(I, "releasing savepoint '$name'"); - my $stat = $self->request($self->app.".savepoint.release") + my $stat = $self->request($self->app.".savepoint.release", $name) or $self->log(E, "error releasing savepoint '$name'"); return $stat; } @@ -278,7 +278,7 @@ sub rollback_savepoint { my $name = shift || 'savepoint'; return unless $self->{session} and $self->{xact_id}; $self->log(I, "rollback savepoint '$name'"); - my $stat = $self->request($self->app.".savepoint.rollback") + my $stat = $self->request($self->app.".savepoint.rollback", $name) or $self->log(E, "error rolling back savepoint '$name'"); return $stat; } diff --git a/Open-ILS/src/perlmods/OpenILS/WWW/BadDebt.pm b/Open-ILS/src/perlmods/OpenILS/WWW/BadDebt.pm index a5015eeeda..cb7c322d69 100644 --- a/Open-ILS/src/perlmods/OpenILS/WWW/BadDebt.pm +++ b/Open-ILS/src/perlmods/OpenILS/WWW/BadDebt.pm @@ -97,9 +97,17 @@ sub handler { my $s = $cstore->request('open-ils.cstore.direct.money.billable_xact_summary.retrieve' => $xact)->gather(1); my $u = $cstore->request('open-ils.cstore.direct.actor.usr.retrieve' => $s->usr)->gather(1); my $c = $cstore->request('open-ils.cstore.direct.actor.card.retrieve' => $u->card)->gather(1); - my $w = $s->xact_type eq 'circulation' ? - $cstore->request('open-ils.cstore.direct.action.circulation.retrieve' => $xact)->gather(1)->circ_lib : - $cstore->request('open-ils.cstore.direct.money.grocery.retrieve' => $xact)->gather(1)->billing_location; + my $w; + + if ($s->xact_type eq 'circulation') { + $w = $cstore->request('open-ils.cstore.direct.action.circulation.retrieve' => $xact)->gather(1)->circ_lib : + } elsif ($s->xact_type eq 'grocery') { + $w = $cstore->request('open-ils.cstore.direct.money.grocery.retrieve' => $xact)->gather(1)->billing_location; + } elsif ($s->xact_type eq 'reservation') { + $w = $cstore->request('open-ils.cstore.direct.booking.reservation.retrieve' => $xact)->gather(1)->pickup_lib; + } else { + die; + } my $failures = $actor->request('open-ils.actor.user.perm.check', $auth_ses, $user->id, $w, ['MARK_BAD_DEBT'])->gather(1); diff --git a/Open-ILS/src/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql index dc294cca9e..1033b6b404 100644 --- a/Open-ILS/src/sql/Pg/002.schema.config.sql +++ b/Open-ILS/src/sql/Pg/002.schema.config.sql @@ -51,7 +51,7 @@ CREATE TABLE config.upgrade_log ( install_date TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() ); -INSERT INTO config.upgrade_log (version) VALUES ('0131'); -- dbs +INSERT INTO config.upgrade_log (version) VALUES ('0130'); -- senator CREATE TABLE config.bib_source ( id SERIAL PRIMARY KEY, diff --git a/Open-ILS/src/sql/Pg/095.schema.booking.sql b/Open-ILS/src/sql/Pg/095.schema.booking.sql new file mode 100644 index 0000000000..f79d968cf9 --- /dev/null +++ b/Open-ILS/src/sql/Pg/095.schema.booking.sql @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2009 Equinox Software, Inc. + * Scott McKellar + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + */ + +BEGIN; + +DROP SCHEMA IF EXISTS booking CASCADE; + +CREATE SCHEMA booking; + +CREATE TABLE booking.resource_type ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + elbow_room INTERVAL, + fine_interval INTERVAL, + fine_amount DECIMAL(8,2) NOT NULL DEFAULT 0, + max_fine DECIMAL(8,2), + owner INT NOT NULL + REFERENCES actor.org_unit( id ) + DEFERRABLE INITIALLY DEFERRED, + catalog_item BOOLEAN NOT NULL DEFAULT FALSE, + transferable BOOLEAN NOT NULL DEFAULT FALSE, + record INT REFERENCES biblio.record_entry (id) + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT brt_name_once_per_owner UNIQUE(owner, name, record) +); + +CREATE TABLE booking.resource ( + id SERIAL PRIMARY KEY, + owner INT NOT NULL + REFERENCES actor.org_unit(id) + DEFERRABLE INITIALLY DEFERRED, + type INT NOT NULL + REFERENCES booking.resource_type(id) + DEFERRABLE INITIALLY DEFERRED, + overbook BOOLEAN NOT NULL DEFAULT FALSE, + barcode TEXT NOT NULL, + deposit BOOLEAN NOT NULL DEFAULT FALSE, + deposit_amount DECIMAL(8,2) NOT NULL DEFAULT 0.00, + user_fee DECIMAL(8,2) NOT NULL DEFAULT 0.00, + CONSTRAINT br_unique UNIQUE(owner, barcode) +); + +-- For non-catalog items: hijack barcode for name/description + +CREATE TABLE booking.resource_attr ( + id SERIAL PRIMARY KEY, + owner INT NOT NULL + REFERENCES actor.org_unit(id) + DEFERRABLE INITIALLY DEFERRED, + name TEXT NOT NULL, + resource_type INT NOT NULL + REFERENCES booking.resource_type(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + required BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT bra_name_once_per_type UNIQUE(resource_type, name) +); + +CREATE TABLE booking.resource_attr_value ( + id SERIAL PRIMARY KEY, + owner INT NOT NULL + REFERENCES actor.org_unit(id) + DEFERRABLE INITIALLY DEFERRED, + attr INT NOT NULL + REFERENCES booking.resource_attr(id) + DEFERRABLE INITIALLY DEFERRED, + valid_value TEXT NOT NULL, + CONSTRAINT brav_logical_key UNIQUE(owner, attr, valid_value) +); + +-- Do we still need a name column? + + +CREATE TABLE booking.resource_attr_map ( + id SERIAL PRIMARY KEY, + resource INT NOT NULL + REFERENCES booking.resource(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + resource_attr INT NOT NULL + REFERENCES booking.resource_attr(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + value INT NOT NULL + REFERENCES booking.resource_attr_value(id) + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT bram_one_value_per_attr UNIQUE(resource, resource_attr) +); + +CREATE TABLE booking.reservation ( + request_time TIMESTAMPTZ NOT NULL DEFAULT now(), + start_time TIMESTAMPTZ, + end_time TIMESTAMPTZ, + capture_time TIMESTAMPTZ, + cancel_time TIMESTAMPTZ, + pickup_time TIMESTAMPTZ, + return_time TIMESTAMPTZ, + booking_interval INTERVAL, + fine_interval INTERVAL, + fine_amount DECIMAL(8,2), + max_fine DECIMAL(8,2), + target_resource_type INT NOT NULL + REFERENCES booking.resource_type(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + target_resource INT REFERENCES booking.resource(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + current_resource INT REFERENCES booking.resource(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + request_lib INT NOT NULL + REFERENCES actor.org_unit(id) + DEFERRABLE INITIALLY DEFERRED, + pickup_lib INT REFERENCES actor.org_unit(id) + DEFERRABLE INITIALLY DEFERRED, + capture_staff INT REFERENCES actor.usr(id) + DEFERRABLE INITIALLY DEFERRED +) INHERITS (money.billable_xact); + +ALTER TABLE booking.reservation ADD PRIMARY KEY (id); + +ALTER TABLE booking.reservation + ADD CONSTRAINT booking_reservation_usr_fkey + FOREIGN KEY (usr) REFERENCES actor.usr (id) + DEFERRABLE INITIALLY DEFERRED; + +CREATE TRIGGER mat_summary_create_tgr AFTER INSERT ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_create ('reservation'); +CREATE TRIGGER mat_summary_change_tgr AFTER UPDATE ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_update (); +CREATE TRIGGER mat_summary_remove_tgr AFTER DELETE ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_delete (); + + +CREATE TABLE booking.reservation_attr_value_map ( + id SERIAL PRIMARY KEY, + reservation INT NOT NULL + REFERENCES booking.reservation(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + attr_value INT NOT NULL + REFERENCES booking.resource_attr_value(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT bravm_logical_key UNIQUE(reservation, attr_value) +); + +CREATE TABLE action.reservation_transit_copy ( + reservation INT REFERENCES booking.reservation (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED +) INHERITS (action.transit_copy); +ALTER TABLE action.reservation_transit_copy ADD PRIMARY KEY (id); +ALTER TABLE action.reservation_transit_copy ADD CONSTRAINT artc_tc_fkey FOREIGN KEY (target_copy) REFERENCES booking.resource (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; +CREATE INDEX active_reservation_transit_dest_idx ON "action".reservation_transit_copy (dest); +CREATE INDEX active_reservation_transit_source_idx ON "action".reservation_transit_copy (source); +CREATE INDEX active_reservation_transit_cp_idx ON "action".reservation_transit_copy (target_copy); + +COMMIT; diff --git a/Open-ILS/src/sql/Pg/100.circ_matrix.sql b/Open-ILS/src/sql/Pg/100.circ_matrix.sql index 9eb1d5bf24..06558e4a32 100644 --- a/Open-ILS/src/sql/Pg/100.circ_matrix.sql +++ b/Open-ILS/src/sql/Pg/100.circ_matrix.sql @@ -431,6 +431,12 @@ BEGIN SELECT SUM(f.balance_owed) INTO current_fines FROM money.materialized_billable_xact_summary f JOIN ( + SELECT r.id + FROM booking.reservation r + JOIN actor.org_unit_full_path( max_fines.org_unit ) fp ON (r.pickup_lib = fp.id) + WHERE usr = match_user + AND xact_finish IS NULL + UNION ALL SELECT g.id FROM money.grocery g JOIN actor.org_unit_full_path( max_fines.org_unit ) fp ON (g.billing_location = fp.id) @@ -611,6 +617,12 @@ BEGIN SELECT SUM(f.balance_owed) INTO current_fines FROM money.materialized_billable_xact_summary f JOIN ( + SELECT r.id + FROM booking.reservation r + JOIN actor.org_unit_full_path( max_fines.org_unit ) fp ON (r.pickup_lib = fp.id) + WHERE usr = match_user + AND xact_finish IS NULL + UNION ALL SELECT g.id FROM money.grocery g JOIN actor.org_unit_full_path( max_fines.org_unit ) fp ON (g.billing_location = fp.id) diff --git a/Open-ILS/src/sql/Pg/500.view.cross-schema.sql b/Open-ILS/src/sql/Pg/500.view.cross-schema.sql index fbc46ffc24..4909e8219b 100644 --- a/Open-ILS/src/sql/Pg/500.view.cross-schema.sql +++ b/Open-ILS/src/sql/Pg/500.view.cross-schema.sql @@ -20,7 +20,7 @@ BEGIN; CREATE OR REPLACE VIEW money.open_billable_xact_summary AS SELECT xact.id AS id, xact.usr AS usr, - COALESCE(circ.circ_lib,groc.billing_location) AS billing_location, + COALESCE(circ.circ_lib,groc.billing_location,res.pickup_lib) AS billing_location, xact.xact_start AS xact_start, xact.xact_finish AS xact_finish, SUM(credit.amount) AS total_paid, @@ -37,6 +37,7 @@ CREATE OR REPLACE VIEW money.open_billable_xact_summary AS JOIN pg_class p ON (xact.tableoid = p.oid) LEFT JOIN "action".circulation circ ON (circ.id = xact.id) LEFT JOIN money.grocery groc ON (groc.id = xact.id) + LEFT JOIN booking.reservation res ON (groc.id = xact.id) LEFT JOIN ( SELECT billing.xact, billing.voided, diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index 502b0e4933..63045fb22b 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -126,6 +126,7 @@ INSERT INTO config.copy_status (id,name) VALUES (11,oils_i18n_gettext(11, 'Catal INSERT INTO config.copy_status (id,name,opac_visible) VALUES (12,oils_i18n_gettext(12, 'Reserves', 'ccs', 'name'),'t'); INSERT INTO config.copy_status (id,name) VALUES (13,oils_i18n_gettext(13, 'Discard/Weed', 'ccs', 'name')); INSERT INTO config.copy_status (id,name) VALUES (14,oils_i18n_gettext(14, 'Damaged', 'ccs', 'name')); +INSERT INTO config.copy_status (id,name) VALUES (15,oils_i18n_gettext(15, 'On reservation shelf', 'ccs', 'name')); SELECT SETVAL('config.copy_status_id_seq'::TEXT, 100); @@ -1231,6 +1232,17 @@ INSERT INTO permission.perm_list VALUES (201, 'DELETE_MFHD_RECORD', oils_i18n_gettext(201, 'Allows a user to delete an MFHD record', 'ppl', 'description')), (202, 'ADMIN_ACQ_FUND', oils_i18n_gettext(202, 'Allow a user to create/view/update/delete a fund', 'ppl', 'description')), (203, 'group_application.user.staff.acq_admin', oils_i18n_gettext(203, 'Allows a user to add/remove/edit users in the "Acquisitions Administrators" group', 'ppl', 'description')) + (351, 'HOLD_LOCAL_AVAIL_OVERRIDE', oils_i18n_gettext(351, 'Allow a user to place a hold despite the availability of a local copy', 'ppl', 'description')), + (352, 'ADMIN_BOOKING_RESOURCE', oils_i18n_gettext(352, 'Enables the user to create/update/delete booking resources', 'ppl', 'description')), + (353, 'ADMIN_BOOKING_RESOURCE_TYPE', oils_i18n_gettext(353, 'Enables the user to create/update/delete booking resource types', 'ppl', 'description')), + (354, 'ADMIN_BOOKING_RESOURCE_ATTR', oils_i18n_gettext(354, 'Enables the user to create/update/delete booking resource attributes', 'ppl', 'description')), + (355, 'ADMIN_BOOKING_RESOURCE_ATTR_MAP', oils_i18n_gettext(355, 'Enables the user to create/update/delete booking resource attribute maps', 'ppl', 'description')), + (356, 'ADMIN_BOOKING_RESOURCE_ATTR_VALUE', oils_i18n_gettext(356, 'Enables the user to create/update/delete booking resource attribute values', 'ppl', 'description')), + (357, 'ADMIN_BOOKING_RESERVATION', oils_i18n_gettext(357, 'Enables the user to create/update/delete booking reservations', 'ppl', 'description')), + (358, 'ADMIN_BOOKING_RESERVATION_ATTR_VALUE_MAP', oils_i18n_gettext(358, 'Enables the user to create/update/delete booking reservation attribute value maps', 'ppl', 'description')), + (359, 'HOLD_ITEM_CHECKED_OUT.override', oils_i18n_gettext(359, 'Allows a user to place a hold on an item that they already have checked out', 'ppl', 'description')), + (360, 'RETRIEVE_RESERVATION_PULL_LIST', oils_i18n_gettext(360, 'Allows a user to retrieve a booking reservation pull list', 'ppl', 'description')), + (361, 'CAPTURE_RESERVATION', oils_i18n_gettext(361, 'Allows a user to capture booking reservations', 'ppl', 'description')) ; SELECT SETVAL('permission.perm_list_id_seq'::TEXT, (SELECT MAX(id) FROM permission.perm_list)); @@ -1379,7 +1391,7 @@ INSERT INTO permission.perm_list (code) VALUES ('UPDATE_ORG_UNIT_SETTING.cat.bib INSERT INTO permission.perm_list (code) VALUES ('UPDATE_ORG_UNIT_SETTING.cat.bib.alert_on_empty'); INSERT INTO permission.perm_list (code) VALUES ('UPDATE_ORG_UNIT_SETTING.patron.password.use_phone'); - +SELECT SETVAL('permission.perm_list_id_seq'::TEXT, 1000); INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES (1, oils_i18n_gettext(1, 'Users', 'pgt', 'name'), NULL, NULL, '3 years', FALSE, 'group_application.user'); diff --git a/Open-ILS/src/sql/Pg/build-db.sh b/Open-ILS/src/sql/Pg/build-db.sh index 705e7706f7..a6642a0b38 100755 --- a/Open-ILS/src/sql/Pg/build-db.sh +++ b/Open-ILS/src/sql/Pg/build-db.sh @@ -89,15 +89,18 @@ ordered_file_list=" 005.schema.actors.sql 006.schema.permissions.sql + 008.schema.query.sql 010.schema.biblio.sql 011.schema.authority.sql 012.schema.vandelay.sql + 015.schema.staging.sql 020.schema.functions.sql 030.schema.metabib.sql 040.schema.asset.sql 070.schema.container.sql 080.schema.money.sql 090.schema.action.sql + 095.schema.booking.sql 100.circ_matrix.sql 110.hold_matrix.sql diff --git a/Open-ILS/src/sql/Pg/upgrade/0086.schema.booking-tables.sql b/Open-ILS/src/sql/Pg/upgrade/0086.schema.booking-tables.sql new file mode 100644 index 0000000000..112cd71415 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/0086.schema.booking-tables.sql @@ -0,0 +1,135 @@ +BEGIN; + +INSERT INTO config.upgrade_log (version) VALUES ('0086'); + +DROP SCHEMA IF EXISTS booking CASCADE; + +CREATE SCHEMA booking; + +CREATE TABLE booking.resource_type ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + fine_interval INTERVAL, + fine_amount DECIMAL(8,2) NOT NULL DEFAULT 0, + owner INT NOT NULL + REFERENCES actor.org_unit( id ) + DEFERRABLE INITIALLY DEFERRED, + catalog_item BOOLEAN NOT NULL DEFAULT FALSE, + transferable BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT brt_name_once_per_owner UNIQUE(owner, name) +); + +CREATE TABLE booking.resource ( + id SERIAL PRIMARY KEY, + owner INT NOT NULL + REFERENCES actor.org_unit(id) + DEFERRABLE INITIALLY DEFERRED, + type INT NOT NULL + REFERENCES booking.resource_type(id) + DEFERRABLE INITIALLY DEFERRED, + overbook BOOLEAN NOT NULL DEFAULT FALSE, + barcode TEXT NOT NULL, + deposit BOOLEAN NOT NULL DEFAULT FALSE, + deposit_amount DECIMAL(8,2) NOT NULL DEFAULT 0.00, + user_fee DECIMAL(8,2) NOT NULL DEFAULT 0.00, + CONSTRAINT br_unique UNIQUE(owner, type, barcode) +); + +-- For non-catalog items: hijack barcode for name/description + +CREATE TABLE booking.resource_attr ( + id SERIAL PRIMARY KEY, + owner INT NOT NULL + REFERENCES actor.org_unit(id) + DEFERRABLE INITIALLY DEFERRED, + name TEXT NOT NULL, + resource_type INT NOT NULL + REFERENCES booking.resource_type(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + required BOOLEAN NOT NULL DEFAULT FALSE, + CONSTRAINT bra_name_once_per_type UNIQUE(resource_type, name) +); + +CREATE TABLE booking.resource_attr_value ( + id SERIAL PRIMARY KEY, + owner INT NOT NULL + REFERENCES actor.org_unit(id) + DEFERRABLE INITIALLY DEFERRED, + attr INT NOT NULL + REFERENCES booking.resource_attr(id) + DEFERRABLE INITIALLY DEFERRED, + valid_value TEXT NOT NULL, + CONSTRAINT brav_logical_key UNIQUE(owner, attr, valid_value) +); + +-- Do we still need a name column? + + +CREATE TABLE booking.resource_attr_map ( + id SERIAL PRIMARY KEY, + resource INT NOT NULL + REFERENCES booking.resource(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + resource_attr INT NOT NULL + REFERENCES booking.resource_attr(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + value INT NOT NULL + REFERENCES booking.resource_attr_value(id) + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT bram_one_value_per_attr UNIQUE(resource, resource_attr) +); + +CREATE TABLE booking.reservation ( + request_time TIMESTAMPTZ NOT NULL DEFAULT now(), + start_time TIMESTAMPTZ, + end_time TIMESTAMPTZ, + capture_time TIMESTAMPTZ, + cancel_time TIMESTAMPTZ, + pickup_time TIMESTAMPTZ, + return_time TIMESTAMPTZ, + booking_interval INTERVAL, + fine_interval INTERVAL, + fine_amount DECIMAL(8,2), + target_resource_type INT NOT NULL + REFERENCES booking.resource_type(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + target_resource INT REFERENCES booking.resource(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + current_resource INT REFERENCES booking.resource(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + request_lib INT NOT NULL + REFERENCES actor.org_unit(id) + DEFERRABLE INITIALLY DEFERRED, + pickup_lib INT REFERENCES actor.org_unit(id) + DEFERRABLE INITIALLY DEFERRED, + capture_staff INT REFERENCES actor.usr(id) + DEFERRABLE INITIALLY DEFERRED +) INHERITS (money.billable_xact); + +ALTER TABLE booking.reservation ADD PRIMARY KEY (id); + +ALTER TABLE booking.reservation + ADD CONSTRAINT booking_reservation_usr_fkey + FOREIGN KEY (usr) REFERENCES actor.usr (id) + DEFERRABLE INITIALLY DEFERRED; + +CREATE TABLE booking.reservation_attr_value_map ( + id SERIAL PRIMARY KEY, + reservation INT NOT NULL + REFERENCES booking.reservation(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + attr_value INT NOT NULL + REFERENCES booking.resource_attr_value(id) + ON DELETE CASCADE + DEFERRABLE INITIALLY DEFERRED, + CONSTRAINT bravm_logical_key UNIQUE(reservation, attr_value) +); + +COMMIT; diff --git a/Open-ILS/src/sql/Pg/upgrade/0090.schema.booking.bib-base-types.sql b/Open-ILS/src/sql/Pg/upgrade/0090.schema.booking.bib-base-types.sql new file mode 100644 index 0000000000..2ee8bb7407 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/0090.schema.booking.bib-base-types.sql @@ -0,0 +1,9 @@ +BEGIN; + +INSERT INTO config.upgrade_log (version) VALUES ('0090'); -- miker + +ALTER TABLE booking.resource_type DROP CONSTRAINT brt_name_once_per_owner; +ALTER TABLE booking.resource_type ADD COLUMN record BIGINT REFERENCES biblio.record_entry (id) DEFERRABLE INITIALLY DEFERRED; +ALTER TABLE booking.resource_type ADD CONSTRAINT brt_name_or_record_once_per_owner UNIQUE(owner, name, record); + +COMMIT; diff --git a/Open-ILS/src/sql/Pg/upgrade/0103.schema.booking.max_fine.sql b/Open-ILS/src/sql/Pg/upgrade/0103.schema.booking.max_fine.sql new file mode 100644 index 0000000000..34dc26b610 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/0103.schema.booking.max_fine.sql @@ -0,0 +1,8 @@ +BEGIN; + +INSERT INTO config.upgrade_log (version) VALUES ('0103'); -- miker + +ALTER TABLE booking.resource_type ADD COLUMN max_fine NUMERIC(8,2); +ALTER TABLE booking.reservation ADD COLUMN max_fine NUMERIC(8,2); + +COMMIT; diff --git a/Open-ILS/src/sql/Pg/upgrade/0105.schema.booking-integration.sql b/Open-ILS/src/sql/Pg/upgrade/0105.schema.booking-integration.sql new file mode 100644 index 0000000000..2887048384 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/0105.schema.booking-integration.sql @@ -0,0 +1,339 @@ +BEGIN; + +INSERT INTO config.upgrade_log (version) VALUES ('0105'); -- miker + +CREATE TRIGGER mat_summary_create_tgr AFTER INSERT ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_create ('reservation'); +CREATE TRIGGER mat_summary_change_tgr AFTER UPDATE ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_update (); +CREATE TRIGGER mat_summary_remove_tgr AFTER DELETE ON booking.reservation FOR EACH ROW EXECUTE PROCEDURE money.mat_summary_delete (); + +CREATE OR REPLACE VIEW money.open_billable_xact_summary AS + SELECT xact.id AS id, + xact.usr AS usr, + COALESCE(circ.circ_lib,groc.billing_location,res.pickup_lib) AS billing_location, + xact.xact_start AS xact_start, + xact.xact_finish AS xact_finish, + SUM(credit.amount) AS total_paid, + MAX(credit.payment_ts) AS last_payment_ts, + LAST(credit.note) AS last_payment_note, + LAST(credit.payment_type) AS last_payment_type, + SUM(debit.amount) AS total_owed, + MAX(debit.billing_ts) AS last_billing_ts, + LAST(debit.note) AS last_billing_note, + LAST(debit.billing_type) AS last_billing_type, + COALESCE(SUM(debit.amount),0) - COALESCE(SUM(credit.amount),0) AS balance_owed, + p.relname AS xact_type + FROM money.billable_xact xact + JOIN pg_class p ON (xact.tableoid = p.oid) + LEFT JOIN "action".circulation circ ON (circ.id = xact.id) + LEFT JOIN money.grocery groc ON (groc.id = xact.id) + LEFT JOIN booking.reservation res ON (groc.id = xact.id) + LEFT JOIN ( + SELECT billing.xact, + billing.voided, + sum(billing.amount) AS amount, + max(billing.billing_ts) AS billing_ts, + last(billing.note) AS note, + last(billing.billing_type) AS billing_type + FROM money.billing + WHERE billing.voided IS FALSE + GROUP BY billing.xact, billing.voided + ) debit ON (xact.id = debit.xact AND debit.voided IS FALSE) + LEFT JOIN ( + SELECT payment_view.xact, + payment_view.voided, + sum(payment_view.amount) AS amount, + max(payment_view.payment_ts) AS payment_ts, + last(payment_view.note) AS note, + last(payment_view.payment_type) AS payment_type + FROM money.payment_view + WHERE payment_view.voided IS FALSE + GROUP BY payment_view.xact, payment_view.voided + ) credit ON (xact.id = credit.xact AND credit.voided IS FALSE) + WHERE xact.xact_finish IS NULL + GROUP BY 1,2,3,4,5,15 + ORDER BY MAX(debit.billing_ts), MAX(credit.payment_ts); + +CREATE OR REPLACE FUNCTION actor.calculate_system_penalties( match_user INT, context_org INT ) RETURNS SETOF actor.usr_standing_penalty AS $func$ +DECLARE + user_object actor.usr%ROWTYPE; + new_sp_row actor.usr_standing_penalty%ROWTYPE; + existing_sp_row actor.usr_standing_penalty%ROWTYPE; + collections_fines permission.grp_penalty_threshold%ROWTYPE; + max_fines permission.grp_penalty_threshold%ROWTYPE; + max_overdue permission.grp_penalty_threshold%ROWTYPE; + max_items_out permission.grp_penalty_threshold%ROWTYPE; + tmp_grp INT; + items_overdue INT; + items_out INT; + context_org_list INT[]; + current_fines NUMERIC(8,2) := 0.0; + tmp_fines NUMERIC(8,2); + tmp_groc RECORD; + tmp_circ RECORD; + tmp_org actor.org_unit%ROWTYPE; +BEGIN + SELECT INTO user_object * FROM actor.usr WHERE id = match_user; + + -- Max fines + SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org; + + -- Fail if the user has a high fine balance + LOOP + tmp_grp := user_object.profile; + LOOP + SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 1 AND org_unit = tmp_org.id; + + IF max_fines.threshold IS NULL THEN + SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp; + ELSE + EXIT; + END IF; + + IF tmp_grp IS NULL THEN + EXIT; + END IF; + END LOOP; + + IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN + EXIT; + END IF; + + SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou; + + END LOOP; + + IF max_fines.threshold IS NOT NULL THEN + + FOR existing_sp_row IN + SELECT * + FROM actor.usr_standing_penalty + WHERE usr = match_user + AND org_unit = max_fines.org_unit + AND (stop_date IS NULL or stop_date > NOW()) + AND standing_penalty = 1 + LOOP + RETURN NEXT existing_sp_row; + END LOOP; + + SELECT SUM(f.balance_owed) INTO current_fines + FROM money.materialized_billable_xact_summary f + JOIN ( + SELECT r.id + FROM booking.reservation r + JOIN actor.org_unit_full_path( max_fines.org_unit ) fp ON (r.pickup_lib = fp.id) + WHERE usr = match_user + AND xact_finish IS NULL + UNION ALL + SELECT g.id + FROM money.grocery g + JOIN actor.org_unit_full_path( max_fines.org_unit ) fp ON (g.billing_location = fp.id) + WHERE usr = match_user + AND xact_finish IS NULL + UNION ALL + SELECT circ.id + FROM action.circulation circ + JOIN actor.org_unit_full_path( max_fines.org_unit ) fp ON (circ.circ_lib = fp.id) + WHERE usr = match_user + AND xact_finish IS NULL ) l USING (id); + + IF current_fines >= max_fines.threshold THEN + new_sp_row.usr := match_user; + new_sp_row.org_unit := max_fines.org_unit; + new_sp_row.standing_penalty := 1; + RETURN NEXT new_sp_row; + END IF; + END IF; + + -- Start over for max overdue + SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org; + + -- Fail if the user has too many overdue items + LOOP + tmp_grp := user_object.profile; + LOOP + + SELECT * INTO max_overdue FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 2 AND org_unit = tmp_org.id; + + IF max_overdue.threshold IS NULL THEN + SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp; + ELSE + EXIT; + END IF; + + IF tmp_grp IS NULL THEN + EXIT; + END IF; + END LOOP; + + IF max_overdue.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN + EXIT; + END IF; + + SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou; + + END LOOP; + + IF max_overdue.threshold IS NOT NULL THEN + + FOR existing_sp_row IN + SELECT * + FROM actor.usr_standing_penalty + WHERE usr = match_user + AND org_unit = max_overdue.org_unit + AND (stop_date IS NULL or stop_date > NOW()) + AND standing_penalty = 2 + LOOP + RETURN NEXT existing_sp_row; + END LOOP; + + SELECT INTO items_overdue COUNT(*) + FROM action.circulation circ + JOIN actor.org_unit_full_path( max_overdue.org_unit ) fp ON (circ.circ_lib = fp.id) + WHERE circ.usr = match_user + AND circ.checkin_time IS NULL + AND circ.due_date < NOW() + AND (circ.stop_fines = 'MAXFINES' OR circ.stop_fines IS NULL); + + IF items_overdue >= max_overdue.threshold::INT THEN + new_sp_row.usr := match_user; + new_sp_row.org_unit := max_overdue.org_unit; + new_sp_row.standing_penalty := 2; + RETURN NEXT new_sp_row; + END IF; + END IF; + + -- Start over for max out + SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org; + + -- Fail if the user has too many checked out items + LOOP + tmp_grp := user_object.profile; + LOOP + SELECT * INTO max_items_out FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 3 AND org_unit = tmp_org.id; + + IF max_items_out.threshold IS NULL THEN + SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp; + ELSE + EXIT; + END IF; + + IF tmp_grp IS NULL THEN + EXIT; + END IF; + END LOOP; + + IF max_items_out.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN + EXIT; + END IF; + + SELECT INTO tmp_org * FROM actor.org_unit WHERE id = tmp_org.parent_ou; + + END LOOP; + + + -- Fail if the user has too many items checked out + IF max_items_out.threshold IS NOT NULL THEN + + FOR existing_sp_row IN + SELECT * + FROM actor.usr_standing_penalty + WHERE usr = match_user + AND org_unit = max_items_out.org_unit + AND (stop_date IS NULL or stop_date > NOW()) + AND standing_penalty = 3 + LOOP + RETURN NEXT existing_sp_row; + END LOOP; + + SELECT INTO items_out COUNT(*) + FROM action.circulation circ + JOIN actor.org_unit_full_path( max_items_out.org_unit ) fp ON (circ.circ_lib = fp.id) + WHERE circ.usr = match_user + AND circ.checkin_time IS NULL + AND (circ.stop_fines IN ('MAXFINES','LONGOVERDUE') OR circ.stop_fines IS NULL); + + IF items_out >= max_items_out.threshold::INT THEN + new_sp_row.usr := match_user; + new_sp_row.org_unit := max_items_out.org_unit; + new_sp_row.standing_penalty := 3; + RETURN NEXT new_sp_row; + END IF; + END IF; + + -- Start over for collections warning + SELECT INTO tmp_org * FROM actor.org_unit WHERE id = context_org; + + -- Fail if the user has a collections-level fine balance + LOOP + tmp_grp := user_object.profile; + LOOP + SELECT * INTO max_fines FROM permission.grp_penalty_threshold WHERE grp = tmp_grp AND penalty = 4 AND org_unit = tmp_org.id; + + IF max_fines.threshold IS NULL THEN + SELECT parent INTO tmp_grp FROM permission.grp_tree WHERE id = tmp_grp; + ELSE + EXIT; + END IF; + + IF tmp_grp IS NULL THEN + EXIT; + END IF; + END LOOP; + + IF max_fines.threshold IS NOT NULL OR tmp_org.parent_ou IS NULL THEN + EXIT; + END IF; + + SELECT * INTO tmp_org FROM actor.org_unit WHERE id = tmp_org.parent_ou; + + END LOOP; + + IF max_fines.threshold IS NOT NULL THEN + + FOR existing_sp_row IN + SELECT * + FROM actor.usr_standing_penalty + WHERE usr = match_user + AND org_unit = max_fines.org_unit + AND (stop_date IS NULL or stop_date > NOW()) + AND standing_penalty = 4 + LOOP + RETURN NEXT existing_sp_row; + END LOOP; + + SELECT SUM(f.balance_owed) INTO current_fines + FROM money.materialized_billable_xact_summary f + JOIN ( + SELECT r.id + FROM booking.reservation r + JOIN actor.org_unit_full_path( max_fines.org_unit ) fp ON (r.pickup_lib = fp.id) + WHERE usr = match_user + AND xact_finish IS NULL + UNION ALL + SELECT g.id + FROM money.grocery g + JOIN actor.org_unit_full_path( max_fines.org_unit ) fp ON (g.billing_location = fp.id) + WHERE usr = match_user + AND xact_finish IS NULL + UNION ALL + SELECT circ.id + FROM action.circulation circ + JOIN actor.org_unit_full_path( max_fines.org_unit ) fp ON (circ.circ_lib = fp.id) + WHERE usr = match_user + AND xact_finish IS NULL ) l USING (id); + + IF current_fines >= max_fines.threshold THEN + new_sp_row.usr := match_user; + new_sp_row.org_unit := max_fines.org_unit; + new_sp_row.standing_penalty := 4; + RETURN NEXT new_sp_row; + END IF; + END IF; + + + RETURN; +END; +$func$ LANGUAGE plpgsql; + +COMMIT; + diff --git a/Open-ILS/src/sql/Pg/upgrade/0106.booking.admin_permissions.sql b/Open-ILS/src/sql/Pg/upgrade/0106.booking.admin_permissions.sql new file mode 100644 index 0000000000..337d5a7276 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/0106.booking.admin_permissions.sql @@ -0,0 +1,15 @@ +BEGIN; + +INSERT INTO config.upgrade_log (version) VALUES ('0106'); -- senator + +INSERT INTO permission.perm_list (id, code, description) VALUES + (352, 'ADMIN_BOOKING_RESOURCE', oils_i18n_gettext(352, 'Enables the user to create/update/delete booking resources', 'ppl', 'description')), + (353, 'ADMIN_BOOKING_RESOURCE_TYPE', oils_i18n_gettext(353, 'Enables the user to create/update/delete booking resource types', 'ppl', 'description')), + (354, 'ADMIN_BOOKING_RESOURCE_ATTR', oils_i18n_gettext(354, 'Enables the user to create/update/delete booking resource attributes', 'ppl', 'description')), + (355, 'ADMIN_BOOKING_RESOURCE_ATTR_MAP', oils_i18n_gettext(355, 'Enables the user to create/update/delete booking resource attribute maps', 'ppl', 'description')), + (356, 'ADMIN_BOOKING_RESOURCE_ATTR_VALUE', oils_i18n_gettext(356, 'Enables the user to create/update/delete booking resource attribute values', 'ppl', 'description')), + (357, 'ADMIN_BOOKING_RESERVATION', oils_i18n_gettext(357, 'Enables the user to create/update/delete booking reservations', 'ppl', 'description')), + (358, 'ADMIN_BOOKING_RESERVATION_ATTR_VALUE_MAP', oils_i18n_gettext(358, 'Enables the user to create/update/delete booking reservation attribute value maps', 'ppl', 'description')) +; + +COMMIT; diff --git a/Open-ILS/src/sql/Pg/upgrade/0109.data.org-settings-booking_alter_due_date.sql b/Open-ILS/src/sql/Pg/upgrade/0109.data.org-settings-booking_alter_due_date.sql new file mode 100644 index 0000000000..479f615121 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/0109.data.org-settings-booking_alter_due_date.sql @@ -0,0 +1,20 @@ +BEGIN; + +INSERT INTO config.upgrade_log (version) VALUES ('0109'); --miker + +INSERT INTO config.org_unit_setting_type (name, label, description, datatype) VALUES ( + 'circ.booking_reservation.stop_circ', + 'Disallow circulation of items when they are on booking reserve and that reserve overlaps with the checkout period', + 'When true, items on booking reserve during the proposed checkout period will not be allowed to circulate unless overridden with the COPY_RESERVED.override permission.', + 'bool' +); + +INSERT INTO config.org_unit_setting_type (name, label, description, datatype) VALUES ( + 'circ.booking_reservation.default_elbow_room', + 'Default amount of time by which a circulation should be shortened to allow for booking reservation delivery', + 'When an item is on booking reserve, and that reservation overlaps with the proposed checkout period, and circulations have not been strictly disallowed on reserved items, Evergreen will attempt to adjust the due date of the circulation for this about of time before the beginning of the reservation period. If this is not possible because the due date would end up in the past, the circulation is disallowed.', + 'interval' +); + +COMMIT; + diff --git a/Open-ILS/src/sql/Pg/upgrade/0110.schema.booking_resource_type.elbow_room.sql b/Open-ILS/src/sql/Pg/upgrade/0110.schema.booking_resource_type.elbow_room.sql new file mode 100644 index 0000000000..69e98e5fe7 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/0110.schema.booking_resource_type.elbow_room.sql @@ -0,0 +1,8 @@ +BEGIN; + +INSERT INTO config.upgrade_log (version) VALUES ('0110'); --miker + +ALTER TABLE booking.resource_type ADD COLUMN elbow_room INTERVAL; + +COMMIT; + diff --git a/Open-ILS/src/sql/Pg/upgrade/0122.data.reservation-shelf-status.sql b/Open-ILS/src/sql/Pg/upgrade/0122.data.reservation-shelf-status.sql new file mode 100644 index 0000000000..e41334a27f --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/0122.data.reservation-shelf-status.sql @@ -0,0 +1,7 @@ +BEGIN; + +INSERT INTO config.upgrade_log (version) VALUES ('0122'); -- miker + +INSERT INTO config.copy_status (id,name) VALUES (15,oils_i18n_gettext(15, 'On reservation shelf', 'ccs', 'name')); + +COMMIT; diff --git a/Open-ILS/src/sql/Pg/upgrade/0130.booking.resource_constraint_and_perms.sql b/Open-ILS/src/sql/Pg/upgrade/0130.booking.resource_constraint_and_perms.sql new file mode 100644 index 0000000000..b6bf9ae589 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/0130.booking.resource_constraint_and_perms.sql @@ -0,0 +1,12 @@ +BEGIN; + +INSERT INTO config.upgrade_log (version) VALUES ('0130'); -- senator + +ALTER TABLE booking.resource DROP CONSTRAINT br_unique; +ALTER TABLE booking.resource ADD CONSTRAINT br_unique UNIQUE (owner, barcode); + +INSERT into permission.perm_list VALUES + (360, 'RETRIEVE_RESERVATION_PULL_LIST', oils_i18n_gettext(360, 'Allows a user to retrieve a booking reservation pull list', 'ppl', 'description')), + (361, 'CAPTURE_RESERVATION', oils_i18n_gettext(361, 'Allows a user to capture booking reservations', 'ppl', 'description')) ; + +COMMIT; diff --git a/Open-ILS/src/sql/Pg/upgrade/119.schema.booking.transits.sql b/Open-ILS/src/sql/Pg/upgrade/119.schema.booking.transits.sql new file mode 100644 index 0000000000..c3a1814393 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/119.schema.booking.transits.sql @@ -0,0 +1,15 @@ +BEGIN; + +INSERT INTO config.upgrade_log (version) VALUES ('0119'); -- miker + +CREATE TABLE action.reservation_transit_copy ( + reservation INT REFERENCES booking.reservation (id) ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED +) INHERITS (action.transit_copy); +ALTER TABLE action.reservation_transit_copy ADD PRIMARY KEY (id); +ALTER TABLE action.reservation_transit_copy ADD CONSTRAINT artc_tc_fkey FOREIGN KEY (target_copy) REFERENCES booking.resource (id) ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED; +CREATE INDEX active_reservation_transit_dest_idx ON "action".reservation_transit_copy (dest); +CREATE INDEX active_reservation_transit_source_idx ON "action".reservation_transit_copy (source); +CREATE INDEX active_reservation_transit_cp_idx ON "action".reservation_transit_copy (target_copy); + +COMMIT; + diff --git a/Open-ILS/src/support-scripts/test-scripts/collections.pl b/Open-ILS/src/support-scripts/test-scripts/collections.pl index 4dcf77d8c1..5b3b516be6 100644 --- a/Open-ILS/src/support-scripts/test-scripts/collections.pl +++ b/Open-ILS/src/support-scripts/test-scripts/collections.pl @@ -80,6 +80,7 @@ for my $d (@$user_data) { my $user = $xact_data->{usr}->{__data__}; my $circs = $xact_data->{transactions}->{circulations}; my $grocery = $xact_data->{transactions}->{grocery}; + my $reservations = $xact_data->{transactions}->{reservations}; # -------------------------------------------------------------------- @@ -96,7 +97,7 @@ for my $d (@$user_data) { $a->{post_code}) . "\n"; } - print_xact_details($_->{__data__}) for (@$circs, @$grocery); + print_xact_details($_->{__data__}) for (@$circs, @$grocery, @$reservations); print "\n" . '-'x60 . "\n"; } diff --git a/Open-ILS/web/css/skin/default/booking.css b/Open-ILS/web/css/skin/default/booking.css new file mode 100644 index 0000000000..2c42404366 --- /dev/null +++ b/Open-ILS/web/css/skin/default/booking.css @@ -0,0 +1,89 @@ +div#brsrc_available_outer { + width: 50%; + float: left; + border-right: 1px solid #999999; +} +div#bra_and_brav { +} +div#reserve_right_side { + float: right; + width: 49%; + padding-left: 4px; +} +div#reserve_under { + clear: both; +} +div#reserve_datetime_start { + padding-bottom: 6px; +} +div#reserve_datetime_end { + padding-bottom: 6px; + border-bottom: 1px solid #999999; +} +label.bra { + font-style: italic; + padding-right: 12px; +} +h1.booking, h2.booking, h3.booking { + margin: 0; + padding-top: 0; + padding-bottom: 8px; +} +h1.booking { font-size: 16pt; font-weight: bold; } +select#brsrc_list { + width: 90%; +} +label.reserve_datetime { + font-style: italic; + margin-bottom: 2px; +} +id#patron_barcode { + width: 150px; +} +div.nice_vertical_padding { + padding-top: 6px; + padding-bottom: 6px; +} +span.two_buttons { + text-align: center; +} +option.forced_unavailable { + background-color: #ffcccc; + color: #990000; + font-weight: bold; + font-style: italic; +} +input#arbitrary_resource { margin-left: 8px; margin-right: 8px; } +div#or { font-size: 12pt; font-weight: bold; } +input#interval_in_days { width: 75px; } +table#the_table thead tr th { + vertical-align: top; + background-color: #dddddd; + color: #000000; + font-weight: bold; + padding: 0 6px 0 6px; + border-left: 1px #cccccc solid; + border-right: 1px #333333 solid; +} +tbody#the_table_body td { + vertical-align: top; + padding: 2px; + border-top: 1px #cccccc solid; + border-left: 1px #cccccc solid; + border-bottom: 1px #333333 solid; + border-right: 1px #333333 solid; +} +.capture_failure { color: #cc0000; } +.capture_success { color: #00cc00; } +ul { list-style-type: square; } +.capture_info { font-size: 12pt; font-weight: bold; margin-bottom: 4px; } +.transit_notice { + font-size: 12pt; font-weight: bold; color: #ff6666; + margin-bottom: 4px; margin-top: 4px; +} +span#result_display { margin-left: 12px; } +div#contains_misc_controls { text-align:right; } +div#patron_info { font-size: 12pt; font-weight: bold; } +div#no_ready_bresv, div#no_out_bresv, div#no_in_bresv { + font-style: italic; +} diff --git a/Open-ILS/web/js/dojo/openils/booking/nls/capture.js b/Open-ILS/web/js/dojo/openils/booking/nls/capture.js new file mode 100644 index 0000000000..7e78e11dee --- /dev/null +++ b/Open-ILS/web/js/dojo/openils/booking/nls/capture.js @@ -0,0 +1,24 @@ +{ + 'FAILURE': "Capture failed", + 'SUCCESS': "Capture succeeded", + 'UNKNOWN_PROBLEM': "An unknown problem occurred during capture attempt.", + 'CAPTURED_NOTHING': "Didn't capture anything.", + 'NO_PAYLOAD': + "We did not receive further information from the server about this" + + "attempt to capture.", + 'HERES_WHAT_WE_KNOW': + "The following information is available about the failed capture:", + 'CAPTURE_INFO': "Capture Information", + 'CAPTURE_BRESV_DATES': "Reservation time:", + 'CAPTURE_BRESV_BRSRC': "Resource barcode:", + 'CAPTURE_BRESV_PICKUP_LIB': "Pickup library:", + 'CAPTURE_BRESV_PATRON_BARCODE': "Patron barcode:", + 'CAPTURE_CAUSES_TRANSIT': "This item is now in transit!", + 'CAPTURE_TRANSIT_SOURCE': "From:", + 'CAPTURE_TRANSIT_DEST': "To:", + + 'AUTO_capture_heading': "Capture Reserved Resources", + 'AUTO_resource_barcode': "Enter barcode:", + 'AUTO_pickup_lib_selector': "Pickup library:", + 'AUTO_ATTR_VALUE_capture': "Capture" +} diff --git a/Open-ILS/web/js/dojo/openils/booking/nls/pickup_and_return.js b/Open-ILS/web/js/dojo/openils/booking/nls/pickup_and_return.js new file mode 100644 index 0000000000..6d786e3673 --- /dev/null +++ b/Open-ILS/web/js/dojo/openils/booking/nls/pickup_and_return.js @@ -0,0 +1,38 @@ +{ + 'NO_PATRON_BARCODE': "Please enter a patron barcode.", + 'RESERVATIONS_NO_RESPONSE': + "No response from server when asking for reservations.", + 'RESERVATIONS_ERROR': + "Error communicating with server (asking for reservations):", + 'PICKUP_NO_RESPONSE': "No response from server when attempting pickup.", + 'PICKUP_ERROR': "Error communicating with server (attempting pickup):", + 'RETURN_NO_RESPONSE': "No response from server when attempting return.", + 'RETURN_ERROR': "Error communicating with server (attempting return):", + 'RETURN_SUCCESS': "Return successful.", + 'SELECT_SOMETHING': "You have not selected any reservations.", + 'NO_SUCH_RETURNABLE_RESOURCE': "No such returnable resource.", + 'RETURNABLE_RESOURCE_ERROR': "Error looking up returnable resource:", + 'NOTICE_CHANGE_OF_PATRON': + "Note that the resource scanned was out on reservation to different\n" + + "patron than the last resource you scanned. If this is not\n" + + "expected, stop to examine outstanding reservations for your patron\n" + + "or on the resource.", + + 'AUTO_h1': "Reservations Pickup", + 'AUTO_return_h1': "Reservations Return", + 'AUTO_patron_barcode': "Enter patron barcode:", + 'AUTO_barcode_type': "Return by barcode of", + 'AUTO_in_bresv': "Patron has returned these resources today:", + 'AUTO_ready_bresv': "Patron has these reservations ready for pickup:", + 'AUTO_out_bresv': "Patron currently has these reservations out:", + 'AUTO_no_ready_bresv': + "Patron has no reservations ready for pickup at this time.", + 'AUTO_no_out_bresv': "Patron has no more reservations out at this time.", + 'AUTO_no_in_bresv': "Patron has not returned any resources today.", + 'AUTO_patron': "Patron", + 'AUTO_resource': "Resource", + 'AUTO_ATTR_VALUE_go': "Go", + 'AUTO_ATTR_VALUE_reset': "Clear / New Patron", + 'AUTO_ATTR_VALUE_pickup': "Pick up", + 'AUTO_ATTR_VALUE_return': "Return" +} diff --git a/Open-ILS/web/js/dojo/openils/booking/nls/pull_list.js b/Open-ILS/web/js/dojo/openils/booking/nls/pull_list.js new file mode 100644 index 0000000000..30ae8ec804 --- /dev/null +++ b/Open-ILS/web/js/dojo/openils/booking/nls/pull_list.js @@ -0,0 +1,20 @@ +{ + 'PULL_LIST_NO_RESPONSE': "No response from server trying to get pull list!", + 'PULL_LIST_ERROR': "Error trying to fetch pull list: ", + 'COPY_LOOKUP_NO_RESPONSE': "No response looking up copies by barcode", + 'COPY_LOOKUP_ERROR': "Error looking up copies by barcode: ", + 'COPY_MISSING': "Unexpected error: No information for copy: ", + + 'AUTO_no_results': "No results", + 'AUTO_owning_lib_selector': "See pull list for library:", + 'AUTO_pull_list_title': "Booking Pull List", + 'AUTO_interval_in_days': "Generate list for this many days hence: ", + 'AUTO_ATTR_VALUE_fetch': "Fetch", + 'AUTO_th_title_or_name': "Title or name", + 'AUTO_th_barcode': "Barcode", + 'AUTO_th_call_number': "Call number", + 'AUTO_th_copy_location': "Copy location", + 'AUTO_th_copy_number': "Copy number", + 'AUTO_th_resv_details': "Reservation details", + 'AUTO_ATTR_VALUE_print': "Print", +} diff --git a/Open-ILS/web/js/dojo/openils/booking/nls/reservation.js b/Open-ILS/web/js/dojo/openils/booking/nls/reservation.js new file mode 100644 index 0000000000..76cf080732 --- /dev/null +++ b/Open-ILS/web/js/dojo/openils/booking/nls/reservation.js @@ -0,0 +1,82 @@ +{ + 'NO_BRT_RESULTS': "There are no bookable resource types registered.", + 'NO_TARG_DIV': "Could not find target div", + 'NO_BRA_RESULTS': "Couldn't retrieve booking resource attributes.", + 'SELECT_A_BRSRC_THEN': "Select a resource from the big list above.", + 'CREATE_BRESV_LOCAL_ERROR': "Exception trying to create reservation: ", + 'CREATE_BRESV_SERVER_ERROR': "Server error trying to create reservation: ", + 'CREATE_BRESV_SERVER_NO_RESPONSE': + "No response from server after trying to create reservation.", + /* FIXME: Users aren't likely to be able to do anything with the following + * message. Figure out a way to do something more helpful. + */ + 'CREATE_BRESV_OK_MISSING_TARGET': function(n, m) { + return "Created " + n + " reservation(s), but " + m + " of these " + + "couldn't target any resources.\n\n" + + "This means that it won't be possible to fulfill some of these\n" + + "reservations until a suitable resource becomes available."; + }, + 'CREATE_BRESV_OK': function(n) { + return "Created " + n + " reservation" + (n == 1 ? "" : "s") + "."; + }, + 'WHERES_THE_BARCODE': "Enter a patron's barcode to make a reservation.", + 'ACTOR_CARD_NOT_FOUND': "Patron barcode not found. Please try again.", + 'GET_BRESV_LIST_ERR': "Error while retrieving reservation list: ", + 'GET_BRESV_LIST_NO_RESULT': + "No results from server retrieving reservation list.", + 'OUTSTANDING_BRESV': "Outstanding reservations for patron", + 'UNTARGETED': "None targeted", + 'GET_PATRON_NO_RESULT': + "No server response after attempting to look up patron by barcode.", + 'HERE_ARE_EXISTING_BRESV': "Existing reservations for", + 'NO_EXISTING_BRESV': "This user has no existing reservations at this time.", + 'NO_USABLE_BRSRC': + "No reservable resources. Adjust start and end time\n" + + "until a resource is available for reservation.", + 'CXL_BRESV_SUCCESS': function(n) { + return ("Canceled " + n + " reservation" + (n == 1 ? "" : "s") + "."); + }, + 'CXL_BRESV_FAILURE': "Error canceling reservations; server silent.", + 'CXL_BRESV_FAILURE2': "Error canceling reservations:\n", + 'CXL_BRESV_SELECT_SOMETHING': + "You have not selected any reservations to cancel.", + 'NEED_EXACTLY_ONE_BRT_PASSED_IN': + "Can't book multiple resource types at once", + 'COULD_NOT_RETRIEVE_BRT_PASSED_IN': + "Error retrieving booking resource type", + 'INVALID_TS_RANGE': + "You must choose a valid start and end time for the reservation.", + 'BRSRC_NOT_FOUND': "Could not locate that resource.", + 'BRSRC_RETRIVE_ERROR': "Error retrieving resource: ", + 'ON_FLY_NO_RESPONSE': + "No response from server attempting to make item a bookable resource.", + 'ON_FLY_ERROR': + "Error attempting to make item a bookable resource:", + 'ANY': "ANY", + + 'AUTO_choose_a_brt': "Choose a Bookable Resource Type", + 'AUTO_i_need_this_resource': "I need this resource...", + 'AUTO_starting_at': "Between", + 'AUTO_ending_at': "and", + 'AUTO_with_these_attr': "With these attributes:", + 'AUTO_patron_barcode': "Reserve to patron barcode:", + 'AUTO_ATTR_VALUE_next': "Next", + 'AUTO_ATTR_VALUE_reserve_brsrc': "Reserve Selected", + 'AUTO_ATTR_VALUE_reserve_brt': "Reserve Any", + 'AUTO_ATTR_VALUE_button_edit_existing': "Edit selected", + 'AUTO_ATTR_VALUE_button_cancel_existing': "Cancel selcted", + 'AUTO_bresv_grid_type': "Type", + 'AUTO_bresv_grid_resource': "Resource", + 'AUTO_bresv_grid_start_time': "Start time", + 'AUTO_bresv_grid_end_time': "End time", + 'AUTO_brt_noncat_only': "Show only non-cataloged bookable resource types", + 'AUTO_arbitrary_resource': + "Enter the barcode of a cataloged, bookable resource:", + 'AUTO_explain_bookable': + "To reserve an item that is not yet registered as a bookable " + + "resource, find it in the catalog or under Display Item, and "+ + "select Make Item Bookable or Book Item Now there.", + 'AUTO_pickup_lib_selector': + "Choose the pickup library for this reservation:", + 'AUTO_or': '- Or -' +} diff --git a/Open-ILS/web/js/ui/default/booking/capture.js b/Open-ILS/web/js/ui/default/booking/capture.js new file mode 100644 index 0000000000..5502b5b3b2 --- /dev/null +++ b/Open-ILS/web/js/ui/default/booking/capture.js @@ -0,0 +1,174 @@ +dojo.require("openils.User"); +dojo.require("openils.widget.OrgUnitFilteringSelect"); +dojo.requireLocalization("openils.booking", "capture"); + +const CAPTURE_FAILURE = 0; +const CAPTURE_SUCCESS = 1; +const CAPTURE_UNKNOWN = 2; + +var localeStrings = dojo.i18n.getLocalization("openils.booking", "capture"); + +function CaptureDisplay(element) { this.element = element; } +CaptureDisplay.prototype.no_payload = function() { + this.element.appendChild(document.createTextNode(localeStrings.NO_PAYLOAD)); +}; +CaptureDisplay.prototype.dump = function(payload) { + var div = document.createElement("div"); + div.appendChild(document.createTextNode(localeStrings.HERES_WHAT_WE_KNOW)); + this.element.appendChild(div); + + var ul = document.createElement("ul"); + for (var k in payload) { + var li = document.createElement("li"); + li.appendChild(document.createTextNode(k + ": " + payload[k])); + ul.appendChild(li); + } + this.element.appendChild(ul); +}; +CaptureDisplay.prototype.generate_transit_display = function(payload) { + var super_div = document.createElement("div"); + var div; + + div = document.createElement("div"); + div.appendChild(document.createTextNode( + localeStrings.CAPTURE_CAUSES_TRANSIT + )); + div.setAttribute("class", "transit_notice"); + super_div.appendChild(div); + + div = document.createElement("div"); + div.appendChild(document.createTextNode( + localeStrings.CAPTURE_TRANSIT_SOURCE + " " + + fieldmapper.aou.findOrgUnit(payload.transit.source()).shortname() + )); + super_div.appendChild(div); + + div = document.createElement("div"); + div.appendChild(document.createTextNode( + localeStrings.CAPTURE_TRANSIT_DEST + " " + + fieldmapper.aou.findOrgUnit(payload.transit.dest()).shortname() + )); + super_div.appendChild(div); + + return super_div; +}; +CaptureDisplay.prototype.display_with_transit_info = function(payload) { + var div; + + div = document.createElement("div"); + div.appendChild(document.createTextNode(localeStrings.CAPTURE_INFO)); + div.setAttribute("class", "capture_info"); + this.element.appendChild(div); + + if (payload.catalog_item) { + div = document.createElement("div"); + div.appendChild(document.createTextNode( + localeStrings.CAPTURE_BRESV_BRSRC + " " + + payload.catalog_item.barcode() + )); + this.element.appendChild(div); + } + + div = document.createElement("div"); + div.appendChild(document.createTextNode( + localeStrings.CAPTURE_BRESV_DATES + " " + + humanize_timestamp_string(payload.reservation.start_time()) + " - " + + humanize_timestamp_string(payload.reservation.end_time()) + )); + this.element.appendChild(div); + + div = document.createElement("div"); + div.appendChild(document.createTextNode( + localeStrings.CAPTURE_BRESV_PICKUP_LIB + " " + + fieldmapper.aou.findOrgUnit( + payload.reservation.pickup_lib() + ).shortname() + )); + this.element.appendChild(div); + + div = document.createElement("div"); + div.appendChild(document.createTextNode( + localeStrings.CAPTURE_BRESV_PATRON_BARCODE + " " + + payload.reservation.usr().card().barcode() + )); + this.element.appendChild(div); + + if (payload.transit) { + this.element.appendChild(this.generate_transit_display(payload)); + } +}; +CaptureDisplay.prototype.clear = function() { this.element.innerHTML = ""; }; +CaptureDisplay.prototype.load = function(payload) { + try { + this.element.appendChild(document.createElement("hr")); + if (!payload) { + this.no_payload(); + } else if (!payload.fail_cause && payload.captured) { + this.display_with_transit_info(payload); + } else { + this.dump(payload); + } + } catch (E) { + alert(E); /* XXX */ + } +}; + +var capture_display; +var last_result; + +function clear_for_next() { + if (last_result == CAPTURE_SUCCESS) { + last_result = undefined; + document.getElementById("result_display").innerHTML = ""; + document.getElementById("resource_barcode").value = ""; + } +} + +function capture() { + var barcode = document.getElementById("resource_barcode").value; + var result = fieldmapper.standardRequest( + [ + "open-ils.booking", + "open-ils.booking.resources.capture_for_reservation" + ], + [xulG.auth.session.key, barcode] + ); + + if (result && result.ilsevent !== undefined) { + if (result.payload && result.payload.captured > 0) { + capture_display.load(result.payload); + return CAPTURE_SUCCESS; + } else { + capture_display.load(result.payload); + alert(my_ils_error(localeStrings.CAPTURED_NOTHING, result)); + return CAPTURE_FAILURE; + } + } else { + return CAPTURE_UNKNOWN; + } +} + +function attempt_capture() { + var rd = document.getElementById("result_display"); + capture_display.clear(); + switch(last_result = capture()) { + case CAPTURE_FAILURE: + rd.setAttribute("class", "capture_failure"); + rd.innerHTML = localeStrings.FAILURE; + break; + case CAPTURE_SUCCESS: + rd.setAttribute("class", "capture_success"); + rd.innerHTML = localeStrings.SUCCESS; + break; + default: + alert(localeStrings.UNKNOWN_PROBLEM); + break; + } +} + +function my_init() { + init_auto_l10n(document.getElementById("auto_l10n_start_here")); + capture_display = new CaptureDisplay( + document.getElementById("capture_display") + ); +} diff --git a/Open-ILS/web/js/ui/default/booking/common.js b/Open-ILS/web/js/ui/default/booking/common.js new file mode 100644 index 0000000000..cc6f302255 --- /dev/null +++ b/Open-ILS/web/js/ui/default/booking/common.js @@ -0,0 +1,71 @@ +/* Quick and dirty way to localize some strings; not recommended for reuse. + * I'm sure dojo provides a better mechanism for this, but at the moment + * this is faster to implement anew than figuring out the Right way to do + * the same thing w/ dojo. + */ +function init_auto_l10n(el) { + function do_it(myel, cls) { + if (cls) { + var clss = cls.split(" "); + for (var k in clss) { + var parts = clss[k].match(/^AUTO_ATTR_([A-Z]+)_.+$/); + if (parts && localeStrings[clss[k]]) { + myel.setAttribute( + parts[1].toLowerCase(), localeStrings[clss[k]] + ); + } else if (clss[k].match(/^AUTO_/) && localeStrings[clss[k]]) { + myel.innerHTML = localeStrings[clss[k]]; + } + } + } + } + + for (var i in el.attributes) { + if (el.attributes[i].nodeName == "class") { + do_it(el, el.attributes[i].value); + break; + } + } + for (var i in el.childNodes) { + if (el.childNodes[i].nodeType == 1) { // element node? + init_auto_l10n(el.childNodes[i]); // recurse! + } + } +} + +function get_keys(L) { var K = []; for (var k in L) K.push(k); return K; } +function hide_dom_element(e) { e.style.display = "none"; }; +function reveal_dom_element(e) { e.style.display = ""; }; +function formal_name(u) { + var name = u.family_name() + ", " + u.first_given_name(); + if (u.second_given_name()) + name += (" " + u.second_given_name()); + return name; +} +function humanize_timestamp_string(ts) { + /* For now, this discards time zones. */ + var parts = ts.split("T"); + var timeparts = parts[1].split("-")[0].split(":"); + return parts[0] + " " + timeparts[0] + ":" + timeparts[1]; +} +function is_ils_event(e) { return (e.ilsevent != undefined); } +function is_ils_actor_card_error(e) { + return (e.textcode == "ACTOR_CARD_NOT_FOUND"); +} +function my_ils_error(leader, e) { + var s = leader + "\n"; + var keys = [ + "ilsevent", "desc", "textcode", "servertime", "pid", "stacktrace" + ]; + for (var i in keys) { + if (e[keys[i]]) s += ("\t" + keys[i] + ": " + e[keys[i]] + "\n"); + } + return s; +} +function set_datagrid_empty_store(grid, flattener) { + grid.setStore( + new dojo.data.ItemFileReadStore( + {"data": flattener([])} + ) + ); +} diff --git a/Open-ILS/web/js/ui/default/booking/pickup.js b/Open-ILS/web/js/ui/default/booking/pickup.js new file mode 100644 index 0000000000..afa0527cb8 --- /dev/null +++ b/Open-ILS/web/js/ui/default/booking/pickup.js @@ -0,0 +1,32 @@ +dojo.requireLocalization("openils.booking", "pickup_and_return"); +var localeStrings = dojo.i18n.getLocalization( + "openils.booking", "pickup_and_return" +); +var p; + +function react_to_pass_in(opts) { + if (opts && opts.patron_barcode) { + p.populate({"patron": opts.patron_barcode}); + + hide_dom_element( + document.getElementById("contains_barcode_control") + ); + document.getElementById("patron_barcode").value = opts.patron_barcode; + p._extra_resetting = function() { + reveal_dom_element( + document.getElementById("contains_barcode_control") + ); + }; + } +} + +function my_init() { + p = new Populator({ + "ready": ready_bresv, + "out": out_bresv, + "patron": document.getElementById("patron_info") + }, document.getElementById("patron_barcode")); + init_auto_l10n(document.getElementById("auto_l10n_start_here")); + + react_to_pass_in(xulG.bresv_interface_opts); +} diff --git a/Open-ILS/web/js/ui/default/booking/populator.js b/Open-ILS/web/js/ui/default/booking/populator.js new file mode 100644 index 0000000000..dc0d757d16 --- /dev/null +++ b/Open-ILS/web/js/ui/default/booking/populator.js @@ -0,0 +1,283 @@ +/* This module depends on common.js being loaded, as well as the + * localization (Dojo/nls) for pickup and return . */ + +dojo.require("dojo.data.ItemFileReadStore"); + +function Populator(widgets, primary_input) { + this.widgets = widgets; + + this.all = []; + for (var k in widgets) this.all.push(k); + + if (primary_input) this.primary_input = primary_input; + + this.prepare_cache(); + this.prepare_empty_stores(); + this.reset(); +} +Populator.prototype.prepare_cache = function(data) { + this.cache = {}; + for (var k in this.all) this.cache[this.all[k]] = {}; +}; +Populator.prototype.prepare_empty_stores = function(data) { + this.empty_stores = {}; + + for (var i in this.all) { + var name = this.all[i]; + + if (this.widgets[name] && this["flatten_" + name]) { + this.empty_stores[name] = + new dojo.data.ItemFileReadStore({ + "data": this["flatten_" + name]([]) + }); + this.widgets[name].setStore(this.empty_stores[name]); + } + } +}; +Populator.prototype.flatten_ready = function(data) { + return { + "label": "id", + "identifier": "id", + "items": data.map(function(o) { + return { + "id": o.id(), + "type": o.target_resource_type().name(), + "resource": o.current_resource().barcode(), + "start_time": humanize_timestamp_string(o.start_time()), + "end_time": humanize_timestamp_string(o.end_time()) + }; + }) + }; +}; +Populator.prototype.flatten_out = function(data) { + return { + "label": "id", + "identifier": "id", + "items": data.map(function(o) { + return { + "id": o.id(), + "type": o.target_resource_type().name(), + "resource": o.current_resource().barcode(), + "pickup_time": humanize_timestamp_string(o.pickup_time()), + "end_time": humanize_timestamp_string(o.end_time()) + }; + }) + }; +}; +Populator.prototype.flatten_in = function(data) { + return { + "label": "id", + "identifier": "id", + "items": data.map(function(o) { + return { + "id": o.id(), + "type": o.target_resource_type().name(), + "resource": o.current_resource().barcode(), + "due_time": humanize_timestamp_string(o.end_time()), + "return_time": humanize_timestamp_string(o.return_time()) + }; + }) + }; +}; +Populator.prototype.reveal_container = function(widget) { + var el = document.getElementById("contains_" + widget.id); + if (el) reveal_dom_element(el); +}; +Populator.prototype.hide_container = function(widget) { + var el = document.getElementById("contains_" + widget.id); + if (el) hide_dom_element(el); +}; +Populator.prototype.populate_ready = function(data) { + return this._populate_any_resv_grid(data, "ready"); +}; +Populator.prototype.populate_out = function(data) { + return this._populate_any_resv_grid(data, "out"); +}; +Populator.prototype.populate_in = function(data) { + return this._populate_any_resv_grid(data, "in"); +}; +Populator.prototype._populate_any_resv_grid = function(data, which) { + var flattener = this["flatten_" + which]; + var widget = this.widgets[which]; + var cache = this.cache[which]; + var empty_store = this.empty_stores[which]; + + this.reveal_container(widget); + + if (!data || !data.length) { + widget.setStore(empty_store); + this.toggle_anyness(false, which); + } else { + for (var i in data) cache[data[i].id()] = data[i]; + + widget.setStore( + new dojo.data.ItemFileReadStore({"data": flattener(data)}) + ); + + this.toggle_anyness(true, which); + + /* Arrrgh! Horrid but necessary: */ + setTimeout(function() { widget.sort(); }, 100); + } +}; +Populator.prototype.populate_patron = function(data) { + var h2 = document.createElement("h2"); + h2.setAttribute("class", "booking"); + h2.appendChild(document.createTextNode(formal_name(data))); + + this.widgets.patron.innerHTML = ""; + this.widgets.patron.appendChild(h2); + + this.reveal_container(this.widgets.patron); + /* Maybe add patron's home OU or something here later... */ +}; +Populator.prototype.return_by_resource = function(barcode) { + /* XXX instead of talking to the server every time we do this, we could + * also check the "out" cache, iff we have one. */ + var r = fieldmapper.standardRequest( + ["open-ils.booking", + "open-ils.booking.reservations.by_returnable_resource_barcode"], + [xulG.auth.session.key, barcode] + ); + if (!r || r.length < 1) { + alert(localeStrings.NO_SUCH_RETURNABLE_RESOURCE); + } else if (is_ils_event(r)) { + alert(my_ils_error(localeStrings.RETURNABLE_RESOURCE_ERROR, r)); + } else { + try { + var new_barcode = r.usr().card().barcode(); + } catch (E) { + alert(localeStrings.RETURN_ERROR + "\nr: " + js2JSON(r) + "\n" + E); + return; + } + if (this.patron_barcode && this.patron_barcode != new_barcode) { + /* XXX make this more subtle, i.e. flash something in background */ + alert(localeStrings.NOTICE_CHANGE_OF_PATRON); + } + this.patron_barcode = new_barcode; + var ret = this.return(r); + if (!ret) { + alert(localeStrings.RETURN_NO_RESPONSE); + } else if (is_ils_event(ret) && ret.textcode != "SUCCESS") { + alert(my_ils_error(localeStrings.RETURN_ERROR, ret)); + } else { + /* XXX speedbump should go, but something has to happen else + * there's no indication to staff that anything happened when + * starting from a fresh (blank) return interface. + */ + alert(localeStrings.RETURN_SUCCESS); + } + this.populate(); /* Won't recurse with no args. All is well. */ + } +}; +Populator.prototype.populate = function(barcode, which) { + if (barcode) { + if (barcode.patron) { + this.patron_barcode = barcode.patron; + } + else if (barcode.resource) { /* resource OR patron, not both */ + if (!this.return_by_resource(barcode.resource)) + return; + } + } + if (!this.patron_barcode) { + alert(localeStrings.NO_PATRON_BARCODE); + return; + } + + if (!which) which = this.all; + + var result = fieldmapper.standardRequest( + ["open-ils.booking", "open-ils.booking.reservations.get_captured"], + [xulG.auth.session.key, this.patron_barcode, which] + ); + + if (!result) { + this.patron_barcode = undefined; + alert(localeStrings.RESERVATIONS_NO_RESPONSE); + } else if (is_ils_event(result)) { + this.patron_barcode = undefined; + alert(my_ils_error(localeStrings.RESERVATIONS_ERROR, result)); + } else { + for (var k in result) + this["populate_" + k](result[k]); + } +}; +Populator.prototype.toggle_anyness = function(any, which) { + var widget = this.widgets[which].domNode; + var empty_alternate = document.getElementById("no_" + widget.id); + var controls = document.getElementById("controls_" + widget.id); + if (any) { + reveal_dom_element(widget); + if (empty_alternate) hide_dom_element(empty_alternate); + if (controls) reveal_dom_element(controls); + } else { + hide_dom_element(widget); + if (empty_alternate) reveal_dom_element(empty_alternate); + if (controls) hide_dom_element(controls); + } +}; +Populator.prototype.pickup = function(reservation) { + return fieldmapper.standardRequest( + ["open-ils.circ", "open-ils.circ.reservation.pickup"], + [xulG.auth.session.key, { + "patron_barcode": this.patron_barcode, + "reservation": reservation + }] + ); +}; +Populator.prototype.return = function(reservation) { + return fieldmapper.standardRequest( + ["open-ils.circ", "open-ils.circ.reservation.return"], + [xulG.auth.session.key, { + "patron_barcode": this.patron_barcode, + "reservation": reservation.id() + /* yeah just id here ------^; lack of parallelism */ + }] + ); +}; +Populator.prototype.act_on_selected = function(how, which) { + var widget = this.widgets[which]; + var cache = this.cache[which]; + var no_response_msg = localeStrings[how.toUpperCase() + "_NO_RESPONSE"]; + var error_msg = localeStrings[how.toUpperCase() + "_ERROR"]; + + var selected_id_list = + widget.selection.getSelected().map(function(o) { return o.id[0]; }); + + if (!selected_id_list || !selected_id_list.length) { + alert(localeStrings.SELECT_SOMETHING); + return; + } + + var reservations = selected_id_list.map(function(o) { return cache[o]; }); + + /* Do we have to process these one at a time? I think so... */ + for (var i in reservations) { + var result = this[how](reservations[i]); + if (!result) { + alert(no_response_msg); + } else if (is_ils_event(result) && result.textcode != "SUCCESS") { + alert(my_ils_error(error_msg, result)); + } else { + continue; + } + break; + } + + this.populate(); +}; +Populator.prototype.reset = function() { + for (var k in this.widgets) { + this.hide_container(this.widgets[k]); + } + this.patron_barcode = undefined; + + if (typeof(this._extra_resetting) == "function") + this._extra_resetting(); + + if (this.primary_input) { + this.primary_input.value = ""; + this.primary_input.focus(); + } +}; diff --git a/Open-ILS/web/js/ui/default/booking/pull_list.js b/Open-ILS/web/js/ui/default/booking/pull_list.js new file mode 100644 index 0000000000..996cc42756 --- /dev/null +++ b/Open-ILS/web/js/ui/default/booking/pull_list.js @@ -0,0 +1,203 @@ +dojo.require("openils.User"); +dojo.require("openils.PermaCrud"); +dojo.require("fieldmapper.OrgUtils"); +dojo.require("openils.widget.OrgUnitFilteringSelect"); +dojo.requireLocalization("openils.booking", "pull_list"); + +var localeStrings = dojo.i18n.getLocalization("openils.booking", "pull_list"); +var pcrud = new openils.PermaCrud(); + +var owning_lib_selected; +var acp_cache = {}; + +function init_owning_lib_selector() { + var User = new openils.User(); + User.buildPermOrgSelector( + "RETRIEVE_RESERVATION_PULL_LIST", owning_lib_selector, null, + function() { + owning_lib_selected = owning_lib_selector.getValue(); + dojo.connect(owning_lib_selector, "onChange", + function() { owning_lib_selected = this.getValue(); } + ) + } + ); +} + +function retrieve_pull_list(ivl_in_days) { + var secs = Number(ivl_in_days) * 86400; + + if (isNaN(secs) || secs < 1) + throw new Error("Invalid interval"); + + return fieldmapper.standardRequest( + ["open-ils.booking", "open-ils.booking.reservations.get_pull_list"], + [xulG.auth.session.key, null, secs, owning_lib_selected] + ); +} + +function dom_table_rowid(resource_id) { + return "pull_list_resource_" + resource_id; +} + +function generate_result_row(one) { + function cell(id, content) { + var td = document.createElement("td"); + if (id != undefined) td.setAttribute("id", id); + td.appendChild(document.createTextNode(content)); + return td; + } + + function reservation_info_cell(one) { + var td = document.createElement("td"); + for (var i in one.reservations) { + var one_resv = one.reservations[i]; + var div = document.createElement("div"); + var s = humanize_timestamp_string(one_resv.start_time()) + " - " + + humanize_timestamp_string(one_resv.end_time()) + " " + + formal_name(one_resv.usr()); + /* FIXME: The above need patron barcode instead of name, but + * that requires a fix in the middle layer to flesh on the + * right stuff. */ + div.appendChild(document.createTextNode(s)); + td.appendChild(div); + } + return td; + } + + var baseid = dom_table_rowid(one.current_resource.id()); + + var cells = []; + cells.push(cell(undefined, one.target_resource_type.name())); + cells.push(cell(undefined, one.current_resource.barcode())); + cells.push(cell(baseid + "_call_number", "-")); + cells.push(cell(baseid + "_copy_location", "-")); + cells.push(cell(baseid + "_copy_number", "-")); + cells.push(reservation_info_cell(one)); + + var row = document.createElement("tr"); + row.setAttribute("id", baseid); + + for (var i in cells) row.appendChild(cells[i]); + return row; +} + +function render_pull_list_fundamentals(list) { + var rows = []; + + for (var i in list) + rows.push(generate_result_row(list[i])); + + document.getElementById("the_table_body").innerHTML = ""; + + for (var i in rows) + document.getElementById("the_table_body").appendChild(rows[i]); +} + +function get_all_relevant_acp(list) { + var barcodes = []; + for (var i in list) { + if (list[i].target_resource_type.catalog_item()) { + /* There shouldn't be any duplicates. No need to worry bout that */ + barcodes.push(list[i].current_resource.barcode()); + } + } + if (barcodes.length > 0) { + var results = fieldmapper.standardRequest( + [ + "open-ils.booking", + "open-ils.booking.asset.get_copy_fleshed_just_right" + ], + [xulG.auth.session.key, barcodes] + ); + + if (!results) { + alert(localeStrings.COPY_LOOKUP_NO_RESPONSE); + return null; + } else if (is_ils_event(results)) { + alert(my_ils_error(localeStrings.COPY_LOOKUP_ERROR, results)); + return null; + } else { + return results; + } + } +} + +function fill_in_pull_list_details(list, acp_cache) { + for (var i in list) { + var one = list[i]; + if (one.target_resource_type.catalog_item() == "t") { + /* FIXME: This block could stand to be a lot more elegant. */ + var call_number_el = document.getElementById( + dom_table_rowid(one.current_resource.id()) + "_call_number" + ); + var copy_location_el = document.getElementById( + dom_table_rowid(one.current_resource.id()) + "_copy_location" + ); + var copy_number_el = document.getElementById( + dom_table_rowid(one.current_resource.id()) + "_copy_number" + ); + + var bc = one.current_resource.barcode(); + + if (acp_cache[bc]) { + if (call_number_el && acp_cache[bc].call_number()) { + var value = acp_cache[bc].call_number().label(); + if (value) call_number_el.innerHTML = value; + } + if (copy_location_el && acp_cache[bc].location()) { + var value = acp_cache[bc].location().name(); + if (value) copy_location_el.innerHTML = value; + } + if (copy_number_el) { + var value = acp_cache[bc].copy_number(); + if (value) copy_number_el.innerHTML = value; + } + } else { + alert(localeStrings.COPY_MISSING + bc); + } + } + } +} + +function populate_pull_list(form) { + /* Step 1: get the pull list from the server. */ + try { + var results = retrieve_pull_list(form.interval_in_days.value); + } catch (E) { + alert(localeStrings.PULL_LIST_ERROR + E); + return; + } + if (results == null) { + alert(localeStrings.PULL_LIST_NO_RESPONSE); + return; + } else if (is_ils_event(results)) { + alert(my_ils_error(localeStrings.PULL_LIST_ERROR, results)); + return; + } + + if (results.length) { + reveal_dom_element(document.getElementById("table_goes_here")); + hide_dom_element(document.getElementById("no_results")); + + /* Step 2: render the table with the pull list */ + render_pull_list_fundamentals(results); + + /* Step 3: asynchronously fill in the copy details we're missing */ + setTimeout(function() { + var acp_cache = {}; + if ((acp_cache = get_all_relevant_acp(results))) + fill_in_pull_list_details(results, acp_cache); + }, 0); + } else { + hide_dom_element(document.getElementById("table_goes_here")); + reveal_dom_element(document.getElementById("no_results")); + } + +} + +function my_init() { + hide_dom_element(document.getElementById("table_goes_here")); + hide_dom_element(document.getElementById("no_results")); + init_owning_lib_selector(); + init_auto_l10n(document.getElementById("auto_l10n_start_here")); +} diff --git a/Open-ILS/web/js/ui/default/booking/reservation.js b/Open-ILS/web/js/ui/default/booking/reservation.js new file mode 100644 index 0000000000..8e97b22556 --- /dev/null +++ b/Open-ILS/web/js/ui/default/booking/reservation.js @@ -0,0 +1,768 @@ +/* + * Details, details... + */ +dojo.require("fieldmapper.OrgUtils"); +dojo.require("openils.PermaCrud"); +dojo.require("openils.widget.OrgUnitFilteringSelect"); +dojo.require("dojo.data.ItemFileReadStore"); +dojo.require("dijit.form.DateTextBox"); +dojo.require("dijit.form.TimeTextBox"); +dojo.requireLocalization("openils.booking", "reservation"); + +/* + * Globals; prototypes and their instances + */ +var localeStrings = dojo.i18n.getLocalization("openils.booking", "reservation"); +var pcrud = new openils.PermaCrud(); +var opts; +var our_brt; +var pickup_lib_selected; +var brt_list = []; +var brsrc_index = {}; +var bresv_index = {}; +var just_reserved_now = {}; + +function AttrValueTable() { this.t = {}; } +AttrValueTable.prototype.set = function(attr, value) { this.t[attr] = value; }; +AttrValueTable.prototype.update_from_selector = function(selector) { + var attr = selector.name.match(/_(\d+)$/)[1]; + var value = selector.options[selector.selectedIndex].value; + if (attr) + attr_value_table.set(attr, value); +}; +AttrValueTable.prototype.get_all_values = function() { + var values = []; + for (var k in this.t) { + if (this.t[k] != undefined && this.t[k] != "") + values.push(this.t[k]); + } + return values; +}; +var attr_value_table = new AttrValueTable(); + +function TimestampRange() { + this.start = new Date(); + this.end = new Date(); + + this.validity = {"start": false, "end": false}; + this.nodes = { + "start": {"date": undefined, "time": undefined}, + "end": {"date": undefined, "time": undefined} + }; + this.saved_style_properties = {}; + this.invalid_style_properties = { + "backgroundColor": "#ffcccc", + "color": "#990000", + "borderColor": "#990000", + "fontWeight": "bold" + }; +} +TimestampRange.prototype.get_timestamp = function(when) { + return this.any_widget.serialize(this[when]). + replace("T", " ").substr(0, 19); +}; +TimestampRange.prototype.get_range = function() { + return this.is_backwards() ? + [this.get_timestamp("end"), this.get_timestamp("start")] : + [this.get_timestamp("start"), this.get_timestamp("end")]; +}; +TimestampRange.prototype.update_from_widget = function(widget) { + var when = widget.id.match(/(start|end)/)[1]; + var which = widget.id.match(/(date|time)/)[1]; + + if (this.any_widget == undefined) + this.any_widget = widget; + if (this.nodes[when][which] == undefined) + this.nodes[when][which] = widget.domNode; /* We'll need this later */ + + if (when && which) { + this.update_timestamp(when, which, widget.value); + } + + this.compute_validity(); + this.paint_validity(); +}; +TimestampRange.prototype.compute_validity = function() { + if (Math.abs(this.start - this.end) < 1000) { + this.validity.end = false; + } else { + if (this.start < this.current_minimum()) + this.validity.start = false; + else + this.validity.start = true; + + if (this.end < this.current_minimum()) + this.validity.end = false; + else + this.validity.end = true; + } +}; +/* This method provides the minimum timestamp that is considered valid. For + * now it's arbitrarily "now + 15 minutes", meaning that all reservations + * must be made at least 15 minutes in the future. + * + * For reasons of keeping the middle layer happy, this should always return + * a time that is at least somewhat in the future. The ML isn't able to target + * any resources for a reservation with a start date that isn't in the future. + */ +TimestampRange.prototype.current_minimum = function() { + /* XXX This is going to be a problem with local clocks that are off. */ + var n = new Date(); + n.setTime(n.getTime() + 1000 * 900); /* XXX 15 minutes; stop hardcoding! */ + return n; +}; +TimestampRange.prototype.update_timestamp = function(when, which, value) { + if (which == "date") { + this[when].setFullYear(value.getFullYear()); + this[when].setMonth(value.getMonth()); + this[when].setDate(value.getDate()); + } else { /* "time" */ + this[when].setHours(value.getHours()); + this[when].setMinutes(value.getMinutes()); + this[when].setSeconds(0); + } +}; +TimestampRange.prototype.is_backwards = function() { + return (this.start > this.end); +}; +TimestampRange.prototype.paint_validity = function() { + for (var when in this.validity) { + if (this.validity[when]) { + this.paint_valid_node(this.nodes[when].date); + this.paint_valid_node(this.nodes[when].time); + } else { + this.paint_invalid_node(this.nodes[when].date); + this.paint_invalid_node(this.nodes[when].time); + } + } +}; +TimestampRange.prototype.paint_invalid_node = function(node) { + if (node) { + /* Just toggling the class of something would be better than + * manually setting style here, but I haven't been able to get that + * to play nicely with dojo's styling of the date/time textboxen. + */ + if (this.saved_style_properties.backgroundColor == undefined) { + for (var k in this.invalid_style_properties) { + this.saved_style_properties[k] = node.style[k]; + } + } + for (var k in this.invalid_style_properties) { + node.style[k] = this.invalid_style_properties[k]; + } + } +}; +TimestampRange.prototype.paint_valid_node = function(node) { + if (node) { + for (var k in this.saved_style_properties) { + node.style[k] = this.saved_style_properties[k]; + } + } +}; +TimestampRange.prototype.is_valid = function() { + return (this.validity.start && this.validity.end); +}; +var reserve_timestamp_range = new TimestampRange(); + +function SelectorMemory(selector) { + this.selector = selector; + this.memory = {}; +} +SelectorMemory.prototype.save = function() { + for (var i = 0; i < this.selector.options.length; i++) { + if (this.selector.options[i].selected) { + this.memory[this.selector.options[i].value] = true; + } + } +}; +SelectorMemory.prototype.restore = function() { + for (var i = 0; i < this.selector.options.length; i++) { + if (this.memory[this.selector.options[i].value]) { + if (!this.selector.options[i].disabled) + this.selector.options[i].selected = true; + } + } +}; + +/* + * These functions communicate with the middle layer. + */ +function get_all_noncat_brt() { + return pcrud.search("brt", + {"id": {"!=": null}, "catalog_item": "f"}, + {"order_by": {"brt":"name"}} + ); +} + +function get_brt_by_id(id) { + return pcrud.retrieve("brt", id); +} + +function get_brsrc_id_list() { + var options = {"type": our_brt.id(), "pickup_lib": pickup_lib_selected}; + + /* This mechanism for avoiding the passing of an empty 'attribute_values' + * option is essential because if you pass such an option to the + * middle layer API at all, it won't return any IDs for brsrcs that + * don't have at least one attribute of some kind. + */ + var attribute_values = attr_value_table.get_all_values(); + if (attribute_values.length > 0) + options.attribute_values = attribute_values; + + options.available = reserve_timestamp_range.get_range(); + + return fieldmapper.standardRequest( + ["open-ils.booking", "open-ils.booking.resources.filtered_id_list"], + [xulG.auth.session.key, options] + ); +} + +/* FIXME: We need failure checking after pcrud.retrieve() */ +function add_brsrc_to_index_if_needed(list, further) { + for (var i in list) { + if (!brsrc_index[list[i]]) { + brsrc_index[list[i]] = pcrud.retrieve("brsrc", list[i]); + } + if (further) + further(brsrc_index[list[i]]); + } +} + +function sync_brsrc_index_from_ids(available_list, additional_list) { + /* Default states for everything in the index. Read the further comments. */ + for (var i in brsrc_index) { + brsrc_index[i].isdeleted(true); + brsrc_index[i].ischanged(false); + } + + /* Populate the cache with anything that's missing and tag everything + * in the "available" list as *not* deleted, and tag everything in the + * additional list as "changed." See below. */ + add_brsrc_to_index_if_needed( + available_list, function(o) { o.isdeleted(false); } + ); + add_brsrc_to_index_if_needed( + additional_list, + function(o) { + if (!(o.id() in just_reserved_now)) o.ischanged(true); + } + ); + /* NOTE: We lightly abuse the isdeleted() and ischanged() magic fieldmapper + * attributes of the brsrcs in our cache. Because we're not going to + * pass back any brsrcs to the middle layer, it doesn't really matter + * what we set this attribute to. What we're using it for is to indicate + * in our little brsrc cache how a given brsrc should be displayed in this + * UI's current state (based on whether the brsrc matches timestamp range + * availability (isdeleted(false)) and whether the brsrc has been forced + * into the list because it was selected in a previous interface (like + * the catalog) (ischanged(true))). + */ +} + +function check_bresv_targeting(results) { + var missing = 0; + for (var i in results) { + if (!(results[i].targeting && results[i].targeting.current_resource)) { + missing++; + } else { + just_reserved_now[results[i].targeting.current_resource] = true; + } + } + return missing; +} + +function create_bresv(resource_list) { + var barcode = document.getElementById("patron_barcode").value; + if (barcode == "") { + alert(localeStrings.WHERES_THE_BARCODE); + return; + } else if (!reserve_timestamp_range.is_valid()) { + alert(localeStrings.INVALID_TS_RANGE); + return; + } + var results; + try { + results = fieldmapper.standardRequest( + ["open-ils.booking", "open-ils.booking.reservations.create"], + [ + xulG.auth.session.key, + barcode, + reserve_timestamp_range.get_range(), + pickup_lib_selected, + our_brt.id(), + resource_list, + attr_value_table.get_all_values() + ] + ); + } catch (E) { + alert(localeStrings.CREATE_BRESV_LOCAL_ERROR + E); + } + if (results) { + if (is_ils_event(results)) { + if (is_ils_actor_card_error(results)) { + alert(localeStrings.ACTOR_CARD_NOT_FOUND); + } else { + alert(my_ils_error( + localeStrings.CREATE_BRESV_SERVER_ERROR, results + )); + } + } else { + var missing; + alert((missing = check_bresv_targeting(results)) ? + localeStrings.CREATE_BRESV_OK_MISSING_TARGET( + results.length, missing + ) : + localeStrings.CREATE_BRESV_OK(results.length) + ); + update_brsrc_list(); + update_bresv_grid(); + } + } else { + alert(localeStrings.CREATE_BRESV_SERVER_NO_RESPONSE); + } +} + +function flatten_to_dojo_data(obj_list) { + return { + "label": "id", + "identifier": "id", + "items": obj_list.map(function(o) { + var new_obj = { + "id": o.id(), + "type": o.target_resource_type().name(), + "start_time": humanize_timestamp_string(o.start_time()), + "end_time": humanize_timestamp_string(o.end_time()), + }; + + if (o.current_resource()) + new_obj["resource"] = o.current_resource().barcode(); + else if (o.target_resource()) + new_obj["resource"] = "* " + o.target_resource().barcode(); + else + new_obj["resource"] = "* " + localeStrings.UNTARGETED + " *"; + return new_obj; + }) + }; +} + +function create_bresv_on_brsrc() { + var selector = document.getElementById("brsrc_list"); + var selected_values = []; + for (var i in selector.options) { + if (selector.options[i] && selector.options[i].selected) + selected_values.push(selector.options[i].value); + } + if (selected_values.length > 0) + create_bresv(selected_values); + else + alert(localeStrings.SELECT_A_BRSRC_THEN); +} + +function create_bresv_on_brt() { + if (any_usable_brsrc()) + create_bresv(); + else + alert(localeStrings.NO_USABLE_BRSRC); +} + +function get_actor_by_barcode(barcode) { + var usr = fieldmapper.standardRequest( + ["open-ils.actor", "open-ils.actor.user.fleshed.retrieve_by_barcode"], + [xulG.auth.session.key, barcode] + ); + if (usr == null) { + alert(localeStrings.GET_PATRON_NO_RESULT); + } else if (is_ils_event(usr)) { + return null; /* XXX inelegant: this function is quiet about errors + here because to report them would be redundant with + another function that gets called right after this one. + */ + } else { + return usr; + } +} + +function init_bresv_grid(barcode) { + var result = fieldmapper.standardRequest( + ["open-ils.booking", + "open-ils.booking.reservations.filtered_id_list" + ], + [xulG.auth.session.key, { + "user_barcode": barcode, + "fields": { + "pickup_time": null, + "cancel_time": null, + "return_time": null + } + }, /* whole_obj */ true] + ); + if (result == null) { + set_datagrid_empty_store(bresvGrid, flatten_to_dojo_data); + alert(localeStrings.GET_BRESV_LIST_NO_RESULT); + } else if (is_ils_event(result)) { + set_datagrid_empty_store(bresvGrid, flatten_to_dojo_data); + if (is_ils_actor_card_error(result)) { + alert(localeStrings.ACTOR_CARD_NOT_FOUND); + } else { + alert(my_ils_error(localeStrings.GET_BRESV_LIST_ERR, result)); + } + } else { + if (result.length < 1) { + document.getElementById("bresv_grid_alt_explanation").innerHTML = + localeStrings.NO_EXISTING_BRESV; + hide_dom_element(document.getElementById("bresv_grid")); + reveal_dom_element(document.getElementById("reserve_under")); + } else { + document.getElementById("bresv_grid_alt_explanation").innerHTML = + ""; + reveal_dom_element(document.getElementById("bresv_grid")); + reveal_dom_element(document.getElementById("reserve_under")); + } + /* May as well do the following in either case... */ + bresvGrid.setStore( + new dojo.data.ItemFileReadStore( + {"data": flatten_to_dojo_data(result)} + ) + ); + bresv_index = {}; + for (var i in result) { + bresv_index[result[i].id()] = result[i]; + } + } +} + +function cancel_reservations(bresv_id_list) { + try { + var result = fieldmapper.standardRequest( + ["open-ils.booking", "open-ils.booking.reservations.cancel"], + [xulG.auth.session.key, bresv_id_list] + ); + } catch (E) { + alert(localeStrings.CXL_BRESV_FAILURE2 + E); + return; + } + setTimeout(update_bresv_grid, 0); + if (!result) { + alert(localeStrings.CXL_BRESV_FAILURE); + } else if (is_ils_event(result)) { + alert(my_ils_error(localeStrings.CXL_BRESV_FAILURE2, result)); + } else { + alert(localeStrings.CXL_BRESV_SUCCESS(result.length)); + } +} + +function munge_specific_resource(barcode) { + try { + var copy_list = pcrud.search( + "acp", {"barcode": barcode, "deleted": "f"} + ); + if (copy_list && copy_list.length > 0) { + var r = fieldmapper.standardRequest( + ["open-ils.booking", + "open-ils.booking.resources.create_from_copies"], + [xulG.auth.session.key, + copy_list.map(function(o) { return o.id(); })] + ); + + if (!r) { + alert(localeStrings.ON_FLY_NO_RESPONSE); + } else if (is_ils_event(r)) { + alert(my_ils_error(localeStrings.ON_FLY_ERROR, r)); + } else { + if (!(our_brt = get_brt_by_id(r.brt[0][0]))) { + alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN); + } else { + opts.booking_results = r; + init_reservation_interface(); + } + } + } else { + alert(localeStrings.BRSRC_NOT_FOUND); + } + } catch (E) { + alert(localeStrings.BRSRC_RETRIEVE_ERROR + E); + } +} + +/* + * These functions deal with interface tricks (populating widgets, + * changing the page, etc.). + */ +function init_pickup_lib_selector() { + var User = new openils.User(); + User.buildPermOrgSelector( + "ADMIN_BOOKING_RESERVATION", pickup_lib_selector, null, + function() { + pickup_lib_selected = pickup_lib_selector.getValue(); + dojo.connect(pickup_lib_selector, "onChange", + function() { + pickup_lib_selected = this.getValue(); + update_brsrc_list(); + } + ) + } + ); +} + +function provide_brt_selector(targ_div) { + if (!targ_div) { + alert(localeStrings.NO_TARG_DIV); + } else { + brt_list = get_all_noncat_brt(); + if (!brt_list || brt_list.length < 1) { + document.getElementById("select_noncat_brt_block"). + style.display = "none"; + } else { + var selector = document.createElement("select"); + selector.setAttribute("id", "brt_selector"); + selector.setAttribute("name", "brt_selector"); + /* I'm reluctantly hardcoding this "size" attribute as 8 + * because you can't accomplish this with CSS anyway. + */ + selector.setAttribute("size", 8); + for (var i in brt_list) { + var option = document.createElement("option"); + option.setAttribute("value", brt_list[i].id()); + option.appendChild(document.createTextNode(brt_list[i].name())); + selector.appendChild(option); + } + targ_div.innerHTML = ""; + targ_div.appendChild(selector); + } + } +} + +function init_resv_iface_arb() { + init_reservation_interface(document.getElementById("arbitrary_resource")); +} + +function init_resv_iface_sel() { + init_reservation_interface(document.getElementById("brt_selector")); +} + +function init_reservation_interface(widget) { + /* Save a global reference to the brt we're going to reserve */ + if (widget && (widget.selectedIndex != undefined)) { + our_brt = brt_list[widget.selectedIndex]; + } else if (widget != undefined) { + if (!munge_specific_resource(widget.value)) + return; + } + + /* Hide and reveal relevant divs. */ + var search_block = document.getElementById("brt_search_block"); + var reserve_block = document.getElementById("brt_reserve_block"); + hide_dom_element(search_block); + reveal_dom_element(reserve_block); + + /* Get a list of attributes that can apply to that brt. */ + var bra_list = pcrud.search("bra", {"resource_type": our_brt.id()}); + if (!bra_list) { + alert(localeString.NO_BRA_LIST); + return; + } + + /* Get a table of values that can apply to the above attributes. */ + var brav_by_bra = {}; + bra_list.map(function(o) { + brav_by_bra[o.id()] = pcrud.search("brav", {"attr": o.id()}); + }); + + /* Hide the label over the attributes widgets if we have nothing to show. */ + var domf = (bra_list.length < 1) ? hide_dom_element : reveal_dom_element; + domf(document.getElementById("bra_and_brav_header")); + + /* Create DOM widgets to represent each attribute/values set. */ + for (var i in bra_list) { + var bra_div = document.createElement("div"); + bra_div.setAttribute("class", "nice_vertical_padding"); + + var bra_select = document.createElement("select"); + bra_select.setAttribute("name", "bra_" + bra_list[i].id()); + bra_select.setAttribute( + "onchange", + "attr_value_table.update_from_selector(this); update_brsrc_list();" + ); + + var bra_opt_any = document.createElement("option"); + bra_opt_any.appendChild(document.createTextNode(localeStrings.ANY)); + bra_opt_any.setAttribute("value", ""); + + bra_select.appendChild(bra_opt_any); + + var bra_label = document.createElement("label"); + bra_label.setAttribute("class", "bra"); + bra_label.appendChild(document.createTextNode(bra_list[i].name())); + + var j = bra_list[i].id(); + for (var k in brav_by_bra[j]) { + var bra_opt = document.createElement("option"); + bra_opt.setAttribute("value", brav_by_bra[j][k].id()); + bra_opt.appendChild( + document.createTextNode(brav_by_bra[j][k].valid_value()) + ); + bra_select.appendChild(bra_opt); + } + + bra_div.appendChild(bra_label); + bra_div.appendChild(bra_select); + document.getElementById("bra_and_brav").appendChild(bra_div); + } + /* Add a prominent label reminding the user what resource type they're + * asking about. */ + document.getElementById("brsrc_list_header").innerHTML = our_brt.name(); + init_pickup_lib_selector(); + update_brsrc_list(); +} + +function update_brsrc_list() { + var brsrc_id_list = get_brsrc_id_list(); + var force_list = (opts.booking_results && opts.booking_results.brsrc) ? + opts.booking_results.brsrc.map(function(o) { return o[0]; }) : []; + + sync_brsrc_index_from_ids(brsrc_id_list, force_list); + + var target_selector = document.getElementById("brsrc_list"); + var selector_memory = new SelectorMemory(target_selector); + selector_memory.save(); + target_selector.innerHTML = ""; + + for (var i in brsrc_index) { + if (brsrc_index[i].isdeleted() && (!brsrc_index[i].ischanged())) + continue; + + var opt = document.createElement("option"); + opt.setAttribute("value", brsrc_index[i].id()); + opt.appendChild(document.createTextNode(brsrc_index[i].barcode())); + + if (brsrc_index[i].isdeleted() && (brsrc_index[i].ischanged())) { + opt.setAttribute("class", "forced_unavailable"); + opt.setAttribute("disabled", "disabled"); + } + + target_selector.appendChild(opt); + } + + selector_memory.restore(); +} + +function any_usable_brsrc() { + for (var i in brsrc_index) { + if (!brsrc_index[i].isdeleted()) + return true; + } + return false; +} + +function update_bresv_grid() { + var widg = document.getElementById("patron_barcode"); + if (widg.value != "") { + setTimeout(function() { + var target = document.getElementById( + "existing_reservation_patron_line" + ); + var patron = get_actor_by_barcode(widg.value); + if (patron) { + target.innerHTML = ( + localeStrings.HERE_ARE_EXISTING_BRESV + " " + + formal_name(patron) + ": " + ); + } else { + target.innerHTML = ""; + } + }, 0); + setTimeout(function() { init_bresv_grid(widg.value); }, 0); + } +} + +function init_timestamp_widgets() { + var when = ["start", "end"]; + for (var i in when) { + reserve_timestamp_range.update_from_widget( + new dijit.form.TimeTextBox({ + name: "reserve_time_" + when[i], + value: new Date(), + constraints: { + timePattern: "HH:mm", + clickableIncrement: "T00:15:00", + visibleIncrement: "T00:15:00", + visibleRange: "T01:30:00", + }, + onChange: function() { + reserve_timestamp_range.update_from_widget(this); + update_brsrc_list(); + } + }, "reserve_time_" + when[i]) + ); + reserve_timestamp_range.update_from_widget( + new dijit.form.DateTextBox({ + name: "reserve_date_" + when[i], + value: new Date(), + onChange: function() { + reserve_timestamp_range.update_from_widget(this); + update_brsrc_list(); + } + }, "reserve_date_" + when[i]) + ); + } +} + +function cancel_selected_bresv(bresv_dojo_items) { + if (bresv_dojo_items && bresv_dojo_items.length > 0 && + (bresv_dojo_items[0].length == undefined || + bresv_dojo_items[0].length > 0)) { + cancel_reservations( + bresv_dojo_items.map(function(o) { return o.id[0]; }) + ); + /* After some delay to allow the cancellations a chance to get + * committed, refresh the brsrc list as it might reflect newly + * available resources now. */ + setTimeout(update_brsrc_list, 2000); + } else { + alert(localeStrings.CXL_BRESV_SELECT_SOMETHING); + } +} + +/* The following function should return true if the reservation interface + * should start normally (show a list of brt to choose from) or false if + * it should not (because we've "started" it some other way by setting up + * and displaying other widgets). + */ +function early_action_passthru() { + if (opts.booking_results) { + if (opts.booking_results.brt.length != 1) { + alert(localeStrings.NEED_EXACTLY_ONE_BRT_PASSED_IN); + return true; + } else if (!(our_brt = get_brt_by_id(opts.booking_results.brt[0][0]))) { + alert(localeStrings.COULD_NOT_RETRIEVE_BRT_PASSED_IN); + return true; + } + + init_reservation_interface(); + return false; + } + + if (opts.patron_barcode) { + document.getElementById("contain_patron_barcode").style.display="none"; + document.getElementById("patron_barcode").value = opts.patron_barcode; + update_bresv_grid(); + } + + return true; +} + +/* + * my_init + */ +function my_init() { + hide_dom_element(document.getElementById("brt_reserve_block")); + reveal_dom_element(document.getElementById("brt_search_block")); + hide_dom_element(document.getElementById("reserve_under")); + init_auto_l10n(document.getElementById("auto_l10n_start_here")); + init_timestamp_widgets(); + + if (!(opts = xulG.bresv_interface_opts)) opts = {}; + if (early_action_passthru()) + provide_brt_selector(document.getElementById("brt_selector_here")); +} diff --git a/Open-ILS/web/js/ui/default/booking/return.js b/Open-ILS/web/js/ui/default/booking/return.js new file mode 100644 index 0000000000..64c9cf86b3 --- /dev/null +++ b/Open-ILS/web/js/ui/default/booking/return.js @@ -0,0 +1,41 @@ +dojo.requireLocalization("openils.booking", "pickup_and_return"); +var localeStrings = dojo.i18n.getLocalization( + "openils.booking", "pickup_and_return" +); +var p; + +function react_to_pass_in(opts) { + if (opts && opts.patron_barcode) { + p.populate({"patron": opts.patron_barcode}); + + hide_dom_element( + document.getElementById("contains_barcode_control") + ); + + document.getElementById("barcode").value = opts.patron_barcode; + var barcode_type = document.getElementById("barcode_type"); + for (var i in barcode_type.options) { + if (barcode_type.options[i].value == "patron") { + barcode_type.selectedIndex = i; + break; + } + } + + p._extra_resetting = function() { + reveal_dom_element( + document.getElementById("contains_barcode_control") + ); + }; + } +} + +function my_init() { + p = new Populator({ + "out": out_bresv, + "in": in_bresv, + "patron": document.getElementById("patron_info") + }, document.getElementById("barcode")); + init_auto_l10n(document.getElementById("auto_l10n_start_here")); + + react_to_pass_in(xulG.bresv_interface_opts); +} diff --git a/Open-ILS/web/opac/locale/en-US/lang.dtd b/Open-ILS/web/opac/locale/en-US/lang.dtd index d5eeee3e93..05bbbac9a1 100644 --- a/Open-ILS/web/opac/locale/en-US/lang.dtd +++ b/Open-ILS/web/opac/locale/en-US/lang.dtd @@ -262,6 +262,8 @@ + + @@ -635,6 +637,18 @@ + + + + + + + + + + + + @@ -716,6 +730,19 @@ + + + + + + + + + + + + + @@ -1236,6 +1263,8 @@ + + @@ -1557,6 +1586,13 @@ + + + + + + + @@ -1765,6 +1801,10 @@ + + + + @@ -1946,6 +1986,10 @@ + + + + @@ -1993,6 +2037,9 @@ + + + diff --git a/Open-ILS/web/opac/skin/default/js/myopac.js b/Open-ILS/web/opac/skin/default/js/myopac.js index 2be360cd78..8876fc49b4 100644 --- a/Open-ILS/web/opac/skin/default/js/myopac.js +++ b/Open-ILS/web/opac/skin/default/js/myopac.js @@ -616,6 +616,12 @@ function myOPACShowTransactions(r) { else if(trans.xact_type() == 'grocery' ) myopacShowGenericTransaction( trans ); + +/* XXX need to copy circulation output function here + else if(trans.xact_type() == 'reservation' ) + myopacShowReservationTransaction( trans ); +*/ + } } diff --git a/Open-ILS/web/templates/default/booking/capture.tt2 b/Open-ILS/web/templates/default/booking/capture.tt2 new file mode 100644 index 0000000000..d7baf870be --- /dev/null +++ b/Open-ILS/web/templates/default/booking/capture.tt2 @@ -0,0 +1,21 @@ +[% WRAPPER "default/base.tt2" %] + + + + +
+ +

+
+ + + + +
+
+
+
+[% END %] diff --git a/Open-ILS/web/templates/default/booking/pickup.tt2 b/Open-ILS/web/templates/default/booking/pickup.tt2 new file mode 100644 index 0000000000..396e9eb842 --- /dev/null +++ b/Open-ILS/web/templates/default/booking/pickup.tt2 @@ -0,0 +1,77 @@ +[% WRAPPER "default/base.tt2" %] + + + + + +
+

+
+
+ + + +
+
+
+
+
+
+

+
+ + + + + + + + + +
TitleBarcodeStart timeEnd time
+
+
+ +
+
+
+
+
+

+
+ + + + + + + + + +
TitleBarcodePickup timeDue time
+
+
+
+
+ +
+
+
+[% END %] diff --git a/Open-ILS/web/templates/default/booking/pull_list.tt2 b/Open-ILS/web/templates/default/booking/pull_list.tt2 new file mode 100644 index 0000000000..90de2068fa --- /dev/null +++ b/Open-ILS/web/templates/default/booking/pull_list.tt2 @@ -0,0 +1,50 @@ +[% WRAPPER "default/base.tt2" %] + + + + +
+

+
+
+ + +
+
+ + + + + +
+ +
+
+
+ + + + + + + + + + + + + +
+ +
+
+
+[% END %] diff --git a/Open-ILS/web/templates/default/booking/reservation.tt2 b/Open-ILS/web/templates/default/booking/reservation.tt2 new file mode 100644 index 0000000000..b21d11e059 --- /dev/null +++ b/Open-ILS/web/templates/default/booking/reservation.tt2 @@ -0,0 +1,117 @@ +[% WRAPPER "default/base.tt2" %] + + + + +
+
+

+
+
+
+
+ +
+
+
+
+
+ + + +

+
+
+
+ +
+
+
+

+ + +
+ + +
+
+ + +
+
+ + +   + + +
+
+
+

+
+
+ + +
+
+
+ + +
+

+
+
+
+
+ +
+
+

+
+ + + + + + + + + +
TypeResourceStart timeEnd time
+
+ + +
+
+
+[% END %] diff --git a/Open-ILS/web/templates/default/booking/return.tt2 b/Open-ILS/web/templates/default/booking/return.tt2 new file mode 100644 index 0000000000..47bc853e56 --- /dev/null +++ b/Open-ILS/web/templates/default/booking/return.tt2 @@ -0,0 +1,87 @@ +[% WRAPPER "default/base.tt2" %] + + + + + +
+

+
+
+ + + + +
+
+
+
+
+
+

+
+ + + + + + + + + +
TitleBarcodePickup timeDue time
+
+
+ +
+
+
+
+
+

+
+ + + + + + + + + +
TitleBarcodeDue timeReturn time
+
+
+
+
+ +
+
+
+[% END %] diff --git a/Open-ILS/web/templates/default/conify/global/booking/reservation.tt2 b/Open-ILS/web/templates/default/conify/global/booking/reservation.tt2 new file mode 100644 index 0000000000..40a2a5c14c --- /dev/null +++ b/Open-ILS/web/templates/default/conify/global/booking/reservation.tt2 @@ -0,0 +1,39 @@ +[% WRAPPER default/base.tt2 %] +[% ctx.page_title = 'Reservations' %] + + + + + +
+
Reservations
+
+ + +
+
+ +
+ +
+
+ +[% END %] diff --git a/Open-ILS/web/templates/default/conify/global/booking/reservation_attr_value_map.tt2 b/Open-ILS/web/templates/default/conify/global/booking/reservation_attr_value_map.tt2 new file mode 100644 index 0000000000..a879e3f688 --- /dev/null +++ b/Open-ILS/web/templates/default/conify/global/booking/reservation_attr_value_map.tt2 @@ -0,0 +1,39 @@ +[% WRAPPER default/base.tt2 %] +[% ctx.page_title = 'Reservation Attribute Value Maps' %] + + + + + +
+
Reservation Attribute Value Maps
+
+ + +
+
+ +
+ +
+
+ +[% END %] diff --git a/Open-ILS/web/templates/default/conify/global/booking/resource.tt2 b/Open-ILS/web/templates/default/conify/global/booking/resource.tt2 new file mode 100644 index 0000000000..e5ae269401 --- /dev/null +++ b/Open-ILS/web/templates/default/conify/global/booking/resource.tt2 @@ -0,0 +1,43 @@ +[% WRAPPER default/base.tt2 %] +[% ctx.page_title = 'Resources' %] + + + + +
+
Resources
+
+ + +
+
+ +
+ +
+
+ +[% END %] diff --git a/Open-ILS/web/templates/default/conify/global/booking/resource_attr.tt2 b/Open-ILS/web/templates/default/conify/global/booking/resource_attr.tt2 new file mode 100644 index 0000000000..f26ef6df32 --- /dev/null +++ b/Open-ILS/web/templates/default/conify/global/booking/resource_attr.tt2 @@ -0,0 +1,39 @@ +[% WRAPPER default/base.tt2 %] +[% ctx.page_title = 'Resource Attributes' %] + + + + + +
+
Resource Attributes
+
+ + +
+
+ +
+ +
+
+ +[% END %] diff --git a/Open-ILS/web/templates/default/conify/global/booking/resource_attr_map.tt2 b/Open-ILS/web/templates/default/conify/global/booking/resource_attr_map.tt2 new file mode 100644 index 0000000000..69679afa1b --- /dev/null +++ b/Open-ILS/web/templates/default/conify/global/booking/resource_attr_map.tt2 @@ -0,0 +1,39 @@ +[% WRAPPER default/base.tt2 %] +[% ctx.page_title = 'Resource Attribute Maps' %] + + + + + +
+
Resource Attribute Maps
+
+ + +
+
+ +
+ +
+
+ +[% END %] diff --git a/Open-ILS/web/templates/default/conify/global/booking/resource_attr_value.tt2 b/Open-ILS/web/templates/default/conify/global/booking/resource_attr_value.tt2 new file mode 100644 index 0000000000..b4f92baee2 --- /dev/null +++ b/Open-ILS/web/templates/default/conify/global/booking/resource_attr_value.tt2 @@ -0,0 +1,39 @@ +[% WRAPPER default/base.tt2 %] +[% ctx.page_title = 'Resource Attribute Values' %] + + + + + +
+
Resource Attribute Values
+
+ + +
+
+ +
+ +
+
+ +[% END %] diff --git a/Open-ILS/web/templates/default/conify/global/booking/resource_type.tt2 b/Open-ILS/web/templates/default/conify/global/booking/resource_type.tt2 new file mode 100644 index 0000000000..4e8d70466b --- /dev/null +++ b/Open-ILS/web/templates/default/conify/global/booking/resource_type.tt2 @@ -0,0 +1,39 @@ +[% WRAPPER default/base.tt2 %] +[% ctx.page_title = 'Resource Types' %] + + + + + +
+
Resource Types
+
+ + +
+
+ +
+ +
+
+ +[% END %] diff --git a/Open-ILS/xul/staff_client/chrome/content/main/constants.js b/Open-ILS/xul/staff_client/chrome/content/main/constants.js index cfa2b9ed4a..aa4f2252c2 100644 --- a/Open-ILS/xul/staff_client/chrome/content/main/constants.js +++ b/Open-ILS/xul/staff_client/chrome/content/main/constants.js @@ -155,6 +155,10 @@ const api = { 'FM_AUSP_APPLY' : { 'app' : 'open-ils.actor', 'method' : 'open-ils.actor.user.penalty.apply' }, 'FM_AUSP_REMOVE' : { 'app' : 'open-ils.actor', 'method' : 'open-ils.actor.user.penalty.remove' }, 'FM_AUSP_UPDATE_NOTE' : { 'app' : 'open-ils.actor', 'method' : 'open-ils.actor.user.penalty.note.update' }, + 'FM_BOOKING_CREATE_BRT_AND_BRSRC' : { 'app' : 'open-ils.booking', 'method' : 'open-ils.booking.create_brt_and_brsrc_from_copies' }, + 'FM_BRESV_RETRIEVE_VIA_PCRUD' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.bresv.atomic' }, + 'FM_BRSRC_RETRIEVE_VIA_PCRUD' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.brsrc.atomic' }, + 'FM_BRT_RETRIEVE_VIA_PCRUD' : { 'app' : 'open-ils.pcrud', 'method' : 'open-ils.pcrud.search.brt.atomic' }, 'FM_BRE_RETRIEVE_VIA_ID' : { 'app' : 'open-ils.cat', 'method' : 'open-ils.cat.biblio.record.metadata.retrieve', 'secure' : false }, 'FM_BRE_RETRIEVE_VIA_ID.authoritative' : { 'app' : 'open-ils.cat', 'method' : 'open-ils.cat.biblio.record.metadata.retrieve.authoritative', 'secure' : false }, 'FM_BRE_ID_SEARCH_VIA_BARCODE' : { 'app' : 'open-ils.search', 'method' : 'open-ils.search.biblio.find_by_barcode', 'secure' : false }, @@ -205,6 +209,7 @@ const api = { 'FM_MP_RETRIEVE_VIA_MBTS_ID.authoritative' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.money.payment.retrieve.all.authoritative' }, 'FM_MG_CREATE' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.money.grocery.create' }, 'FM_MG_RETRIEVE' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.money.grocery.retrieve' }, + 'FM_BRESV_RETRIEVE' : { 'app' : 'open-ils.circ', 'method' : 'open-ils.circ.booking.reservation.retrieve' }, 'FM_MOBTS_HAVING_BALANCE' : { 'app' : 'open-ils.actor', 'method' : 'open-ils.actor.user.transactions.have_balance' }, 'FM_MOBTS_HAVING_BALANCE.authoritative' : { 'app' : 'open-ils.actor', 'method' : 'open-ils.actor.user.transactions.have_balance.authoritative' }, 'FM_MOBTS_TOTAL_HAVING_BALANCE' : { 'app' : 'open-ils.actor', 'method' : 'open-ils.actor.user.transactions.have_balance.total' }, diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu.js b/Open-ILS/xul/staff_client/chrome/content/main/menu.js index d59c3169f5..29f9bfcd84 100644 --- a/Open-ILS/xul/staff_client/chrome/content/main/menu.js +++ b/Open-ILS/xul/staff_client/chrome/content/main/menu.js @@ -116,7 +116,6 @@ main.menu.prototype = { ); } - var cmd_map = { 'cmd_broken' : [ ['oncommand'], @@ -589,6 +588,26 @@ main.menu.prototype = { ['oncommand'], function() { open_eg_web_page('conify/global/config/circ_modifier'); } ], + 'cmd_server_admin_booking_resource': [ + ['oncommand'], + function() { open_eg_web_page('conify/global/booking/resource'); } + ], + 'cmd_server_admin_booking_resource_type': [ + ['oncommand'], + function() { open_eg_web_page('conify/global/booking/resource_type'); } + ], + 'cmd_server_admin_booking_resource_attr': [ + ['oncommand'], + function() { open_eg_web_page('conify/global/booking/resource_attr'); } + ], + 'cmd_server_admin_booking_resource_attr_value': [ + ['oncommand'], + function() { open_eg_web_page('conify/global/booking/resource_attr_value'); } + ], + 'cmd_server_admin_booking_resource_attr_map': [ + ['oncommand'], + function() { open_eg_web_page('conify/global/booking/resource_attr_map'); } + ], 'cmd_acq_view_picklist' : [ ['oncommand'], function() { open_eg_web_page('acq/picklist/list', 'menu.cmd_acq_view_picklist.tab'); } @@ -633,7 +652,81 @@ main.menu.prototype = { ['oncommand'], function() { open_eg_web_page('conify/global/acq/distribution_formula', 'menu.cmd_acq_view_distrib_formula.tab'); } ], - + 'cmd_booking_reservation' : [ + ['oncommand'], + function() { + obj.set_tab( + "/eg/booking/reservation", + { + "tab_name": offlineStrings.getString( + "menu.cmd_booking_reservation.tab" + ), + "browser": false + }, + xulG + ); + } + ], + 'cmd_booking_pull_list' : [ + ['oncommand'], + function() { + obj.set_tab( + "/eg/booking/pull_list", + { + "tab_name": offlineStrings.getString( + "menu.cmd_booking_pull_list.tab" + ), + "browser": false + }, + xulG + ); + } + ], + 'cmd_booking_capture' : [ + ['oncommand'], + function() { + obj.set_tab( + "/eg/booking/capture", + { + "tab_name": offlineStrings.getString( + "menu.cmd_booking_capture.tab" + ), + "browser": false + }, + xulG + ); + } + ], + 'cmd_booking_reservation_pickup' : [ + ['oncommand'], + function() { + obj.set_tab( + "/eg/booking/pickup", + { + "tab_name": offlineStrings.getString( + "menu.cmd_booking_reservation_pickup.tab" + ), + "browser": false + }, + xulG + ); + } + ], + 'cmd_booking_reservation_return' : [ + ['oncommand'], + function() { + obj.set_tab( + "/eg/booking/return", + { + "tab_name": offlineStrings.getString( + "menu.cmd_booking_reservation_return.tab" + ), + "browser": false + }, + xulG + ); + } + ], 'cmd_reprint' : [ ['oncommand'], function() { diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul index c1a356da5f..e3daa503cc 100644 --- a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul +++ b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_menus.xul @@ -86,6 +86,12 @@ + + + + + + @@ -117,6 +123,11 @@ + + + + + @@ -243,6 +254,16 @@ + + + + + + + + + + @@ -305,6 +326,15 @@ + + + + + + + + + diff --git a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_overlay.xul b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_overlay.xul index 0358de29cf..4723b60505 100644 --- a/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_overlay.xul +++ b/Open-ILS/xul/staff_client/chrome/content/main/menu_frame_overlay.xul @@ -72,6 +72,7 @@ +