From ba51c5c17e19e05c37a9672b2a62e990622323fe Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Tue, 28 Jun 2016 17:05:56 +0300 Subject: [PATCH] Bug 16826: Add API route for getting item availability GET /availability/items?itemnumber=123 GET /availability/items?itemnumber=123+456+789 GET /availability/items?biblionumber=321 GET /availability/items?biblionumber=321+654+987 This patch adds above routes for checking anonymous item availability. Patron status is not checked. The returned data is for example: { "checkout": { "available": false, "description": ["onloan"], "expected_available": "2016-07-13" }, "hold": { "available": true, "description": [], "expected_available": null }, "local_use": { "available": false, "description": ["onloan"], "expected_available": "2016-07-13" }, "onsite_checkout": { "available": false, "description": ["onsite_checkouts_disabled"], "expected_available": null }, "hold_queue_length": 1, + some basic item information on locations, bib/bib(item)numbers etc. } Possible values in availability description are an empty array and any of: "onloan", "reserved", "damaged", "withdrawn", "itemlost", "restricted", "notforloan", "ordered". (+"onsite_checkouts_disabled" for onsite_checkout) To test: 1. Apply patch and run minifySwagger.pl perl misc/devel/minifySwagger.pl -s api/v1/swagger/swagger.json -d api/v1/swagger 2. Play around with an item. Place a hold on it, checkout, set it damaged etc. 3. Make GET requests to /api/v1/availability/items?itemnumber=YYY, where YYY is an existing itemnumber. You can also try with biblionumber=XXX query parameter, where XXX is an existing biblionumber. 4. Check that the availability statuses are correct and that availability description lists the appropriate status for unavailability. 5. Repeat steps 2-4 until you are confident the route works as expected. 6. Run the following tests: - t/Koha/Item/Availability.t - t/db_dependent/Items.t - t/db_dependent/api/v1/availability.t Signed-off-by: Jiri Kozlovsky --- Koha/Item.pm | 171 ++++++++++++ Koha/Item/Availability.pm | 283 ++++++++++++++++++++ Koha/REST/V1/Availability.pm | 99 +++++++ api/v1/swagger/definitions.json | 6 + api/v1/swagger/definitions/availabilities.json | 4 + api/v1/swagger/definitions/availability.json | 57 ++++ api/v1/swagger/definitions/availabilitystatus.json | 16 ++ api/v1/swagger/parameters.json | 6 + api/v1/swagger/parameters/biblio.json | 12 + api/v1/swagger/parameters/item.json | 10 + api/v1/swagger/paths.json | 3 + api/v1/swagger/paths/availability.json | 36 +++ t/Koha/Item/Availability.t | 71 +++++ t/db_dependent/Items.t | 111 +++++++- t/db_dependent/api/v1/availability.t | 289 +++++++++++++++++++++ 15 files changed, 1173 insertions(+), 1 deletion(-) create mode 100644 Koha/Item/Availability.pm create mode 100644 Koha/REST/V1/Availability.pm create mode 100644 api/v1/swagger/definitions/availabilities.json create mode 100644 api/v1/swagger/definitions/availability.json create mode 100644 api/v1/swagger/definitions/availabilitystatus.json create mode 100644 api/v1/swagger/parameters/biblio.json create mode 100644 api/v1/swagger/paths/availability.json create mode 100644 t/Koha/Item/Availability.t create mode 100644 t/db_dependent/api/v1/availability.t diff --git a/Koha/Item.pm b/Koha/Item.pm index 0bbc493..aced6b6 100644 --- a/Koha/Item.pm +++ b/Koha/Item.pm @@ -23,7 +23,12 @@ use Carp; use Koha::Database; +use C4::Context; +use Koha::Holds; +use Koha::Issues; +use Koha::Item::Availability; use Koha::Item::Transfer; +use Koha::ItemTypes; use Koha::Patrons; use Koha::Libraries; @@ -39,6 +44,160 @@ Koha::Item - Koha Item object class =cut +=head3 availabilities + +my $available = $item->availabilities(); + +Gets different availability types, generally, without considering patron status. + +Returns hash-ref containing Koha::Item::Availability objects for each availability +type. Currently implemented availabilities are: + * hold + * checkout + * local_use + * onsite_checkout + +=cut + +sub availabilities { + my ( $self, $params ) = @_; + + my $availabilities; # contains different types of availabilities + my $availability = Koha::Item::Availability->new->set_available; + + $availability->set_unavailable("withdrawn") if $self->withdrawn; + $availability->set_unavailable("itemlost") if $self->itemlost; + $availability->set_unavailable("restricted") if $self->restricted; + + if ($self->damaged) { + if (C4::Context->preference('AllowHoldsOnDamagedItems')) { + $availability->add_description("damaged"); + } else { + $availability->set_unavailable("damaged"); + } + } + + my $itemtype; + if (C4::Context->preference('item-level_itypes')) { + $itemtype = Koha::ItemTypes->find( $self->itype ); + } else { + my $biblioitem = Koha::Biblioitems->find( $self->biblioitemnumber ); + $itemtype = Koha::ItemTypes->find( $biblioitem->itemype ); + } + + if ($self->notforloan > 0 || $itemtype && $itemtype->notforloan) { + $availability->set_unavailable("notforloan"); + } elsif ($self->notforloan < 0) { + $availability->set_unavailable("ordered"); + } + + # Hold + $availabilities->{'hold'} = $availability->clone; + + # Checkout + if ($self->onloan) { + my $issue = Koha::Issues->search({ itemnumber => $self->itemnumber })->next; + $availability->set_unavailable("onloan", $issue->date_due) if $issue; + } + + if (Koha::Holds->search( [ + { itemnumber => $self->itemnumber }, + { found => [ '=', 'W', 'T' ] } + ])->count()) { + $availability->set_unavailable("reserved"); + } + + $availabilities->{'checkout'} = $availability->clone; + + # Local Use, + if (grep(/^notforloan$/, @{$availability->{description}}) + && @{$availability->{description}} == 1) { + $availabilities->{'local_use'} = $availability->clone->set_available + ->del_description("notforloan"); + } else { + $availabilities->{'local_use'} = $availability->clone + ->del_description("notforloan"); + } + + # On-site checkout + if (!C4::Context->preference('OnSiteCheckouts')) { + $availabilities->{'onsite_checkout'} + = Koha::Item::Availability->new + ->set_unavailable("onsite_checkouts_disabled"); + } else { + $availabilities->{'onsite_checkout'} + = $availabilities->{'local_use'}->clone; + } + + return $availabilities; +} + +=head3 availability_for_checkout + +my $available = $item->availability_for_checkout(); + +Gets checkout availability of the item. This subroutine does not check patron +status, instead the purpose is to check general availability for this item. + +Returns Koha::Item::Availability object. + +=cut + +sub availability_for_checkout { + my ( $self ) = @_; + + return $self->availabilities->{'checkout'}; +} + +=head3 availability_for_local_use + +my $available = $item->availability_for_local_use(); + +Gets local use availability of the item. + +Returns Koha::Item::Availability object. + +=cut + +sub availability_for_local_use { + my ( $self ) = @_; + + return $self->availabilities->{'local_use'}; +} + +=head3 availability_for_onsite_checkout + +my $available = $item->availability_for_onsite_checkout(); + +Gets on-site checkout availability of the item. + +Returns Koha::Item::Availability object. + +=cut + +sub availability_for_onsite_checkout { + my ( $self ) = @_; + + return $self->availabilities->{'onsite_checkout'}; +} + +=head3 availability_for_reserve + +my $available = $item->availability_for_reserve(); + +Gets reserve availability of the item. This subroutine does not check patron +status, instead the purpose is to check general availability for this item. + +Returns Koha::Item::Availability object. + +=cut + +sub availability_for_reserve { + my ( $self ) = @_; + + return $self->availabilities->{'hold'}; +} + =head3 effective_itemtype Returns the itemtype for the item based on whether item level itemtypes are set or not. @@ -51,6 +210,18 @@ sub effective_itemtype { return $self->_result()->effective_itemtype(); } +=head3 hold_queue_length + +=cut + +sub hold_queue_length { + my ( $self ) = @_; + + my $reserves = Koha::Holds->search({ itemnumber => $self->itemnumber }); + return $reserves->count() if $reserves; + return 0; +} + =head3 home_branch =cut diff --git a/Koha/Item/Availability.pm b/Koha/Item/Availability.pm new file mode 100644 index 0000000..c143386 --- /dev/null +++ b/Koha/Item/Availability.pm @@ -0,0 +1,283 @@ +package Koha::Item::Availability; + +# Copyright KohaSuomi 2016 +# +# This file is part of Koha. +# +# Koha 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 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Storable qw(dclone); + +=head1 NAME + +Koha::Item::Availability - Koha Item Availability object class + +=head1 SYNOPSIS + + my $item = Koha::Items->find(1337); + my $availabilities = $item->availabilities(); + # ref($availabilities) eq 'HASH' + # ref($availabilities->{'checkout'}) eq 'Koha::Item::Availability' + + print "Available for checkout!" if $availabilities->{'checkout'}->{available}; + +=head1 DESCRIPTION + +This class holds item availability information. + +See Koha::Item for availability subroutines. + +=head2 Class Methods + +=cut + +=head3 new + +Returns a new Koha::Item::Availability object. + +=cut + +sub new { + my ( $class ) = @_; + + my $self = { + description => [], + availability_needs_confirmation => undef, + available => undef, + expected_available => undef, + }; + + bless( $self, $class ); +} + +=head3 add_description + +$availability->add_description("notforloan"); +$availability->add_description("withdrawn); + +# $availability->{description} = ["notforloan", "withdrawn"] + +Pushes a new description to $availability object. Does not duplicate existing +descriptions. + +Returns updated Koha::Item::Availability object. + +=cut + +sub add_description { + my ($self, $description) = @_; + + return $self unless $description; + + if (ref($description) eq 'ARRAY') { + foreach my $desc (@$description) { + if (grep(/^$desc$/, @{$self->{description}})){ + next; + } + push $self->{description}, $desc; + } + } else { + if (!grep(/^$description$/, @{$self->{description}})){ + push $self->{description}, $description; + } + } + + return $self; +} + +=head3 clone + +$availability_cloned = $availability->clone; +$availability->set_unavailable; + +# $availability_cloned->{available} != $availability->{available} + +Clones the Koha::Item::Availability object. + +Returns cloned object. + +=cut + +sub clone { + my ( $self ) = @_; + + return dclone($self); +} + +=head3 del_description + +$availability->add_description(["notforloan", "withdrawn", "itemlost", "restricted"]); +$availability->del_description("withdrawn"); + +# $availability->{description} == ["notforloan", "itemlost", "restricted"] +$availability->del_description(["withdrawn", "restricted"]); +# $availability->{description} == ["itemlost"] + +Deletes an availability description(s) if it exists. + +Returns (possibly updated) Koha::Item::Availability object. + +=cut + +sub del_description { + my ($self, $description) = @_; + + return $self unless $description; + + my @updated; + if (ref($description) eq 'ARRAY') { + foreach my $desc (@$description) { + @updated = grep(!/^$desc$/, @{$self->{description}}); + } + } else { + @updated = grep(!/^$description$/, @{$self->{description}}); + } + $self->{description} = \@updated; + + return $self; +} + +=head3 has_description + +$availability->add_description(["notforloan", "withdrawn"]); +$availability->has_description("withdrawn"); # 1 +$availability->has_description(["notforloan", "withdrawn"]); # 1 +$availability->has_description("itemlost"); # 0 + +Finds description(s) in availability descriptions. + +Returns 1 if found, 0 otherwise. + +=cut + +sub has_description { + my ($self, $description) = @_; + + return 0 unless $description; + + my @found; + if (ref($description) eq 'ARRAY') { + foreach my $desc (@$description) { + if (!grep(/^$desc$/, @{$self->{description}})){ + return 0; + } + } + } else { + if (!grep(/^$description$/, @{$self->{description}})){ + return 0; + } + } + + return 1; +} + +=head3 reset + +$availability->reset; + +Resets the object. + +=cut + +sub reset { + my ( $self ) = @_; + + $self->{available} = undef; + $self->{availability_needs_confirmation} = undef; + $self->{expected_available} = undef; + $self->{description} = []; + return $self; +} + +=head3 set_available + +$availability->set_available; + +Sets the Koha::Item::Availability object status to available. + $availability->{available} == 1 + +Overrides old availability status, but does not override other stored data in +the object. Create a new Koha::Item::Availability object to get a fresh start. +Appends any previously defined availability descriptions with add_description(). + +Returns updated Koha::Item::Availability object. + +=cut + +sub set_available { + my ($self, $description) = @_; + + return $self->_update_availability_status(1, 0, $description); +} + +=head3 set_needs_confirmation + +$availability->set_needs_confirmation("unbelieveable_reason", "2016-07-07"); + +Sets the Koha::Item::Availability object status to unavailable, +but needs confirmation. + $availability->{available} == 0 + $availability->{availability_needs_confirmation} == 1 + +Overrides old availability statuses, but does not override other stored data in +the object. Create a new Koha::Item::Availability object to get a fresh start. +Appends any previously defined availability descriptions with add_description(). +Allows you to define expected availability date in C<$expected>. + +Returns updated Koha::Item::Availability object. + +=cut + +sub set_needs_confirmation { + my ($self, $description, $expected) = @_; + + return $self->_update_availability_status(0, 1, $description, $expected); +} + +=head3 set_unavailable + +$availability->set_unavailable("onloan", "2016-07-07"); + +Sets the Koha::Item::Availability object status to unavailable. + $availability->{available} == 0 + +Overrides old availability status, but does not override other stored data in +the object. Create a new Koha::Item::Availability object to get a fresh start. +Appends any previously defined availability descriptions with add_description(). +Allows you to define expected availability date in C<$expected>. + +Returns updated Koha::Item::Availability object. + +=cut + +sub set_unavailable { + my ($self, $description, $expected) = @_; + + return $self->_update_availability_status(0, 0, $description, $expected); +} + +sub _update_availability_status { + my ( $self, $available, $needs, $desc, $expected ) = @_; + + $self->{available} = $available; + $self->{availability_needs_confirmation} = $needs; + $self->{expected_available} = $expected if $expected; + $self->add_description($desc); + + return $self; +} + +1; diff --git a/Koha/REST/V1/Availability.pm b/Koha/REST/V1/Availability.pm new file mode 100644 index 0000000..ee4cf10 --- /dev/null +++ b/Koha/REST/V1/Availability.pm @@ -0,0 +1,99 @@ +package Koha::REST::V1::Availability; + +# This file is part of Koha. +# +# Koha 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 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; +use Mojo::JSON; + +use Koha::Holds; +use Koha::Items; + +sub items { + my ($c, $args, $cb) = @_; + + my @items; + if ($args->{'itemnumber'}) { + push @items, _item_availability(@{$args->{'itemnumber'}}); + } + if ($args->{'biblionumber'}) { + my $found_items = Koha::Items->search({ biblionumber => { + '=', \@{$args->{'biblionumber'}} + } })->as_list(); + my @itemnumbers; + foreach my $item (@$found_items) { + push @itemnumbers, $item->itemnumber; + } + + push @items, _item_availability(@itemnumbers); + } + + return $c->$cb({ error => "Item(s) not found"}, 404) unless scalar @items; + return $c->$cb([ @items ], 200); +} + +sub _item_availability { + my (@itemnumbers) = @_; + + my @items; + + foreach my $itemnumber (@itemnumbers) { + my $item = Koha::Items->find($itemnumber); + + unless ($item) { + next; + } + + my $availabilities = _swaggerize_availabilities($item->availabilities()); + + my $holds; + $holds->{'hold_queue_length'} = $item->hold_queue_length(); + + my $iteminfo = { + itemnumber => $item->itemnumber, + barcode => $item->barcode, + biblionumber => $item->biblionumber, + biblioitemnumber => $item->biblioitemnumber, + holdingbranch => $item->holdingbranch, + homebranch => $item->homebranch, + location => $item->location, + itemcallnumber => $item->itemcallnumber, + }; + + # merge availability, hold information and item information + push @items, { %{$availabilities}, %{$holds}, %{$iteminfo} }; + } + + return @items; +} + +sub _swaggerize_availabilities { + my ($availabilities) = @_; + + foreach my $availability (keys $availabilities) { + delete $availabilities->{$availability}->{availability_needs_confirmation}; + $availabilities->{$availability}->{available} = + $availabilities->{$availability}->{available} + ? Mojo::JSON->true + : Mojo::JSON->false; + $availabilities->{$availability} = { %{$availabilities->{$availability}} }; + } + + return $availabilities; +} + +1; diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json index a6fa6cf..9de637d 100644 --- a/api/v1/swagger/definitions.json +++ b/api/v1/swagger/definitions.json @@ -1,4 +1,10 @@ { + "availability": { + "$ref": "definitions/availability.json" + }, + "availabilities": { + "$ref": "definitions/availabilities.json" + }, "patron": { "$ref": "definitions/patron.json" }, diff --git a/api/v1/swagger/definitions/availabilities.json b/api/v1/swagger/definitions/availabilities.json new file mode 100644 index 0000000..20fa958 --- /dev/null +++ b/api/v1/swagger/definitions/availabilities.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "availability.json" } +} diff --git a/api/v1/swagger/definitions/availability.json b/api/v1/swagger/definitions/availability.json new file mode 100644 index 0000000..ae70624 --- /dev/null +++ b/api/v1/swagger/definitions/availability.json @@ -0,0 +1,57 @@ +{ + "type": "object", + "properties": { + "barcode": { + "type": ["string", "null"], + "description": "item barcode" + }, + "biblioitemnumber": { + "type": "string", + "description": "internally assigned biblio item identifier" + }, + "biblionumber": { + "type": "string", + "description": "internally assigned biblio identifier" + }, + "checkout": { + "$ref": "availabilitystatus.json" + }, + "expected_available": { + "type": ["string", "null"], + "description": "date this item is expected to be available" + }, + "hold": { + "$ref": "availabilitystatus.json" + }, + "holdQueueLength": { + "type": ["integer", "null"], + "description": "number of holdings placed on title/item" + }, + "holdingbranch": { + "type": ["string", "null"], + "description": "library that is currently in possession item" + }, + "homebranch": { + "type": ["string", "null"], + "description": "library that owns this item" + }, + "itemcallnumber": { + "type": ["string", "null"], + "description": "call number for this item" + }, + "itemnumber": { + "type": "string", + "description": "internally assigned item identifier" + }, + "local_use": { + "$ref": "availabilitystatus.json" + }, + "location": { + "type": ["string", "null"], + "description": "authorized value for the shelving location for this item" + }, + "onsite_checkout": { + "$ref": "availabilitystatus.json" + } + } +} diff --git a/api/v1/swagger/definitions/availabilitystatus.json b/api/v1/swagger/definitions/availabilitystatus.json new file mode 100644 index 0000000..2bf1ea6 --- /dev/null +++ b/api/v1/swagger/definitions/availabilitystatus.json @@ -0,0 +1,16 @@ +{ + "type": "object", + "properties": { + "available": { + "type": "boolean", + "description": "availability status" + }, + "description": { + "type": "array", + "items": { + "type": ["string", "null"], + "description": "more information on availability" + } + } + } +} diff --git a/api/v1/swagger/parameters.json b/api/v1/swagger/parameters.json index 38c19a7..ed2e672 100644 --- a/api/v1/swagger/parameters.json +++ b/api/v1/swagger/parameters.json @@ -1,4 +1,7 @@ { + "biblionumbersQueryParam": { + "$ref": "parameters/biblio.json#/biblionumbersQueryParam" + }, "borrowernumberPathParam": { "$ref": "parameters/patron.json#/borrowernumberPathParam" }, @@ -10,5 +13,8 @@ }, "itemnumberPathParam": { "$ref": "parameters/item.json#/itemnumberPathParam" + }, + "itemnumbersQueryParam": { + "$ref": "parameters/item.json#/itemnumbersQueryParam" } } diff --git a/api/v1/swagger/parameters/biblio.json b/api/v1/swagger/parameters/biblio.json new file mode 100644 index 0000000..b5d9534 --- /dev/null +++ b/api/v1/swagger/parameters/biblio.json @@ -0,0 +1,12 @@ +{ + "biblionumbersQueryParam": { + "name": "biblionumber", + "in": "query", + "description": "Internal biblios identifier", + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "ssv" + } +} diff --git a/api/v1/swagger/parameters/item.json b/api/v1/swagger/parameters/item.json index c215adb..409be6c 100644 --- a/api/v1/swagger/parameters/item.json +++ b/api/v1/swagger/parameters/item.json @@ -5,5 +5,15 @@ "description": "Internal item identifier", "required": true, "type": "integer" + }, + "itemnumbersQueryParam": { + "name": "itemnumber", + "in": "query", + "description": "Internal items identifier", + "type": "array", + "items": { + "type": "integer" + }, + "collectionFormat": "ssv" } } diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index e36ebb3..11243fa 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -1,4 +1,7 @@ { + "/availability/items": { + "$ref": "paths/availability.json#/~1availability~1items" + }, "/holds": { "$ref": "paths/holds.json#/~1holds" }, diff --git a/api/v1/swagger/paths/availability.json b/api/v1/swagger/paths/availability.json new file mode 100644 index 0000000..b482757 --- /dev/null +++ b/api/v1/swagger/paths/availability.json @@ -0,0 +1,36 @@ +{ + "/availability/items": { + "get": { + "operationId": "itemsAvailability", + "tags": ["items", "availability"], + "parameters": [{ + "$ref": "../parameters.json#/itemnumbersQueryParam" + }, { + "$ref": "../parameters.json#/biblionumbersQueryParam" + } + ], + "consumes": ["application/json"], + "produces": ["application/json"], + "responses": { + "200": { + "description": "Availability information on item(s)", + "schema": { + "$ref": "../definitions.json#/availabilities" + } + }, + "400": { + "description": "Missing or wrong parameters", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "No item(s) found", + "schema": { + "$ref": "../definitions.json#/error" + } + } + } + } + } +} diff --git a/t/Koha/Item/Availability.t b/t/Koha/Item/Availability.t new file mode 100644 index 0000000..5322c19 --- /dev/null +++ b/t/Koha/Item/Availability.t @@ -0,0 +1,71 @@ +#!/usr/bin/perl + +# Copyright KohaSuomi 2016 +# +# This file is part of Koha +# +# Koha 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 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Test::More tests => 16; + +use_ok('Koha::Item::Availability'); + +my $availability = Koha::Item::Availability->new->set_available; + +is($availability->{available}, 1, "Available"); +$availability->set_needs_confirmation; +is($availability->{availability_needs_confirmation}, 1, "Needs confirmation"); +$availability->set_unavailable; +is($availability->{available}, 0, "Not available"); + +$availability->add_description("such available"); +$availability->add_description("wow"); +$availability->add_description("wow"); + +ok($availability->has_description("wow"), "Found description 'wow'"); +ok($availability->has_description(["wow", "such available"]), + "Found description 'wow' and 'such available'"); +is($availability->has_description(["wow", "much not found"]), 0, + "Didn't find 'wow' and 'much not found'"); +is($availability->{description}[0], "such available", + "Found correct description in correct index 1/4"); +is($availability->{description}[1], "wow", + "Found correct description in correct index 2/2"); + +$availability->add_description(["much description", "very doge"]); +is($availability->{description}[2], "much description", + "Found correct description in correct index 3/4"); +is($availability->{description}[3], "very doge", + "Found correct description in correct index 4/4"); + +$availability->del_description("wow"); +is($availability->{description}[1], "much description", + "Found description from correct index after del"); +$availability->del_description(["very doge", "such available"]); +is($availability->{description}[0], "much description", + "Found description from correct index after del"); + + +my $availability_clone = $availability; +$availability->set_unavailable; +is($availability_clone->{available}, $availability->{available}, + "Availability_clone points to availability"); +$availability_clone = $availability->clone; +$availability->set_available; +isnt($availability_clone->{available}, $availability->{available}, + "Availability_clone was cloned and no longer has same availability status"); + +$availability->reset; +is($availability->{available}, undef, "Availability reset"); diff --git a/t/db_dependent/Items.t b/t/db_dependent/Items.t index 3e886e2..0bb5215 100755 --- a/t/db_dependent/Items.t +++ b/t/db_dependent/Items.t @@ -20,7 +20,12 @@ use Modern::Perl; use MARC::Record; use C4::Biblio; +use C4::Circulation; +use C4::Reserves; use Koha::Database; +use Koha::Hold; +use Koha::Issue; +use Koha::Item::Availability; use Koha::Library; use t::lib::Mocks; @@ -432,7 +437,7 @@ subtest 'SearchItems test' => sub { subtest 'Koha::Item(s) tests' => sub { - plan tests => 5; + plan tests => 40; $schema->storage->txn_begin(); @@ -443,6 +448,15 @@ subtest 'Koha::Item(s) tests' => sub { my $library2 = $builder->build({ source => 'Branch', }); + my $borrower = $builder->build({ + source => 'Borrower', + }); + my $itemtype = $builder->build({ + source => 'Itemtype', + value => { + notforloan => 1 + } + }); # Create a biblio and item for testing t::lib::Mocks::mock_preference('marcflavour', 'MARC21'); @@ -461,6 +475,101 @@ subtest 'Koha::Item(s) tests' => sub { is( ref($holdingbranch), 'Koha::Library', "Got Koha::Library from holding_branch method" ); is( $holdingbranch->branchcode(), $library2->{branchcode}, "Home branch code matches holdingbranch" ); + # Availability tests + my $availability = $item->availability_for_checkout(); + is (ref($availability), 'Koha::Item::Availability', 'Got Koha::Item::Availability'); + is( $availability->{available}, 1, "Item is available" ); + $availability = $item->availability_for_local_use(); + is( $availability->{available}, 1, "Item is available for local use" ); + t::lib::Mocks::mock_preference('OnSiteCheckouts', 0); + $availability = $item->availability_for_onsite_checkout(); + is( $availability->{available}, 0, "Not available for on-site checkouts" ); + is( $availability->{description}[0], "onsite_checkouts_disabled", "Availability description is 'onsite_checkouts_disabled'" ); + t::lib::Mocks::mock_preference('OnSiteCheckouts', 1); + $availability = $item->availability_for_onsite_checkout(); + is( $availability->{available}, 1, "Available for on-site checkouts" ); + + $item->set({ onloan => "", damaged => 1 })->store(); + t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Damaged item unavailable" ); + is( $availability->{description}[0], "damaged", "Availability description is 'damaged'" ); + $availability = $item->availability_for_local_use(); + is( $availability->{available}, 0, "Item is not available for local use" ); + $availability = $item->availability_for_onsite_checkout(); + is( $availability->{available}, 0, "Item is not available for on-site checkouts" ); + t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 1); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 1, "Damaged item available" ); + is( $availability->{description}[0], "damaged", "Availability description is 'damaged'" ); + $availability = $item->availability_for_local_use(); + is( $availability->{available}, 1, "Item is available for local use" ); + $availability = $item->availability_for_onsite_checkout(); + is( $availability->{available}, 1, "Item is available for on-site checkouts" ); + + $item->set({ damaged => 0, withdrawn => 1 })->store(); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{description}[0], "withdrawn", "Availability description is 'withdrawn'" ); + + $item->set({ withdrawn => 0, itemlost => 1 })->store(); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{description}[0], "itemlost", "Availability description is 'itemlost'" ); + + $item->set({ itemlost => 0, restricted => 1 })->store(); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{description}[0], "restricted", "Availability description is 'restricted'" ); + + $item->set({ restricted => 0, notforloan => 1 })->store(); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{description}[0], "notforloan", "Availability description is 'notforloan'" ); + $availability = $item->availability_for_local_use(); + is( $availability->{available}, 1, "Item is available for local use" ); + $availability = $item->availability_for_onsite_checkout(); + is( $availability->{available}, C4::Context->preference('OnSiteCheckouts'), "Good availability for on-site checkouts" ); + + $item->set({ notforloan => 0, itype => $itemtype->{itemtype} })->store(); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{description}[0], "notforloan", "Availability description is 'notforloan' (itemtype)" ); + $availability = $item->availability_for_local_use(); + is( $availability->{available}, 1, "Item is available for local use" ); + $availability = $item->availability_for_onsite_checkout(); + is( $availability->{available}, C4::Context->preference('OnSiteCheckouts'), "Good availability for on-site checkouts" ); + + $item->set({ itype => undef, barcode => "test" })->store(); + my $reserve = Koha::Hold->new( + { + biblionumber => $item->biblionumber, + itemnumber => $item->itemnumber, + waitingdate => '2000-01-01', + borrowernumber => $borrower->{borrowernumber}, + branchcode => $item->homebranch, + suspend => 0, + } + )->store(); + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{description}[0], "reserved", "Availability description is 'reserved'" ); + $availability = $item->availability_for_reserve(); + is( $availability->{available}, 1, "Item is available for reserve" ); + CancelReserve({ reserve_id => $reserve->reserve_id }); + + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 1, "Item is available" ); + + my $module = new Test::MockModule('C4::Context'); + $module->mock( 'userenv', sub { { branch => $borrower->{branchcode} } } ); + my $issue = AddIssue($borrower, $item->barcode, undef, 1); + $item = Koha::Items->find($item->itemnumber); # refresh item + $availability = $item->availability_for_checkout(); + is( $availability->{available}, 0, "Item is not available" ); + is( $availability->{description}[0], "onloan", "Availability description is 'onloan'" ); + is( $availability->{expected_available}, $issue->date_due, "Expected to be available '".$issue->date_due."'"); + $schema->storage->txn_rollback; }; diff --git a/t/db_dependent/api/v1/availability.t b/t/db_dependent/api/v1/availability.t new file mode 100644 index 0000000..b3d18bf --- /dev/null +++ b/t/db_dependent/api/v1/availability.t @@ -0,0 +1,289 @@ +#!/usr/bin/env perl + +# Copyright KohaSuomi 2016 +# +# This file is part of Koha. +# +# Koha 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 3 of the License, or (at your option) any later +# version. +# +# Koha is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with Koha; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +use Modern::Perl; + +use Test::More tests => 165; +use Test::Mojo; +use t::lib::Mocks; +use t::lib::TestBuilder; + +use Mojo::JSON; + +use C4::Auth; +use C4::Circulation; +use C4::Context; + +use Koha::Database; +use Koha::Items; +use Koha::Patron; + +my $builder = t::lib::TestBuilder->new(); + +my $dbh = C4::Context->dbh; +$dbh->{AutoCommit} = 0; +$dbh->{RaiseError} = 1; + +$ENV{REMOTE_ADDR} = '127.0.0.1'; +my $t = Test::Mojo->new('Koha::REST::V1'); + +my $categorycode = $builder->build({ source => 'Category' })->{ categorycode }; +my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode }; + +my $borrower = $builder->build({ source => 'Borrower' }); +my $biblio = $builder->build({ source => 'Biblio' }); +my $biblio2 = $builder->build({ source => 'Biblio' }); +my $biblionumber = $biblio->{biblionumber}; +my $biblionumber2 = $biblio2->{biblionumber}; + +my $module = new Test::MockModule('C4::Context'); +$module->mock( 'userenv', sub { { branch => $borrower->{branchcode} } } ); + +# $item = available, $item2 = unavailable +my $items; +$items->{available} = build_item($biblionumber); +$items->{notforloan} = build_item($biblionumber2, { notforloan => 1 }); +$items->{damaged} = build_item($biblionumber2, { damaged => 1 }); +$items->{withdrawn} = build_item($biblionumber2, { withdrawn => 1 }); +$items->{onloan} = build_item($biblionumber2, { onloan => undef }); +$items->{itemlost} = build_item($biblionumber2, { itemlost => 1 }); +$items->{reserved} = build_item($biblionumber2); +my $reserve = Koha::Hold->new( + { + biblionumber => $items->{reserved}->{biblionumber}, + itemnumber => $items->{reserved}->{itemnumber}, + waitingdate => '2000-01-01', + borrowernumber => $borrower->{borrowernumber}, + branchcode => $items->{reserved}->{homebranch}, + suspend => 0, + } + )->store(); + +my $itemnumber = $items->{available}->{itemnumber}; + +$t->get_ok("/api/v1/availability/items?itemnumber=-500382") + ->status_is(404); + +$t->get_ok("/api/v1/availability/items?itemnumber=-500382+-500383") + ->status_is(404); + +$t->get_ok("/api/v1/availability/items?biblionumber=-500382") + ->status_is(404); + +$t->get_ok("/api/v1/availability/items?biblionumber=-500382+-500383") + ->status_is(404); + +t::lib::Mocks::mock_preference('OnSiteCheckouts', 0); +t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0); +# available item +$t->get_ok("/api/v1/availability/items?itemnumber=$itemnumber") + ->status_is(200) + ->json_is('/0/itemnumber', $itemnumber) + ->json_is('/0/biblionumber', $biblionumber) + ->json_is('/0/checkout/available', Mojo::JSON->true) + ->json_is('/0/checkout/description', []) + ->json_is('/0/hold/available', Mojo::JSON->true) + ->json_is('/0/hold/description', []) + ->json_is('/0/local_use/available', Mojo::JSON->true) + ->json_is('/0/local_use/description', []) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["onsite_checkouts_disabled"]) + ->json_is('/0/hold_queue_length', 0); +t::lib::Mocks::mock_preference('OnSiteCheckouts', 1); +$t->get_ok("/api/v1/availability/items?biblionumber=$biblionumber") + ->status_is(200) + ->json_is('/0/itemnumber', $itemnumber) + ->json_is('/0/biblionumber', $biblionumber) + ->json_is('/0/checkout/available', Mojo::JSON->true) + ->json_is('/0/checkout/description', []) + ->json_is('/0/hold/available', Mojo::JSON->true) + ->json_is('/0/hold/description', []) + ->json_is('/0/local_use/available', Mojo::JSON->true) + ->json_is('/0/local_use/description', []) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->true) + ->json_is('/0/onsite_checkout/description', []) + ->json_is('/0/hold_queue_length', 0); + +# notforloan item +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{notforloan}->{itemnumber}) + ->status_is(200) + ->json_is('/0/itemnumber', $items->{notforloan}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description/0', "notforloan") + ->json_is('/0/hold/available', Mojo::JSON->false) + ->json_is('/0/hold/description', ["notforloan"]) + ->json_is('/0/local_use/available', Mojo::JSON->true) + ->json_is('/0/local_use/description', []) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->true) + ->json_is('/0/onsite_checkout/description', []) + ->json_is('/0/hold_queue_length', 0); +t::lib::Mocks::mock_preference('OnSiteCheckouts', 0); +$t->get_ok("/api/v1/availability/items?itemnumber=$items->{notforloan}->{itemnumber}") + ->status_is(200) + ->json_is('/0/itemnumber', $items->{notforloan}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description', ["notforloan"]) + ->json_is('/0/hold/available', Mojo::JSON->false) + ->json_is('/0/hold/description', ["notforloan"]) + ->json_is('/0/local_use/available', Mojo::JSON->true) + ->json_is('/0/local_use/description', []) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["onsite_checkouts_disabled"]) + ->json_is('/0/hold_queue_length', 0); +t::lib::Mocks::mock_preference('OnSiteCheckouts', 1); + +# damaged item +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{damaged}->{itemnumber}) + ->status_is(200) + ->json_is('/0/itemnumber', $items->{damaged}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description', ["damaged"]) + ->json_is('/0/hold/available', Mojo::JSON->false) + ->json_is('/0/hold/description', ["damaged"]) + ->json_is('/0/local_use/available', Mojo::JSON->false) + ->json_is('/0/local_use/description', ["damaged"]) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["damaged"]) + ->json_is('/0/hold_queue_length', 0); +t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 1); +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{damaged}->{itemnumber}) + ->status_is(200) + ->json_is('/0/itemnumber', $items->{damaged}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/checkout/available', Mojo::JSON->true) + ->json_is('/0/checkout/description', ["damaged"]) + ->json_is('/0/hold/available', Mojo::JSON->true) + ->json_is('/0/hold/description', ["damaged"]) + ->json_is('/0/local_use/available', Mojo::JSON->true) + ->json_is('/0/local_use/description', ["damaged"]) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->true) + ->json_is('/0/onsite_checkout/description', ["damaged"]) + ->json_is('/0/hold_queue_length', 0); + +# withdrawn item +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{withdrawn}->{itemnumber}) + ->status_is(200) + ->json_is('/0/itemnumber', $items->{withdrawn}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description', ["withdrawn"]) + ->json_is('/0/hold/available', Mojo::JSON->false) + ->json_is('/0/hold/description', ["withdrawn"]) + ->json_is('/0/local_use/available', Mojo::JSON->false) + ->json_is('/0/local_use/description', ["withdrawn"]) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["withdrawn"]) + ->json_is('/0/hold_queue_length', 0); + +# lost item +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{itemlost}->{itemnumber}) + ->status_is(200) + ->json_is('/0/itemnumber', $items->{itemlost}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description', ["itemlost"]) + ->json_is('/0/hold/available', Mojo::JSON->false) + ->json_is('/0/hold/description', ["itemlost"]) + ->json_is('/0/local_use/available', Mojo::JSON->false) + ->json_is('/0/local_use/description', ["itemlost"]) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["itemlost"]) + ->json_is('/0/hold_queue_length', 0); + +my $issue = AddIssue($borrower, $items->{onloan}->{barcode}, undef, 1); + +# issued item +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{onloan}->{itemnumber}) + ->status_is(200) + ->json_is('/0/itemnumber', $items->{onloan}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description', ["onloan"]) + ->json_is('/0/hold/available', Mojo::JSON->true) + ->json_is('/0/hold/description', []) + ->json_is('/0/local_use/available', Mojo::JSON->false) + ->json_is('/0/local_use/description', ["onloan"]) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["onloan"]) + ->json_is('/0/checkout/expected_available', $issue->date_due) + ->json_is('/0/local_use/expected_available', $issue->date_due) + ->json_is('/0/onsite_checkout/expected_available', $issue->date_due) + ->json_is('/0/hold_queue_length', 0); + +# reserved item +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{reserved}->{itemnumber}) + ->status_is(200) + ->json_is('/0/itemnumber', $items->{reserved}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description', ["reserved"]) + ->json_is('/0/hold/available', Mojo::JSON->true) + ->json_is('/0/hold/description', []) + ->json_is('/0/local_use/available', Mojo::JSON->false) + ->json_is('/0/local_use/description', ["reserved"]) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->false) + ->json_is('/0/onsite_checkout/description', ["reserved"]) + ->json_is('/0/hold_queue_length', 1); + +# multiple in one request +$t->get_ok("/api/v1/availability/items?itemnumber=".$items->{notforloan}->{itemnumber}."+$itemnumber+-500382") + ->status_is(200) + ->json_is('/0/itemnumber', $items->{notforloan}->{itemnumber}) + ->json_is('/0/biblionumber', $biblionumber2) + ->json_is('/0/checkout/available', Mojo::JSON->false) + ->json_is('/0/checkout/description/0', "notforloan") + ->json_is('/0/hold/available', Mojo::JSON->false) + ->json_is('/0/hold/description', ["notforloan"]) + ->json_is('/0/local_use/available', Mojo::JSON->true) + ->json_is('/0/local_use/description', []) + ->json_is('/0/onsite_checkout/available', Mojo::JSON->true) + ->json_is('/0/onsite_checkout/description', []) + ->json_is('/0/hold_queue_length', 0) + ->json_is('/1/itemnumber', $itemnumber) + ->json_is('/1/biblionumber', $biblionumber) + ->json_is('/1/checkout/available', Mojo::JSON->true) + ->json_is('/1/checkout/description', []) + ->json_is('/1/hold/available', Mojo::JSON->true) + ->json_is('/1/hold/description', []) + ->json_is('/1/local_use/available', Mojo::JSON->true) + ->json_is('/1/local_use/description', []) + ->json_is('/1/onsite_checkout/available', Mojo::JSON->true) + ->json_is('/1/onsite_checkout/description', []) + ->json_is('/1/hold_queue_length', 0); + +sub build_item { + my ($biblionumber, $field) = @_; + + return $builder->build({ + source => 'Item', + value => { + biblionumber => $biblionumber, + notforloan => $field->{notforloan} || 0, + damaged => $field->{damaged} || 0, + withdrawn => $field->{withdrawn} || 0, + itemlost => $field->{itemlost} || 0, + restricted => $field->{restricted} || undef, + onloan => $field->{onloan} || undef, + itype => $field->{itype} || undef, + } + }); +} -- 1.9.1