From: miker Date: Wed, 6 Jan 2010 18:13:28 +0000 (+0000) Subject: Patch from Lebbeous Fogle-Weekley to reservation pull list and resource capture inter... X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=4357c390c71bc23ac2ae979ff2c9232ae9f69bf1;p=evergreen%2Fpines.git Patch from Lebbeous Fogle-Weekley to reservation pull list and resource capture interfaces, and supporting backend and ML changes git-svn-id: svn://svn.open-ils.org/ILS/trunk@15264 dcc99617-32d9-48b4-a31d-7c20da2025e4 --- diff --git a/Open-ILS/src/extras/ils_events.xml b/Open-ILS/src/extras/ils_events.xml index a616504a0e..b9897700aa 100644 --- a/Open-ILS/src/extras/ils_events.xml +++ b/Open-ILS/src/extras/ils_events.xml @@ -851,6 +851,9 @@ Booking reservation capture failed + + Provided parameters describe unacceptable reservation. + diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Booking.pm b/Open-ILS/src/perlmods/OpenILS/Application/Booking.pm index b603a606c6..1705a07953 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Booking.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Booking.pm @@ -22,6 +22,7 @@ sub prepare_new_brt { $brt->name($mvr->title); $brt->record($record_id); $brt->catalog_item('t'); + $brt->transferable('t'); $brt->owner($owning_lib); return $brt; @@ -189,7 +190,7 @@ __PACKAGE__->register_method( sub create_bresv { my ($self, $client, $authtoken, - $target_user_barcode, $datetime_range, + $target_user_barcode, $datetime_range, $pickup_lib, $brt, $brsrc_list, $attr_values) = @_; $brsrc_list = [ undef ] if not defined $brsrc_list; @@ -210,7 +211,7 @@ sub create_bresv { my $bresv = new Fieldmapper::booking::reservation; $bresv->usr($usr->id); $bresv->request_lib($e->requestor->ws_ou); - $bresv->pickup_lib($e->requestor->ws_ou); + $bresv->pickup_lib($pickup_lib); $bresv->start_time($datetime_range->[0]); $bresv->end_time($datetime_range->[1]); @@ -218,9 +219,44 @@ sub create_bresv { # 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) or - return $e->die_event; - return $e->die_event if ($brsrc_itself->type != $brt); + 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); @@ -266,6 +302,7 @@ __PACKAGE__->register_method( {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'}, @@ -285,10 +322,10 @@ sub resource_list_by_attrs { return undef unless ($filters->{type} || $filters->{attribute_values}); my $query = { - 'select' => { brsrc => [ 'id' ] }, - 'from' => { brsrc => {} }, - 'where' => {}, - 'distinct' => 1 + "select" => {brsrc => ["id"]}, + "from" => {brsrc => {"brt" => {}}}, + "where" => {}, + "distinct" => 1 }; $query->{where} = {"-and" => []}; @@ -296,6 +333,14 @@ sub resource_list_by_attrs { 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' }; @@ -479,7 +524,7 @@ sub reservation_list_by_filters { } ]; $cstore->disconnect; - if (not $whole_obj) { + if (not $whole_obj or @$ids < 1) { $e->disconnect; return $ids; } @@ -529,27 +574,15 @@ NOTES sub naive_ts_string { strftime("%F %T", localtime(shift)); } -sub get_pull_list { - my ($self, $client, $auth, $range, $interval_secs, $pickup_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 - ); +# 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 - $range = [ naive_ts_string(time), naive_ts_string(time + $interval_secs) ] - if not $range; - - my @fundamental_constraints = ( - {"current_resource" => {"!=" => undef}}, - {"capture_time" => undef}, - {"cancel_time" => undef}, - {"return_time" => undef}, - {"pickup_time" => undef} - ); + my $from_clause = { + "bresv" => { + "brsrc" => {"field" => "id", "fkey" => "current_resource"} + } + }; my $query = { "select" => { @@ -562,27 +595,39 @@ sub get_pull_list { } ] }, - "from" => "bresv", + "from" => $from_clause, "where" => { "-and" => [ - json_query_ranges_overlap( - $range->[0], $range->[1], "start_time", "end_time" - ), - @fundamental_constraints - ], + {"current_resource" => {"!=" => undef}}, + {"capture_time" => undef}, + {"cancel_time" => undef}, + {"return_time" => undef}, + {"pickup_time" => undef} + ] } }; - if ($pickup_lib) { - push @{$query->{"where"}->{"-and"}}, {"pickup_lib" => $pickup_lib}; + 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 %resource_id_map = (); - my @all_ids = (); + my $current_resource_bresv_map = {}; if (@$rows) { my $id_query = { "select" => {"bresv" => ["id"]}, - "from" => "bresv", + "from" => $from_clause, "where" => { "-and" => [ {"current_resource" => "PLACEHOLDER"}, @@ -590,9 +635,9 @@ sub get_pull_list { ] } }; - if ($pickup_lib) { + if ($o->{"owning_lib"}) { push @{$id_query->{"where"}->{"-and"}}, - {"pickup_lib" => $pickup_lib}; + {"+brsrc" => {"owner" => $o->{"owning_lib"}}}; } foreach (@$rows) { @@ -602,23 +647,42 @@ sub get_pull_list { $_->{"start_time"}; my $results = $e->json_query($id_query); - if (@$results) { - my @these_ids = map { $_->{"id"} } @$results; - push @all_ids, @these_ids; - - $resource_id_map{$_->{"current_resource"}} = [@these_ids]; + if ($results && @$results) { + $current_resource_bresv_map->{$_->{"current_resource"}} = + [map { $_->{"id"} } @$results]; } } } - if (@all_ids) { + 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_ids]}, { + $e->search_booking_reservation([{"id" => [@all_bresv_ids]}, { flesh => 1, flesh_fields => { bresv => [ - "usr", - "target_resource_type", - "current_resource" + "usr", "target_resource_type", "current_resource" ]} }]) } @@ -626,12 +690,12 @@ sub get_pull_list { $e->disconnect; return [ map { my $key = $_; - my $one = $bresv_lookup{$resource_id_map{$key}->[0]}; + 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{$_} } @{$resource_id_map{$key}} + map { $bresv_lookup{$_} } @{$uncaptured->{$key}} ] }; foreach (@{$result->{"reservations"}}) { # deflesh @@ -639,7 +703,7 @@ sub get_pull_list { $_->target_resource_type($_->target_resource_type->id); } $result; - } keys %resource_id_map ]; + } keys %$uncaptured ]; } else { $e->disconnect; return []; @@ -656,7 +720,7 @@ __PACKAGE__->register_method( "range: Date/time range for reservations (opt)"}, {type => "int", desc => "interval: Seconds from now (instead of range)"}, - {type => "number", desc => "(Optional) Pickup library"} + {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 " . @@ -668,6 +732,9 @@ __PACKAGE__->register_method( 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}, @@ -677,7 +744,7 @@ sub get_copy_fleshed_just_right { } ]); - if (ref($results) eq 'ARRAY') { + if (ref($results) eq "ARRAY") { $e->disconnect; return $results->[0] unless ref $barcode; return +{ map { $_->barcode => $_ } @$results }; @@ -702,18 +769,96 @@ __PACKAGE__->register_method( ); +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 = shift; - my $client = shift; - my $auth = shift; - my $res_id = shift; + 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 ); + 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' }) @@ -723,12 +868,15 @@ sub capture_reservation { if ($reservation->cancel_time); # canceled my $resource = $e->retrieve_booking_resource( $reservation->current_resource ); - my $type = $e->retrieve_booking_resource( $resource->type ); + my $type = $e->retrieve_booking_resource_type( $resource->type ); $reservation->capture_staff( $e->requestor->id ); $reservation->capture_time( 'now' ); - return $e->event unless ( $e->update_booking_reservation( $reservation ) and $reservation = $e->data ); + 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 }; @@ -740,9 +888,9 @@ sub capture_reservation { 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 = new Fieldmapper::action::reservation_transit_copy; - $transit->copy($resource->id); + $transit->target_copy($resource->id); $transit->copy_status(15); $transit->source_send_time('now'); $transit->source($here); @@ -754,10 +902,13 @@ sub capture_reservation { my $copy = $e->search_asset_copy( { barcode => $resource->barcode, deleted => 'f' } )->[0]; if ($copy) { - return OpenILS::Event->new('OPEN_CIRCULATION_EXISTS', payload => $copy) if ($copy->status == 1); + 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} = $e->data; + $$ret{catalog_item} = $copy; # $e->data is just id (int) } } } @@ -770,7 +921,7 @@ sub capture_reservation { 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} = $e->data; + $$ret{catalog_item} = $copy; # $e->data is just id (int) } } @@ -785,11 +936,72 @@ __PACKAGE__->register_method( signature=> { params => [ {type => 'string', desc => 'Authentication token'}, - {type => 'number', desc => 'Reservation ID'} + {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" }, + } +); + + 1; diff --git a/Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm b/Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm index 345556e79f..acc422fff9 100644 --- a/Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm +++ b/Open-ILS/src/perlmods/OpenILS/Application/Circ/Circulate.pm @@ -197,7 +197,7 @@ sub run_method { 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_reservation_type ); + 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 @@ -229,7 +229,7 @@ sub run_method { )->[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_reservation_type ); + 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 ); @@ -245,7 +245,7 @@ sub run_method { 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.reservation.capture', + 'open-ils.booking.reservations.capture', $auth => $reservation->id )->gather(1); @@ -2810,6 +2810,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/Const.pm b/Open-ILS/src/perlmods/OpenILS/Const.pm index 5651390857..d1cf7668c1 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/sql/Pg/002.schema.config.sql b/Open-ILS/src/sql/Pg/002.schema.config.sql index 5d01765a6b..edffa602ef 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 ('0129'); -- Scott McKellar +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 index 94ce38e03a..f79d968cf9 100644 --- a/Open-ILS/src/sql/Pg/095.schema.booking.sql +++ b/Open-ILS/src/sql/Pg/095.schema.booking.sql @@ -50,7 +50,7 @@ CREATE TABLE booking.resource ( 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) + CONSTRAINT br_unique UNIQUE(owner, barcode) ); -- For non-catalog items: hijack barcode for name/description 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 cd06700803..eb801aeb6e 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -1249,11 +1249,11 @@ INSERT INTO permission.perm_list VALUES (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')) + (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, 1000); INSERT INTO permission.grp_tree (id, name, parent, description, perm_interval, usergroup, application_perm) VALUES 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/web/css/skin/default/booking.css b/Open-ILS/web/css/skin/default/booking.css index 11871f3630..f4d9f7ee0d 100644 --- a/Open-ILS/web/css/skin/default/booking.css +++ b/Open-ILS/web/css/skin/default/booking.css @@ -61,7 +61,7 @@ table#the_table thead tr th { color: #000000; font-weight: bold; padding: 0 6px 0 6px; - border-left: 1px #333333 solid; + border-left: 1px #cccccc solid; border-right: 1px #333333 solid; } tbody#the_table_body td { @@ -72,3 +72,12 @@ tbody#the_table_body td { 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; } 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/pull_list.js b/Open-ILS/web/js/dojo/openils/booking/nls/pull_list.js index 1bb998752c..bb661b91d9 100644 --- a/Open-ILS/web/js/dojo/openils/booking/nls/pull_list.js +++ b/Open-ILS/web/js/dojo/openils/booking/nls/pull_list.js @@ -5,7 +5,7 @@ 'COPY_LOOKUP_ERROR': "Error looking up copies by barcode: ", 'COPY_MISSING': "Unexpected error: No information for copy: ", - 'AUTO_pickup_lib_selector': "Select a location for pickup:", + '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", diff --git a/Open-ILS/web/js/dojo/openils/booking/nls/reservation.js b/Open-ILS/web/js/dojo/openils/booking/nls/reservation.js index 2939d0f3f1..2f1402ba15 100644 --- a/Open-ILS/web/js/dojo/openils/booking/nls/reservation.js +++ b/Open-ILS/web/js/dojo/openils/booking/nls/reservation.js @@ -6,7 +6,7 @@ '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: ", + "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. */ @@ -36,7 +36,8 @@ 'CXL_BRESV_SUCCESS': function(n) { return ("Canceled " + n + " reservation" + (n == 1 ? "" : "s") + "."); }, - 'CXL_BRESV_FAILURE': "Error canceling reservations.", + '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': @@ -71,5 +72,7 @@ "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/pull_list.js b/Open-ILS/web/js/ui/default/booking/pull_list.js index f295cc720b..208b10689e 100644 --- a/Open-ILS/web/js/ui/default/booking/pull_list.js +++ b/Open-ILS/web/js/ui/default/booking/pull_list.js @@ -7,17 +7,17 @@ dojo.requireLocalization("openils.booking", "pull_list"); var localeStrings = dojo.i18n.getLocalization("openils.booking", "pull_list"); var pcrud = new openils.PermaCrud(); -var pickup_lib_selected; +var owning_lib_selected; var acp_cache = {}; -function init_pickup_lib_selector() { +function init_owning_lib_selector() { var User = new openils.User(); User.buildPermOrgSelector( - "RETRIEVE_RESERVATION_PULL_LIST", pickup_lib_selector, null, + "RETRIEVE_RESERVATION_PULL_LIST", owning_lib_selector, null, function() { - pickup_lib_selected = pickup_lib_selector.getValue(); - dojo.connect(pickup_lib_selector, "onChange", - function() { pickup_lib_selected = this.getValue(); } + owning_lib_selected = owning_lib_selector.getValue(); + dojo.connect(owning_lib_selector, "onChange", + function() { owning_lib_selected = this.getValue(); } ) } ); @@ -31,7 +31,7 @@ function retrieve_pull_list(ivl_in_days) { return fieldmapper.standardRequest( ["open-ils.booking", "open-ils.booking.reservations.get_pull_list"], - [xulG.auth.session.key, null, secs, pickup_lib_selected] + [xulG.auth.session.key, null, secs, owning_lib_selected] ); } @@ -101,22 +101,24 @@ function get_all_relevant_acp(list) { barcodes.push(list[i].current_resource.barcode()); } } - 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_error(results)) { - alert(my_ils_error(localeStrings.COPY_LOOKUP_ERROR, results)); - return null; - } else { - return results; + 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_error(results)) { + alert(my_ils_error(localeStrings.COPY_LOOKUP_ERROR, results)); + return null; + } else { + return results; + } } } @@ -185,6 +187,6 @@ function populate_pull_list(form) { } function my_init() { - init_pickup_lib_selector(); + 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 index 662d4cf5ea..41426a6bcd 100644 --- a/Open-ILS/web/js/ui/default/booking/reservation.js +++ b/Open-ILS/web/js/ui/default/booking/reservation.js @@ -3,6 +3,7 @@ */ 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"); @@ -15,6 +16,7 @@ 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 = {}; @@ -208,7 +210,7 @@ function get_brt_by_id(id) { } function get_brsrc_id_list() { - var options = {"type": our_brt.id()}; + 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 @@ -298,6 +300,7 @@ function create_bresv(resource_list) { xulG.auth.session.key, barcode, reserve_timestamp_range.get_range(), + pickup_lib_selected, our_brt.id(), resource_list, attr_value_table.get_all_values() @@ -358,7 +361,7 @@ function create_bresv_on_brsrc() { var selector = document.getElementById("brsrc_list"); var selected_values = []; for (var i in selector.options) { - if (selector.options[i].selected) + if (selector.options[i] && selector.options[i].selected) selected_values.push(selector.options[i].value); } if (selected_values.length > 0) @@ -440,20 +443,19 @@ function init_bresv_grid(barcode) { } } -function cancel_reservations(bresv_list) { - for (var i in bresv_list) { bresv_list[i].cancel_time("now"); } - pcrud.update( - bresv_list, { - "oncomplete": function() { - update_bresv_grid(); - alert(localeStrings.CXL_BRESV_SUCCESS(bresv_list.length)); - }, - "onerror": function(o) { - update_bresv_grid(); - alert(localeStrings.CXL_BRESV_FAILURE + "\n" + o); - } - } +function cancel_reservations(bresv_id_list) { + var result = fieldmapper.standardRequest( + ["open-ils.booking", "open-ils.booking.reservations.cancel"], + [xulG.auth.session.key, bresv_id_list] ); + setTimeout(update_bresv_grid, 0); + if (!result) { + alert(localeStrings.CXL_BRESV_FAILURE); + } else if (is_ils_error(result)) { + alert(my_ils_error(localeStrings.CXL_BRESV_FAILURE2, result)); + } else { + alert(localeStrings.CXL_BRESV_SUCCESS(result.length)); + } } function munge_specific_resource(barcode) { @@ -481,6 +483,22 @@ function munge_specific_resource(barcode) { * 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); @@ -588,6 +606,7 @@ function init_reservation_interface(widget) { /* 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(); } @@ -686,7 +705,7 @@ function init_timestamp_widgets() { function cancel_selected_bresv(bresv_dojo_items) { if (bresv_dojo_items && bresv_dojo_items.length > 0) { cancel_reservations( - bresv_dojo_items.map(function(o) { return bresv_index[o.id]; }) + 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 diff --git a/Open-ILS/web/opac/locale/en-US/lang.dtd b/Open-ILS/web/opac/locale/en-US/lang.dtd index 9da593cba6..1f34e80e18 100644 --- a/Open-ILS/web/opac/locale/en-US/lang.dtd +++ b/Open-ILS/web/opac/locale/en-US/lang.dtd @@ -772,6 +772,8 @@ + + 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/pull_list.tt2 b/Open-ILS/web/templates/default/booking/pull_list.tt2 index cbaf29ffc4..fa0657aae9 100644 --- a/Open-ILS/web/templates/default/booking/pull_list.tt2 +++ b/Open-ILS/web/templates/default/booking/pull_list.tt2 @@ -6,11 +6,11 @@

-
-