From 8a6989973a20fa17b79193b86b33dd4fcc330220 Mon Sep 17 00:00:00 2001 From: Lari Taskula Date: Mon, 28 Nov 2016 18:16:53 +0200 Subject: [PATCH] Bug 17712: Centralize availability-related checks There are some problems to our current approach for "availability" which makes it complicated to integrate with e.g. REST API. Firstly, there has been no unified way of representing reasons for unavailability. Previously, each implementation of availability calculation has chosen its own way for describing the reasons. For example, CanItemBeReserved string "ageRestricted" vs. CanBookBeIssued key "AGE_RESTRICTION" in a HASHref. Secondly, some of the availability logic is contained outside centralized methods like CanItemBeReserved which is missing checks e.g. for maxreserves (checked instead in opac-reserve.pl) and patron fines. Perhaps this issue could be fixed in another Bug, but I propose we squash it at the same time with this Bug. Currently, because of this, we will not get reliable responses to holdability from CanItemBeReserved alone. This causes problems for API integration where we need to be able to describe the reason in an uniform way and additionally provide information on what needs to be done to fix the issue. Also, we do not want to duplicate all the external checks from .pl files into our REST controllers. Instead of modifying the old methods, I propose a new structure for the whole availability-problem and let us deal with proper "status-codes" to easily integrate availability & additional availability-related information into e.g. REST API. My proposal is an approach to centralize the availability-related checks and have them return uniform reasons to describe reasons for availability problems. Ultimately, we could ask availability something like this: my $holdability = Koha::Availability::Hold->biblio({ biblio => $biblio, patron => $patron, to_branch => 'CPL', })->in_opac; ...and $holdability->unavailabilities HASHref would contain Koha::Exceptions possibily with additional parameters to let us know why this biblio is not holdable in OPAC. This patch adds all availability related logic with centralization in mind. From these individual methods we are able to construct full availability queries in order to determine if something is actually available in some way and also describe the problems with the help of Koha::Exceptions. Since availability is a mixture of multiple different smaller checks from multiple categories like item, patron, issuing rules, etc, they can be categorized into their own subclasses. This lets us centralize availability-related methods per category into their own modules. To test: 1. prove t/db_dependent/Koha/Availability/Checks/* --- Koha/Availability/Checks/Biblio.pm | 144 ++++++ Koha/Availability/Checks/Biblioitem.pm | 86 ++++ Koha/Availability/Checks/Checkout.pm | 108 +++++ Koha/Availability/Checks/IssuingRule.pm | 411 +++++++++++++++++ Koha/Availability/Checks/Item.pm | 457 ++++++++++++++++++ Koha/Availability/Checks/LibraryItemRule.pm | 144 ++++++ Koha/Availability/Checks/Patron.pm | 323 +++++++++++++ Koha/Schema/Result/Reserve.pm | 22 + t/db_dependent/Koha/Availability/Checks/Biblio.t | 165 +++++++ .../Koha/Availability/Checks/Biblioitem.t | 83 ++++ t/db_dependent/Koha/Availability/Checks/Checkout.t | 113 +++++ .../Koha/Availability/Checks/IssuingRule.t | 513 +++++++++++++++++++++ t/db_dependent/Koha/Availability/Checks/Item.t | 382 +++++++++++++++ .../Koha/Availability/Checks/LibraryItemRule.t | 312 +++++++++++++ t/db_dependent/Koha/Availability/Checks/Patron.t | 452 ++++++++++++++++++ t/db_dependent/Koha/Availability/Helpers.pm | 165 +++++++ 16 files changed, 3880 insertions(+) create mode 100644 Koha/Availability/Checks/Biblio.pm create mode 100644 Koha/Availability/Checks/Biblioitem.pm create mode 100644 Koha/Availability/Checks/Checkout.pm create mode 100644 Koha/Availability/Checks/IssuingRule.pm create mode 100644 Koha/Availability/Checks/Item.pm create mode 100644 Koha/Availability/Checks/LibraryItemRule.pm create mode 100644 Koha/Availability/Checks/Patron.pm create mode 100644 t/db_dependent/Koha/Availability/Checks/Biblio.t create mode 100644 t/db_dependent/Koha/Availability/Checks/Biblioitem.t create mode 100644 t/db_dependent/Koha/Availability/Checks/Checkout.t create mode 100644 t/db_dependent/Koha/Availability/Checks/IssuingRule.t create mode 100644 t/db_dependent/Koha/Availability/Checks/Item.t create mode 100644 t/db_dependent/Koha/Availability/Checks/LibraryItemRule.t create mode 100644 t/db_dependent/Koha/Availability/Checks/Patron.t create mode 100644 t/db_dependent/Koha/Availability/Helpers.pm diff --git a/Koha/Availability/Checks/Biblio.pm b/Koha/Availability/Checks/Biblio.pm new file mode 100644 index 0000000..6294d11 --- /dev/null +++ b/Koha/Availability/Checks/Biblio.pm @@ -0,0 +1,144 @@ +package Koha::Availability::Checks::Biblio; + +# Copyright Koha-Suomi Oy 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 base qw(Koha::Availability::Checks); + +use C4::Context; +use C4::Circulation; +use C4::Serials; + +use Koha::Biblios; +use Koha::Patrons; + +use Koha::Exceptions::Biblio; + +sub new { + my ($class, $biblio) = @_; + + unless ($biblio) { + Koha::Exceptions::MissingParameter->throw( + error => 'Class must be instantiated by providing a Koha::Biblio object.' + ); + } + unless (ref($biblio) eq 'Koha::Biblio') { + Koha::Exceptions::BadParameter->throw( + error => 'Biblio must be a Koha::Biblio object.' + ); + } + + my $self = { + biblio => $biblio, + }; + + bless $self, $class; +} + +=head3 another_item_checked_out + +Returns Koha::Exceptions::Biblio::AnotherItemCheckedOut if another item from +the same biblio is checked out. + +=cut + +sub another_item_checked_out { + my ($self, $patron) = @_; + + unless ($patron) { + Koha::Exceptions::MissingParameter->throw( + error => 'Patron related holdability checks require a patron. Not given.' + ); + } + unless (ref($patron) eq 'Koha::Patron') { + Koha::Exceptions::BadParameter->throw( + error => 'Patron must be a Koha::Patron object.' + ); + } + + my $issued = C4::Circulation::CheckIfIssuedToPatron( + $patron->borrowernumber, + $self->biblio->biblionumber, + ); + if ($issued) { + return Koha::Exceptions::Biblio::AnotherItemCheckedOut->new; + } + return; +} + +=head3 checked_out + +Returns Koha::Exceptions::Biblio::CheckedOut if biblio is checked out. + +=cut + +sub checked_out { + my ($self, $patron) = @_; + + my $biblio = $self->biblio; + my $issues = C4::Circulation::GetIssues({ + borrowernumber => $patron->borrowernumber, + biblionumber => $biblio->biblionumber, + }); + my @issues = $issues ? @$issues : (); + if (scalar @issues > 0) { + return Koha::Exceptions::Biblio::CheckedOut->new( + biblionumber => $biblio->biblionumber, + ); + } + return; +} + +=head3 forbid_holds_on_patrons_possessions + +Returns Koha::Exceptions::Biblio::AnotherItemCheckedOut if system preference +AllowHoldsOnPatronsPossessions is disabled and another item from the same biblio +is checked out. + +=cut + +sub forbid_holds_on_patrons_possessions { + my ($self, $patron) = @_; + + if (!C4::Context->preference('AllowHoldsOnPatronsPossessions')) { + return $self->another_item_checked_out($patron); + } + return; +} + +=head3 forbid_multiple_issues + +Returns Koha::Exceptions::Biblio::CheckedOut if system preference +AllowMultipleIssuesOnABiblio is disabled and an item from biblio is checked out. + +=cut + +sub forbid_multiple_issues { + my ($self, $patron) = @_; + + if (!C4::Context->preference('AllowMultipleIssuesOnABiblio')) { + my $biblionumber = $self->biblio->biblionumber; + unless (C4::Serials::CountSubscriptionFromBiblionumber($biblionumber)) { + return $self->checked_out($patron); + } + } + return; +} + +1; diff --git a/Koha/Availability/Checks/Biblioitem.pm b/Koha/Availability/Checks/Biblioitem.pm new file mode 100644 index 0000000..603e1aa --- /dev/null +++ b/Koha/Availability/Checks/Biblioitem.pm @@ -0,0 +1,86 @@ +package Koha::Availability::Checks::Biblioitem; + +# Copyright Koha-Suomi Oy 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 base qw(Koha::Availability::Checks); + +use C4::Context; +use C4::Members; + +use Koha::Biblioitems; + +use Koha::Exceptions::Patron; + +sub new { + my ($class, $biblioitem) = @_; + + unless ($biblioitem) { + Koha::Exceptions::MissingParameter->throw( + error => 'Biblioitem related holdability checks require a biblioitem. Not given.' + ); + } + unless (ref($biblioitem) eq 'Koha::Biblioitem') { + Koha::Exceptions::BadParameter->throw( + error => 'Biblioitem must be a Koha::Biblioitem object.' + ); + } + + my $self = { + biblioitem => $biblioitem, + }; + + bless $self, $class; +} + +=head3 age_restricted + +Returns Koha::Exceptions::Patron::AgeRestricted if biblioitem age restriction +applies for patron. + +=cut + +sub age_restricted { + my ($self, $patron) = @_; + + unless ($patron) { + Koha::Exceptions::MissingParameter->throw( + error => 'Patron related holdability checks require a patron. Not given.' + ); + } + unless (ref($patron) eq 'Koha::Patron') { + Koha::Exceptions::BadParameter->throw( + error => 'Patron must be a Koha::Patron object.' + ); + } + + my $biblioitem = $self->biblioitem; + my $agerestriction = $biblioitem->agerestriction; + my ($restriction_age, $daysToAgeRestriction) = + C4::Circulation::GetAgeRestriction($agerestriction, $patron->unblessed); + my $restricted = $daysToAgeRestriction && $daysToAgeRestriction > 0 ? 1:0; + if ($restricted) { + return Koha::Exceptions::Patron::AgeRestricted->new( + age_restriction => $agerestriction, + ); + } + return; +} + +1; diff --git a/Koha/Availability/Checks/Checkout.pm b/Koha/Availability/Checks/Checkout.pm new file mode 100644 index 0000000..1fb431c --- /dev/null +++ b/Koha/Availability/Checks/Checkout.pm @@ -0,0 +1,108 @@ +package Koha::Availability::Checks::Checkout; + +# Copyright Koha-Suomi Oy 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 base qw(Koha::Availability::Checks); + +use C4::Circulation; +use C4::Context; + +use Koha::DateUtils; +use Koha::Items; + +use Koha::Exceptions::Biblio; +use Koha::Exceptions::Checkout; + +sub new { + my ($class) = @_; + + my $self = {}; + + bless $self, $class; +} + +=head3 invalid_due_date + +Returns Koha::Exceptions::Checkout::InvalidDueDate if given due date is invalid. + +Returns Koha::Exceptions::Checkout::DueDateBeforeNow if given due date is in the +past. + +=cut + +sub invalid_due_date { + my ($self, $item, $patron, $duedate) = @_; + + if ($duedate && ref $duedate ne 'DateTime') { + eval { $duedate = dt_from_string($duedate); }; + if ($@) { + return Koha::Exceptions::Checkout::InvalidDueDate->new( + duedate => $duedate, + ); + } + } + + my $now = DateTime->now(time_zone => C4::Context->tz()); + unless ($duedate) { + my $issuedate = $now->clone(); + + my $branch = C4::Circulation::_GetCircControlBranch($item, $patron); + $duedate = C4::Circulation::CalcDateDue + ( + $issuedate, $item->effective_itemtype, $branch, $patron->unblessed + ); + } + if ($duedate) { + my $today = $now->clone->truncate(to => 'minute'); + if (DateTime->compare($duedate,$today) == -1 ) { + # duedate cannot be before now + return Koha::Exceptions::Checkout::DueDateBeforeNow->new( + duedate => output_pref($duedate), + now => output_pref($now), + ); + } + } else { + return Koha::Exceptions::Checkout::InvalidDueDate->new( + duedate => output_pref($duedate), + ); + } + return; +} + +=head3 no_more_renewals + +Returns Koha::Exceptions::Checkout::NoMoreRenewals if no more renewals are +allowed for given checkout. + +=cut + +sub no_more_renewals { + my ($self, $issue) = @_; + + return unless $issue; + my ($status) = C4::Circulation::CanBookBeRenewed($issue->borrowernumber, + $issue->itemnumber); + if ($status == 0) { + return Koha::Exceptions::Checkout::NoMoreRenewals->new; + } + return; +} + +1; diff --git a/Koha/Availability/Checks/IssuingRule.pm b/Koha/Availability/Checks/IssuingRule.pm new file mode 100644 index 0000000..98cecf9 --- /dev/null +++ b/Koha/Availability/Checks/IssuingRule.pm @@ -0,0 +1,411 @@ +package Koha::Availability::Checks::IssuingRule; + +# Copyright Koha-Suomi Oy 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 base qw(Koha::Availability::Checks); + +use C4::Circulation; +use C4::Context; +use C4::Reserves; + +use Koha::IssuingRules; +use Koha::Items; + +use Koha::Exceptions; +use Koha::Exceptions::Checkout; +use Koha::Exceptions::Hold; + +=head3 new + +OPTIONAL PARAMETERS: + +* categorycode Attempts to match issuing rule with given categorycode +* rule_itemtype Attempts to match issuing rule with given itemtype +* branchcode Attempts to match issuing rule with given branchcode + +* patron Stores patron into the object for reusability. Also + attempts to match issuing rule with item's itemtype + unless specificed with "rule_itemtype" parameter. +* item stores item into the object for reusability. Also + Attempts to match issuing rule with patron's categorycode + unless specified with "categorycode" parameter. +* biblioitem Attempts to match issuing rule with itemtype from given + biblioitem as a fallback + +Stores the effective issuing rule into class variable effective_issuing_rule +for reusability. This means the methods in this class are performed on the +stored effective issuing rule and multiple queries into issuingrules table +are avoided. + +Caches issuing rule momentarily to help performance in biblio availability +calculation. This is helpful because a biblio may have multiple items matching +the same issuing rule and this lets us avoid multiple, unneccessary queries into +the issuingrule table. + +=cut + +sub new { + my $class = shift; + my ($params) = @_; + + my $self = $class->SUPER::new(@_); + + my $categorycode = $params->{'categorycode'}; + my $rule_itemtype = $params->{'rule_itemtype'}; + my $branchcode = $params->{'branchcode'}; + + my $patron = $self->_validate_parameter($params, + 'patron', 'Koha::Patron'); + my $item = $self->_validate_parameter($params, + 'item', 'Koha::Item'); + my $biblioitem = $self->_validate_parameter($params, + 'biblioitem', 'Koha::Biblioitem'); + + unless ($rule_itemtype) { + $rule_itemtype = $item + ? $item->effective_itemtype + : $biblioitem + ? $biblioitem->itemtype + : undef; + } + unless ($categorycode) { + $categorycode = $patron ? $patron->categorycode : undef; + } + if ($params->{'use_cache'}) { + $self->{'use_cache'} = 1; + } + + # Get a matching issuing rule + my $rule; + my $cache; + if ($self->use_cache) { + $cache = Koha::Caches->get_instance('availability'); + my $cached = $cache->get_from_cache('issuingrule-.' + .($categorycode?$categorycode:'*').'-' + .($rule_itemtype?$rule_itemtype:'*').'-' + .($branchcode?$branchcode:'*')); + if ($cached) { + $rule = Koha::IssuingRule->new->set($cached); + } + } + + unless ($rule) { + $rule = Koha::IssuingRules->get_effective_issuing_rule({ + categorycode => $categorycode, + itemtype => $rule_itemtype, + branchcode => $branchcode, + }); + if ($rule && $self->use_cache) { + $cache->set_in_cache('issuingrule-.' + .($categorycode?$categorycode:'*').'-' + .($rule_itemtype?$rule_itemtype:'*').'-' + .($branchcode?$branchcode:'*'), + $rule->unblessed, { expiry => 10 }); + } + } + + # Store effective issuing rule into object + $self->{'effective_issuing_rule'} = $rule; + # Store patron into object + $self->{'patron'} = $patron; + # Store item into object + $self->{'item'} = $item; + + bless $self, $class; +} + +=head3 maximum_checkouts_reached + +Returns Koha::Exceptions::Checkout::MaximumCheckoutsReached if maximum number +of checkouts have been reached by patron. + +=cut + +sub maximum_checkouts_reached { + my ($self, $item, $patron) = @_; + + return unless $patron ||= $self->patron; + return unless $item ||= $self->item; + + my $item_unblessed = $item->unblessed; + $item_unblessed->{'itemtype'} = $item->effective_itemtype; + my $toomany = C4::Circulation::TooMany( + $patron->unblessed, + $item->biblionumber, + $item_unblessed, + ); + + if ($toomany) { + return Koha::Exceptions::Checkout::MaximumCheckoutsReached->new( + error => $toomany->{reason}, + max_checkouts_allowed => $toomany->{max_allowed}, + current_checkout_count => $toomany->{count}, + ); + } + return; +} + +=head3 maximum_holds_for_record_reached + +Returns Koha::Exceptions::Hold::MaximumHoldsForRecordReached if maximum number +of holds on biblio have been reached by patron. + +Returns Koha::Exceptions::Hold::ZeroHoldsAllowed if no holds are allowed at all. + +OPTIONAL PARAMETERS: +nonfound_holds Allows you to pass Koha::Holds object to improve performance + by avoiding another query to reserves table. + (Found holds don't count against a patron's holds limit) +biblionumber Allows you to specify biblionumber; if not given, item's + biblionumber will be used (recommended; but requires you to + provide item while instantiating this class). + +=cut + +sub maximum_holds_for_record_reached { + my ($self, $params) = @_; + + return unless my $rule = $self->effective_issuing_rule; + return unless my $item = $self->item; + + my $biblionumber = $params->{'biblionumber'} ? $params->{'biblionumber'} + : $item->biblionumber; + if ($rule->holds_per_record > 0 && $rule->reservesallowed > 0) { + my $holds_on_this_record; + unless (exists $params->{'nonfound_holds'}) { + $holds_on_this_record = Koha::Holds->search({ + borrowernumber => $self->patron->borrowernumber, + biblionumber => $biblionumber, + found => undef, + })->count; + } else { + $holds_on_this_record = @{$params->{'nonfound_holds'}}; + } + if ($holds_on_this_record >= $rule->holds_per_record) { + return Koha::Exceptions::Hold::MaximumHoldsForRecordReached->new( + max_holds_allowed => $rule->holds_per_record, + current_hold_count => $holds_on_this_record, + ); + } + } else { + return $self->zero_holds; + } + return; +} + +=head3 maximum_holds_reached + +Returns Koha::Exceptions::Hold::MaximumHoldsReached if maximum number +of holds have been reached by patron. + +Returns Koha::Exceptions::Hold::ZeroHoldsAllowed if no holds are allowed at all. + +=cut + +sub maximum_holds_reached { + my ($self) = @_; + + return unless my $rule = $self->effective_issuing_rule; + my $rule_itemtype = $rule->itemtype; + my $controlbranch = $self->controlbranch; + if ($rule->holds_per_record > 0 && $rule->reservesallowed > 0) { + # Get patron's hold count for holds that match the found issuing rule + my $hold_count = $self->_patron_hold_count($rule_itemtype, $controlbranch); + if ($hold_count >= $rule->reservesallowed) { + return Koha::Exceptions::Hold::MaximumHoldsReached->new( + max_holds_allowed => $rule->reservesallowed, + current_hold_count => $hold_count, + ); + } + } else { + return $self->zero_holds; + } + return; +} + +=head3 on_shelf_holds_forbidden + +Returns Koha::Exceptions::Hold::OnShelfNotAllowed if effective issuing rule +restricts on-shelf holds. + +=cut + +sub on_shelf_holds_forbidden { + my ($self) = @_; + + return unless my $rule = $self->effective_issuing_rule; + return unless my $item = $self->item; + my $on_shelf_holds = $rule->onshelfholds; + + if ($on_shelf_holds == 0) { + my $hold_waiting = Koha::Holds->search({ + found => 'W', + itemnumber => $item->itemnumber, + priority => 0 + })->count; + if (!$item->onloan && !$hold_waiting) { + return Koha::Exceptions::Hold::OnShelfNotAllowed->new; + } + return; + } elsif ($on_shelf_holds == 1) { + return; + } elsif ($on_shelf_holds == 2) { + my @items = Koha::Items->search({ biblionumber => $item->biblionumber }); + + my $any_available = 0; + + foreach my $i (@items) { + unless ($i->itemlost + || $i->notforloan > 0 + || $i->withdrawn + || $i->onloan + || C4::Reserves::IsItemOnHoldAndFound( $i->id ) + || ( $i->damaged + && !C4::Context->preference('AllowHoldsOnDamagedItems') ) + || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan) { + $any_available = 1; + last; + } + } + return Koha::Exceptions::Hold::OnShelfNotAllowed->new if $any_available; + } + return; +} + +=head3 opac_item_level_hold_forbidden + +Returns Koha::Exceptions::Hold::ItemLevelHoldNotAllowed if item-level holds are +forbidden in OPAC. + +=cut + +sub opac_item_level_hold_forbidden { + my ($self) = @_; + + return unless my $rule = $self->effective_issuing_rule; + if (defined $rule->opacitemholds && $rule->opacitemholds eq 'N') { + return Koha::Exceptions::Hold::ItemLevelHoldNotAllowed->new; + } + return; +} + +=head3 zero_checkouts_allowed + +Returns Koha::Exceptions::Checkout::ZeroCheckoutsAllowed if checkouts are not +allowed at all. + +=cut + +sub zero_checkouts_allowed { + my ($self) = @_; + + return unless my $rule = $self->effective_issuing_rule; + if (defined $rule->maxissueqty && $rule->maxissueqty == 0) { + return Koha::Exceptions::Checkout::ZeroCheckoutsAllowed->new; + } + return; +} + +=head3 zero_holds_allowed + +Returns Koha::Exceptions::Hold::ZeroHoldsAllowed if holds are not +allowed at all. + +This will inspect both "reservesallowed" and "holds_per_record" value in effective +issuing rule. + +=cut + +sub zero_holds_allowed { + my ($self) = @_; + + return unless my $rule = $self->effective_issuing_rule; + if (defined $rule->reservesallowed && $rule->reservesallowed == 0 + || defined $rule->holds_per_record && $rule->holds_per_record == 0) { + return Koha::Exceptions::Hold::ZeroHoldsAllowed->new; + } + return; +} + +sub _patron_hold_count { + my ($self, $itemtype, $controlbranch) = @_; + + $itemtype ||= '*'; + my $branchcode; + my $branchfield = 'me.branchcode'; + $controlbranch ||= C4::Context->preference('ReservesControlBranch'); + my $patron = $self->patron; + if ($self->patron && $controlbranch eq 'PatronLibrary') { + $branchfield = 'borrower.branchcode'; + $branchcode = $patron->branchcode; + } elsif ($self->item && $controlbranch eq 'ItemHomeLibrary') { + $branchfield = 'item.homebranch'; + $branchcode = $self->item->homebranch; + } + + my $cache; + if ($self->use_cache) { + $cache = Koha::Caches->get_instance('availability'); + my $cached = $cache->get_from_cache('holds_of_'.$patron->borrowernumber.'-' + .$itemtype.'-'.$branchcode); + if (defined $cached) { + return $cached; + } + } + + my $holds = Koha::Holds->search({ + 'me.borrowernumber' => $patron->borrowernumber, + $branchfield => $branchcode, + '-and' => [ + '-or' => [ + $itemtype ne '*' && C4::Context->preference('item-level_itypes') == 1 ? [ + { 'item.itype' => $itemtype }, + { 'biblioitem.itemtype' => $itemtype } + ] : [ { 'biblioitem.itemtype' => $itemtype } ] + ] + ]}, { + join => ['borrower', 'biblioitem', 'item'], + '+select' => [ 'borrower.branchcode', 'item.homebranch' ], + '+as' => ['borrower.branchcode', 'item.homebranch' ] + })->count; + + if ($self->use_cache) { + $cache->set_in_cache('holds_of_'.$patron->borrowernumber.'-' + .$itemtype.'-'.$branchcode, $holds, { expiry => 10 }); + } + + return $holds; +} + +sub _validate_parameter { + my ($self, $params, $key, $ref) = @_; + + if (exists $params->{$key}) { + if (ref($params->{$key}) eq $ref) { + return $params->{$key}; + } else { + Koha::Exceptions::BadParameter->throw( + "Parameter $key must be a $ref object." + ); + } + } +} + +1; diff --git a/Koha/Availability/Checks/Item.pm b/Koha/Availability/Checks/Item.pm new file mode 100644 index 0000000..f8820ae --- /dev/null +++ b/Koha/Availability/Checks/Item.pm @@ -0,0 +1,457 @@ +package Koha::Availability::Checks::Item; + +# Copyright Koha-Suomi Oy 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 base qw(Koha::Availability::Checks); + +use C4::Circulation; +use C4::Context; +use C4::Reserves; + +use Koha::AuthorisedValues; +use Koha::DateUtils; +use Koha::ItemTypes; +use Koha::Items; +use Koha::Item::Transfers; + +use Koha::Exceptions::Item; +use Koha::Exceptions::ItemType; + +sub new { + my ($class, $item) = @_; + + unless ($item) { + Koha::Exceptions::MissingParameter->throw( + error => 'Class must be instantiated by providing a Koha::Item object.' + ); + } + unless (ref($item) eq 'Koha::Item') { + Koha::Exceptions::BadParameter->throw( + error => 'Item must be a Koha::Item object.' + ); + } + + my $self = { + item => $item, + }; + + bless $self, $class; +} + +=head3 checked_out + +Returns Koha::Exceptions::Item::CheckedOut if item is checked out. + +=cut + +sub checked_out { + my ($self, $issue) = @_; + + $issue ||= C4::Circulation::GetItemIssue($self->item->itemnumber); + if (ref($issue) eq 'Koha::Checkout') { + $issue = $issue->unblessed; + } + if ($issue) { + return Koha::Exceptions::Item::CheckedOut->new( + borrowernumber => $issue->{borrowernumber}, + date_due => $issue->{date_due}, + ); + } + return; +} + +=head3 checked_out + +Returns Koha::Exceptions::Checkout::Fee if checking out an item will cause +a checkout fee. + +Koha::Exceptions::Checkout::Fee additional fields: + amount # defines the amount of checkout fee + +=cut + +sub checkout_fee { + my ($self, $patron) = @_; + + my ($rentalCharge) = C4::Circulation::GetIssuingCharges + ( + $self->item->itemnumber, + $patron ? $patron->borrowernumber : undef + ); + if ($rentalCharge > 0){ + return Koha::Exceptions::Checkout::Fee->new( + amount => sprintf("%.02f", $rentalCharge), + ); + } + return; +} + +=head3 damaged + +Returns Koha::Exceptions::Item::Damaged if item is damaged and holds are not +allowed on damaged items. + +=cut + +sub damaged { + my ($self) = @_; + + if ($self->item->damaged + && !C4::Context->preference('AllowHoldsOnDamagedItems')) { + return Koha::Exceptions::Item::Damaged->new; + } + return; +} + +=head3 from_another_library + +Returns Koha::Exceptions::Item::FromAnotherLibrary if IndependentBranches is on, +and item is from another branch than the user currently logged in. + +Koha::Exceptions::Item::FromAnotherLibrary additional fields: + from_library # item's library (according to HomeOrHoldingBranch) + current_library # the library of logged-in user + +=cut + +sub from_another_library { + my ($self) = @_; + + my $item = $self->item; + if (C4::Context->preference("IndependentBranches")) { + return unless my $userenv = C4::Context->userenv; + unless (C4::Context->IsSuperLibrarian()) { + my $homeorholding = C4::Context->preference("HomeOrHoldingBranch"); + if ($userenv->{branch} && $item->$homeorholding ne $userenv->{branch}){ + return Koha::Exceptions::Item::FromAnotherLibrary->new( + from_library => $item->$homeorholding, + current_library => $userenv->{branch}, + ); + } + } + } + return; +} + +=head3 held + +Returns Koha::Exceptions::Item::Held item is held. + +Koha::Exceptions::Item::Held additional fields: + borrowernumber # item's library (according to HomeOrHoldingBranch) + status # the library of logged-in user + hold_queue_length # hold queue length for the item + +=cut + +sub held { + my ($self) = @_; + + my $item = $self->item; + if (my ($s, $reserve) = C4::Reserves::CheckReserves($item->itemnumber)) { + if ($reserve) { + return Koha::Exceptions::Item::Held->new( + borrowernumber => $reserve->{'borrowernumber'}, + status => $s, + hold_queue_length => $item->hold_queue_length, + ); + } + } + return; +} + +=head3 held_by_patron + +Returns Koha::Exceptions::Item::AlreadyHeldForThisPatron if item is already +held by given patron. + +OPTIONAL PARAMETERS +holds # list of Koha::Hold objects to inspect the item's held-status from. + # If not given, a query is made for selecting the holds from database. + # Useful in optimizing biblio-level availability by selecting all holds + # of a biblio and then passing it for this function instead of querying + # reserves table multiple times for each item. + +=cut + +sub held_by_patron { + my ($self, $patron, $params) = @_; + + my $item = $self->item; + my $holds; + if (!exists $params->{'holds'}) { + $holds = Koha::Holds->search({ + borrowernumber => $patron->borrowernumber, + itemnumber => $item->itemnumber, + })->count(); + } else { + foreach my $hold (@{$params->{'holds'}}) { + if ($hold->itemnumber == $item->itemnumber) { + $holds++; + } + } + } + if ($holds) { + return Koha::Exceptions::Item::AlreadyHeldForThisPatron->new + } + return; +} + +=head3 high_hold + +Returns Koha::Exceptions::Item::HighHolds if item is a high-held item and +decreaseLoanHighHolds is enabled. + +=cut + +sub high_hold { + my ($self, $patron) = @_; + + return unless C4::Context->preference('decreaseLoanHighHolds'); + + my $item = $self->item; + my $check = C4::Circulation::checkHighHolds( + $item->unblessed, + $patron->unblessed + ); + + if ($check->{exceeded}) { + return Koha::Exceptions::Item::HighHolds->new( + num_holds => $check->{outstanding}, + duration => $check->{duration}, + returndate => output_pref({ + dt => $check->{due_date}, + dateformat => 'iso', + dateonly => 1 + }), + ); + } + return; +} + +=head3 lost + +Returns Koha::Exceptions::Item::Lost if item is lost. + +=cut + +sub lost { + my ($self) = @_; + + my $item = $self->item; + if ($self->item->itemlost) { + my $av = Koha::AuthorisedValues->search({ + category => 'LOST', + authorised_value => $item->itemlost + }); + my $code = $av->count ? $av->next->lib : ''; + return Koha::Exceptions::Item::Lost->new( + status => $item->itemlost, + code => $code, + ); + } + return; +} + +=head3 notforloan + +Returns Koha::Exceptions::Item::NotForLoan if item is not for loan, and +additionally Koha::Exceptions::ItemType::NotForLoan if itemtype is not for loan. + +=cut + +sub notforloan { + my ($self) = @_; + + my $item = $self->item; + my $cache = Koha::Caches->get_instance('availability'); + my $cached = $cache->get_from_cache('itemtype-'.$item->effective_itemtype); + my $itemtype; + if ($cached) { + $itemtype = Koha::ItemType->new->set($cached); + } else { + $itemtype = Koha::ItemTypes->find($item->effective_itemtype); + $cache->set_in_cache('itemtype-'.$item->effective_itemtype, + $itemtype->unblessed, { expiry => 10 }) if $itemtype; + } + + if ($item->notforloan != 0 || $itemtype && $itemtype->notforloan != 0) { + my $av = Koha::AuthorisedValues->search({ + category => 'NOT_LOAN', + authorised_value => $item->notforloan + }); + my $code = $av->count ? $av->next->lib : ''; + if ($item->notforloan > 0) { + return Koha::Exceptions::Item::NotForLoan->new( + status => $item->notforloan, + code => $code, + ); + } elsif ($itemtype && $itemtype->notforloan > 0) { + return Koha::Exceptions::ItemType::NotForLoan->new( + status => $itemtype->notforloan, + code => $code, + itemtype => $itemtype->itemtype, + ); + } elsif ($item->notforloan < 0) { + return Koha::Exceptions::Item::NotForLoan->new( + status => $item->notforloan, + code => $code, + ); + } + } + return; +} + +=head3 onloan + +Returns Koha::Exceptions::Item::CheckedOut if item is onloan. + +This does not query issues table, but simply checks item's onloan-column. + +=cut + +sub onloan { + my ($self) = @_; + + # This simply checks item's onloan-column to determine item's checked out + # -status. Use C to perform an actual query for checkouts. + if ($self->item->onloan) { + return Koha::Exceptions::Item::CheckedOut->new; + } +} + +=head3 restricted + +Returns Koha::Exceptions::Item::Restricted if item is restricted. + +=cut + +sub restricted { + my ($self) = @_; + + if ($self->item->restricted) { + return Koha::Exceptions::Item::Restricted->new; + } + return; +} + +=head3 transfer + +Returns Koha::Exceptions::Item::Transfer if item is in transfer. + +Koha::Exceptions::Item::Transfer additional fields: + from_library + to_library + datesent + +=cut + +sub transfer { + my ($self) = @_; + + my $transfer = Koha::Item::Transfers->find({ + itemnumber => $self->item->itemnumber, + datesent => { '!=', undef }, + }); + if ($transfer) { + return Koha::Exceptions::Item::Transfer->new( + from_library => $transfer->frombranch, + to_library => $transfer->tobranch, + datesent => $transfer->datesent, + ); + } + return; +} + +=head3 transfer_limit + +Returns Koha::Exceptions::Item::CannotBeTransferred a transfer limit applies +for item. + +Koha::Exceptions::Item::CannotBeTransferred additional parameters: + from_library + to_library + +=cut + +sub transfer_limit { + my ($self, $to_branch) = @_; + + return unless C4::Context->preference('UseBranchTransferLimits'); + my $item = $self->item; + my $limit_type = C4::Context->preference('BranchTransferLimitsType'); + my $code; + if ($limit_type eq 'itemtype') { + $code = $item->effective_itemtype; + } elsif ($limit_type eq 'ccode') { + $code = $item->ccode; + } else { + Koha::Exceptions::BadParameter->throw( + error => 'System preference BranchTransferLimitsType has an' + .' unrecognized value.' + ); + } + + my $allowed = C4::Circulation::IsBranchTransferAllowed( + $to_branch, + $item->holdingbranch, + $code + ); + if (!$allowed) { + return Koha::Exceptions::Item::CannotBeTransferred->new( + from_library => $item->holdingbranch, + to_library => $to_branch, + ); + } + return; +} + +=head3 unknown_barcode + +Returns Koha::Exceptions::Item::UnknownBarcode if item has no barcode. + +=cut + +sub unknown_barcode { + my ($self) = @_; + + my $item = $self->item; + unless ($item->barcode) { + return Koha::Exceptions::Item::UnknownBarcode->new; + } + return; +} + +=head3 withdrawn + +Returns Koha::Exceptions::Item::Withdrawn if item is withdrawn. + +=cut + +sub withdrawn { + my ($self) = @_; + + if ($self->item->withdrawn) { + return Koha::Exceptions::Item::Withdrawn->new; + } + return; +} + +1; diff --git a/Koha/Availability/Checks/LibraryItemRule.pm b/Koha/Availability/Checks/LibraryItemRule.pm new file mode 100644 index 0000000..dfbc67f --- /dev/null +++ b/Koha/Availability/Checks/LibraryItemRule.pm @@ -0,0 +1,144 @@ +package Koha::Availability::Checks::LibraryItemRule; + +# Copyright Koha-Suomi Oy 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 base qw(Koha::Availability::Checks); + +use C4::Circulation; + +use Koha::Exceptions::Hold; + +=head3 new + +With given parameters item (Koha::Item) and patron (Koha::Patron), selects the +effective library item rule and stores it into this object. Further calls to +methods of this object will use this library item rule. + +PARAMETERS: +item # Koha::Item object +patron # Koha::Patron object + +=cut + +sub new { + my $class = shift; + my ($params) = @_; + + my $self = $class->SUPER::new(@_); + + my $independentBranch = C4::Context->preference('IndependentBranches'); + my $circcontrol = C4::Context->preference('CircControl'); + my $patron = $self->{'patron'} = $self->_validate_parameter($params, + 'patron', 'Koha::Patron'); + my $item = $self->{'item'} = $self->_validate_parameter($params, + 'item', 'Koha::Item'); + + my $circ_control_branch; + my $branchitemrule; + if ($circcontrol eq 'ItemHomeLibrary' && $item) { + $circ_control_branch = C4::Circulation::_GetCircControlBranch( + $item->unblessed, undef); + } elsif ($circcontrol eq 'PatronLibrary' && $patron) { + $circ_control_branch = C4::Circulation::_GetCircControlBranch( + undef, $patron->unblessed); + } elsif ($circcontrol eq 'PickupLibrary') { + $circ_control_branch = C4::Circulation::_GetCircControlBranch( + undef, undef); + } else { + bless $self, $class; + return $self; + } + + $self->{'branchitemrule'} = C4::Circulation::GetBranchItemRule( + $circ_control_branch, $item->effective_itemtype); + + bless $self, $class; +} + +=head3 hold_not_allowed_by_library + +Returns Koha::Exceptions::Hold::NotAllowedByLibrary if library item rules does +not allow holds. + +=cut + +sub hold_not_allowed_by_library { + my ($self) = @_; + + return unless my $rule = $self->branchitemrule; + + if (!$rule->{holdallowed}) { + return Koha::Exceptions::Hold::NotAllowedByLibrary->new + } + return; +} + +=head3 hold_not_allowed_by_library + +Returns Koha::Exceptions::Hold::NotAllowedFromOtherLibraries if library item rules +define restrictions for holds on items that are from another library than patron. + +=cut + +sub hold_not_allowed_by_other_library { + my ($self) = @_; + + return unless my $rule = $self->branchitemrule; + return unless my $item = $self->item; + + my $fromotherbranches = C4::Context->preference('canreservefromotherbranches'); + my $independentBranch = C4::Context->preference('IndependentBranches'); + + my $patron = $self->patron; + if ($rule->{holdallowed} == 1) { + if (!$patron) { + # Since we don't know who is asking for item and from which + # library, return NotAllowedFromOtherLibraries, but it should + # be set only as an additional availability note + return Koha::Exceptions::Hold::NotAllowedFromOtherLibraries->new; + } elsif ($patron && $patron->branchcode ne $item->homebranch) { + return Koha::Exceptions::Hold::NotAllowedFromOtherLibraries->new; + } + } elsif ($independentBranch && !$fromotherbranches) { + if (!$patron) { + # This should be stored as an additional note + return Koha::Exceptions::Hold::NotAllowedFromOtherLibraries->new + } elsif ($patron && $item->homebranch ne $patron->branchcode) { + return Koha::Exceptions::Hold::NotAllowedFromOtherLibraries->new + } + } + return; +} + +sub _validate_parameter { + my ($self, $params, $key, $ref) = @_; + + if (exists $params->{$key}) { + if (ref($params->{$key}) eq $ref) { + return $params->{$key}; + } else { + Koha::Exceptions::BadParameter->throw( + "Parameter $key must be a $ref object." + ); + } + } +} + +1; diff --git a/Koha/Availability/Checks/Patron.pm b/Koha/Availability/Checks/Patron.pm new file mode 100644 index 0000000..035f185 --- /dev/null +++ b/Koha/Availability/Checks/Patron.pm @@ -0,0 +1,323 @@ +package Koha::Availability::Checks::Patron; + +# Copyright Koha-Suomi Oy 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 base qw(Koha::Availability::Checks); + +use Scalar::Util qw(looks_like_number); + +use C4::Context; +use C4::Members; + +use Koha::Exceptions::Hold; +use Koha::Exceptions::Patron; + +sub new { + my ($class, $patron) = @_; + + unless ($patron) { + Koha::Exceptions::MissingParameter->throw( + error => 'Patron related holdability checks require a patron. Not given.' + ); + } + unless (ref($patron) eq 'Koha::Patron') { + Koha::Exceptions::BadParameter->throw( + error => 'Patron must be a Koha::Patron object.' + ); + } + + my $self = { + patron => $patron, + }; + + bless $self, $class; +} + +=head3 debarred + +Returns Koha::Exceptions::Patron::Debarred if patron is debarred. + +Koha::Exceptions::Patron::Debarred additional fields: + expiration # expiration date of debarment + comment # comment for this debarment + +=cut + +sub debarred { + my ($self) = @_; + + my $patron = $self->patron; + if ($patron->is_debarred) { + return Koha::Exceptions::Patron::Debarred->new( + expiration => $patron->debarred, + comment => $patron->debarredcomment, + ); + } + return; +} + +=head3 debt_checkout + +Returns Koha::Exceptions::Patron::Debt if patron has outstanding fines that +exceed the amount defined in "noissuescharge" system preference. + +Koha::Exceptions::Patron::Debt additional fields: + max_outstanding # maximum allowed amount of outstanding fines + current_outstanding # current amount of outstanding fines + +=cut + +sub debt_checkout { + my ($self) = @_; + + my ($amount) = C4::Members::GetMemberAccountRecords( + $self->patron->borrowernumber + ); + my $maxoutstanding = C4::Context->preference("noissuescharge"); + if (C4::Context->preference('AllFinesNeedOverride') && $amount > 0) { + # All fines need override, so return Koha::Exceptions::Patron::Debt. + return Koha::Exceptions::Patron::Debt->new( + max_outstanding => sprintf("%.2f", $maxoutstanding), + current_outstanding => sprintf("%.2f", $amount), + ); + } else { + return $self->_debt($amount, $maxoutstanding); + } +} + +=head3 debt_checkout_guarantees + +Returns Koha::Exceptions::Patron::DebtGuarantees if patron's guarantees are +exceeding the amount defined in "NoIssuesChargeGuarantees" system preference. + +Koha::Exceptions::Patron::DebtGuarantees additional fields: + max_outstanding # maximum allowed amount of outstanding fines + current_outstanding # current amount of outstanding fines + +=cut + +sub debt_checkout_guarantees { + my ($self) = @_; + + my $patron = $self->patron; + my $max_charges = C4::Context->preference("NoIssuesChargeGuarantees"); + $max_charges = undef unless looks_like_number($max_charges); + my @guarantees = $patron->guarantees; + return unless scalar(@guarantees); + my $guarantees_non_issues_charges; + foreach my $g (@guarantees) { + my ($b, $n, $o) = C4::Members::GetMemberAccountBalance($g->id); + $guarantees_non_issues_charges += $n; + } + if ($guarantees_non_issues_charges > $max_charges) { + return Koha::Exceptions::Patron::DebtGuarantees->new( + max_outstanding => sprintf("%.2f", $max_charges), + current_outstanding => sprintf("%.2f", $guarantees_non_issues_charges), + ); + } + return; +} + +=head3 debt_hold + +Returns Koha::Exceptions::Patron::Debt if patron has outstanding fines that +exceed the amount defined in "maxoutstanding" system preference. + +Koha::Exceptions::Patron::Debt additional fields: + max_outstanding # maximum allowed amount of outstanding fines + current_outstanding # current amount of outstanding fines + +=cut + +sub debt_hold { + my ($self) = @_; + + my ($amount) = C4::Members::GetMemberAccountRecords( + $self->patron->borrowernumber + ); + my $maxoutstanding = C4::Context->preference("maxoutstanding"); + return $self->_debt($amount, $maxoutstanding); +} + +=head3 debt_checkout + +Returns Koha::Exceptions::Patron::Debt if patron has outstanding fines that +exceed the amount defined in "OPACFineNoRenewals" system preference. + +Koha::Exceptions::Patron::Debt additional fields: + max_outstanding # maximum allowed amount of outstanding fines + current_outstanding # current amount of outstanding fines + +=cut + +sub debt_renew_opac { + my ($self) = @_; + + my ($amount) = C4::Members::GetMemberAccountRecords( + $self->patron->borrowernumber + ); + my $maxoutstanding = C4::Context->preference("OPACFineNoRenewals"); + return $self->_debt($amount, $maxoutstanding); +} + +=head3 exceeded_maxreserves + +Returns Koha::Exceptions::Hold::MaximumHoldsReached if the total amount of +patron's holds exceeds the amount defined in "maxreserves" system preference. + +Koha::Exceptions::Hold::MaximumHoldsReached additional fields: + max_holds_allowed # maximum amount of allowed holds (maxreserves preference) + current_hold_count # total amount of patron's current (non-found) holds + +=cut + +sub exceeded_maxreserves { + my ($self) = @_; + + my $patron = $self->patron; + my $max = C4::Context->preference('maxreserves'); + my $holds = Koha::Holds->search({ + borrowernumber => $patron->borrowernumber, + found => undef, + })->count(); + if ($holds && $max && $holds >= $max) { + return Koha::Exceptions::Hold::MaximumHoldsReached->new( + max_holds_allowed => $max, + current_hold_count => $holds, + ); + } + return; +} + +=head3 expired + +Returns Koha::Exceptions::Patron::CardExpired if patron's card has been expired. + +Koha::Exceptions::Patron::CardExpired additional fields: + expiration_date + +=cut + +sub expired { + my ($self) = @_; + + my $patron = $self->patron; + if ($patron->is_expired) { + return Koha::Exceptions::Patron::CardExpired->new( + expiration_date => $patron->dateexpiry + ); + } + return; +} + +=head3 from_another_library + +Returns Koha::Exceptions::Patron::FromAnotherLibrary if patron is from another +library than currently logged-in user. System preference "IndependentBranches" +is considered for this method. + +Koha::Exceptions::Patron::FromAnotherLibrary additional fields: + patron_branch # patron's library + current_branch # library of currently logged-in user + +=cut + +sub from_another_library { + my ($self) = @_; + + my $patron = $self->patron; + if (C4::Context->preference("IndependentBranches")) { + my $userenv = C4::Context->userenv; + unless (C4::Context->IsSuperLibrarian()) { + if ($patron->branchcode ne $userenv->{branch}) { + return Koha::Exceptions::Patron::FromAnotherLibrary->new( + patron_branch => $patron->branchcode, + current_branch => $userenv->{branch}, + ); + } + } + } + return; +} + +=head3 gonenoaddress + +Returns Koha::Exceptions::Patron::GoneNoAddress if patron is gonenoaddress. + +=cut + +sub gonenoaddress { + my ($self) = @_; + + if ($self->patron->gonenoaddress) { + return Koha::Exceptions::Patron::GoneNoAddress->new; + } + return; +} + +=head3 lost + +Returns Koha::Exceptions::Patron::CardLost if patron's card is marked as lost. + +=cut + +sub lost { + my ($self) = @_; + + if ($self->patron->lost) { + return Koha::Exceptions::Patron::CardLost->new; + } + return; +} + +=head3 overdue_checkouts + +Returns Koha::Exceptions::Patron::DebarredOverdue if patron is debarred because +they have overdue checkouts. + +Koha::Exceptions::Patron::DebarredOverdue additional fields: + number_of_overdues + +=cut + +sub overdue_checkouts { + my ($self) = @_; + + my $number; + if ($number = $self->patron->has_overdues) { + return Koha::Exceptions::Patron::DebarredOverdue->new( + number_of_overdues => $number, + ); + } + return; +} + +sub _debt { + my ($self, $amount, $maxoutstanding) = @_; + if ($amount && $maxoutstanding && $amount > $maxoutstanding) { + return Koha::Exceptions::Patron::Debt->new( + max_outstanding => sprintf("%.2f", $maxoutstanding), + current_outstanding => sprintf("%.2f", $amount), + ); + } + return; +} + +1; diff --git a/Koha/Schema/Result/Reserve.pm b/Koha/Schema/Result/Reserve.pm index 4d56006..319bc1b 100644 --- a/Koha/Schema/Result/Reserve.pm +++ b/Koha/Schema/Result/Reserve.pm @@ -311,6 +311,18 @@ __PACKAGE__->belongs_to( # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:v9+wPKUT381CLNlusQ4LMA __PACKAGE__->belongs_to( + "borrower", + "Koha::Schema::Result::Borrower", + { borrowernumber => "borrowernumber" }, + { + is_deferrable => 1, + join_type => "LEFT", + on_delete => "CASCADE", + on_update => "CASCADE", + }, +); + +__PACKAGE__->belongs_to( "item", "Koha::Schema::Result::Item", { itemnumber => "itemnumber" }, @@ -334,4 +346,14 @@ __PACKAGE__->belongs_to( }, ); +__PACKAGE__->belongs_to( + "biblioitem", + "Koha::Schema::Result::Biblioitem", + { biblionumber => "biblionumber" }, + { + is_deferrable => 1, + join_type => "LEFT", + }, +); + 1; diff --git a/t/db_dependent/Koha/Availability/Checks/Biblio.t b/t/db_dependent/Koha/Availability/Checks/Biblio.t new file mode 100644 index 0000000..9893305 --- /dev/null +++ b/t/db_dependent/Koha/Availability/Checks/Biblio.t @@ -0,0 +1,165 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 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 => 4; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use C4::Members; + +use Koha::Biblios; +use Koha::Database; +use Koha::Items; + +use Koha::Availability::Checks::Biblio; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +subtest 'another_item_checked_out' => \&t_another_item_checked_out; +sub t_another_item_checked_out { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $biblio = Koha::Biblios->find($item->biblionumber); + my $item2 = build_a_test_item(); + $item2->set({ + biblionumber => $item->biblionumber, + biblioitemnumber => $item->biblioitemnumber, + })->store; + issue_item($item, $patron); + my $bibcalc = Koha::Availability::Checks::Biblio->new($biblio); + my $expecting = 'Koha::Exceptions::Biblio::AnotherItemCheckedOut'; + + is(Koha::Checkouts->search({ itemnumber => $item->itemnumber })->count, 1, + 'I found out that item is checked out.'); + is(ref($bibcalc->another_item_checked_out($patron)), $expecting, $expecting); +}; + +subtest 'checked_out' => \&t_checked_out; +sub t_checked_out { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $biblio = Koha::Biblios->find($item->biblionumber); + my $item2 = build_a_test_item(); + $item2->set({ + biblionumber => $item->biblionumber, + biblioitemnumber => $item->biblioitemnumber, + })->store; + issue_item($item, $patron); + my $bibcalc = Koha::Availability::Checks::Biblio->new($biblio); + my $expecting = 'Koha::Exceptions::Biblio::CheckedOut'; + + is(Koha::Checkouts->search({ itemnumber => $item->itemnumber })->count, 1, + 'I found out that item is checked out.'); + is(ref($bibcalc->checked_out($patron)), $expecting, $expecting); +}; + +subtest 'forbid_holds_on_patrons_possessions' => \&t_forbid_holds_on_patrons_possessions; +sub t_forbid_holds_on_patrons_possessions { + plan tests => 5; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $biblio = Koha::Biblios->find($item->biblionumber); + my $item2 = build_a_test_item(); + $item2->set({ + biblionumber => $item->biblionumber, + biblioitemnumber => $item->biblioitemnumber, + })->store; + issue_item($item, $patron); + my $bibcalc = Koha::Availability::Checks::Biblio->new($biblio); + my $expecting = 'Koha::Exceptions::Biblio::AnotherItemCheckedOut'; + + t::lib::Mocks::mock_preference('AllowHoldsOnPatronsPossessions', 0); + is(C4::Context->preference('AllowHoldsOnPatronsPossessions'), 0, + 'System preference AllowHoldsOnPatronsPossessions is disabled.'); + is(Koha::Checkouts->search({ itemnumber => $item->itemnumber })->count, 1, + 'I found out that item is checked out.'); + is(ref($bibcalc->forbid_holds_on_patrons_possessions($patron)), $expecting, + $expecting); + t::lib::Mocks::mock_preference('AllowHoldsOnPatronsPossessions', 1); + is(C4::Context->preference('AllowHoldsOnPatronsPossessions'), 1, + 'Given system preference AllowHoldsOnPatronsPossessions is enabled,'); + ok(!$bibcalc->forbid_holds_on_patrons_possessions($patron), 'When I check' + .' biblio forbid holds on patrons possesions, no exception is given.'); +}; + +subtest 'forbid_multiple_issues' => \&t_forbid_multiple_issues; +sub t_forbid_multiple_issues { + plan tests => 4; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $biblio = Koha::Biblios->find($item->biblionumber); + my $item2 = build_a_test_item(); + $item2->set({ + biblionumber => $item->biblionumber, + biblioitemnumber => $item->biblioitemnumber, + })->store; + issue_item($item2, $patron); + my $checkoutcalc = Koha::Availability::Checks::Biblio->new($biblio); + + is($item->biblionumber, $item2->biblionumber, 'Item one and item two belong' + .' to the same biblio.'); + is(Koha::Checkouts->find({ itemnumber => $item2->itemnumber})->borrowernumber, + $patron->borrowernumber, 'Item two seems to be issued to me.'); + + subtest 'Given AllowMultipleIssuesOnABiblio is enabled' => sub { + plan tests => 2; + + t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1); + is(C4::Context->preference('AllowMultipleIssuesOnABiblio'), 1, + 'System preference AllowMultipleIssuesOnABiblio is enabled.'); + is($checkoutcalc->forbid_multiple_issues($patron), undef, + 'When I ask if there is another biblio already checked out,' + .' then no exception is returned.'); + }; + + subtest 'Given AllowMultipleIssuesOnABiblio is disabled' => sub { + plan tests => 3; + + my $expecting = 'Koha::Exceptions::Biblio::CheckedOut'; + my $returned; + + t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0); + is(C4::Context->preference('AllowMultipleIssuesOnABiblio'), 0, + 'System preference AllowMultipleIssuesOnABiblio is disabled.'); + ok($returned = ref($checkoutcalc->forbid_multiple_issues($patron)), + 'When I ask if there is another biblio already checked out,'); + is ($returned, $expecting, "then exception $expecting is returned."); + }; +}; + +$schema->storage->txn_rollback; + +1; diff --git a/t/db_dependent/Koha/Availability/Checks/Biblioitem.t b/t/db_dependent/Koha/Availability/Checks/Biblioitem.t new file mode 100644 index 0000000..012eca0 --- /dev/null +++ b/t/db_dependent/Koha/Availability/Checks/Biblioitem.t @@ -0,0 +1,83 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 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 => 2; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use Koha::Biblioitems; +use Koha::Database; +use Koha::DateUtils; +use Koha::Items; + +use Koha::Availability::Checks::Biblioitem; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +subtest 'age_restricted, given patron is old enough' => \&t_old_enough; +sub t_old_enough { + plan tests => 3; + + my $item = build_a_test_item(); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + $biblioitem->set({ agerestriction => 'PEGI 18' })->store; + my $patron = build_a_test_patron(); + my $bibitemcalc = Koha::Availability::Checks::Biblioitem->new($biblioitem); + + is($patron->dateofbirth, '1950-10-10', 'Patron says he is born in the 1950s.'); + is($biblioitem->agerestriction, 'PEGI 18', 'Item is restricted for under 18.'); + ok(!$bibitemcalc->age_restricted($patron), 'When I check for age restriction, then no exception is given.'); +}; + +subtest 'age_restricted, given patron is too young' => \&t_too_young; +sub t_too_young { + plan tests => 4; + + my $item = build_a_test_item(); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + $biblioitem->set({ agerestriction => 'PEGI 18' })->store; + my $patron = build_a_test_patron(); + my $now = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 }); + $patron->set({ dateofbirth => $now })->store; + my $bibitemcalc = Koha::Availability::Checks::Biblioitem->new($biblioitem); + my $expecting = 'Koha::Exceptions::Patron::AgeRestricted'; + + is($patron->dateofbirth, $now, 'Patron says he is born today.'); + my $reason = $bibitemcalc->age_restricted($patron); + is($biblioitem->agerestriction, 'PEGI 18', 'Biblio item is restricted for under 18.'); + is(ref($reason), $expecting, + "When I check for age restriction, then exception $expecting is given."); + is($reason->age_restriction, 'PEGI 18', 'The reason also specifies the' + .' age restriction, PEGI 18.'); +}; + +$schema->storage->txn_rollback; + +1; diff --git a/t/db_dependent/Koha/Availability/Checks/Checkout.t b/t/db_dependent/Koha/Availability/Checks/Checkout.t new file mode 100644 index 0000000..40a220c --- /dev/null +++ b/t/db_dependent/Koha/Availability/Checks/Checkout.t @@ -0,0 +1,113 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 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 => 4; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use C4::Members; + +use Koha::Biblios; +use Koha::Database; +use Koha::Items; + +use Koha::Availability::Checks::Checkout; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +subtest 'invalid_due_date, invalid due date' => \&t_invalid_due_date; +sub t_invalid_due_date { + plan tests => 1; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + + my $duedate = 'invalid'; + my $checkoutcalc = Koha::Availability::Checks::Checkout->new; + my $expecting = 'Koha::Exceptions::Checkout::InvalidDueDate'; + is(ref($checkoutcalc->invalid_due_date($item, $patron, $duedate)), $expecting, + "When using duedate 'invalid', then exception $expecting is given."); +}; + +subtest 'invalid_due_date, due date before now' => \&t_invalid_due_date_before_now; +sub t_invalid_due_date_before_now { + plan tests => 1; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + + my $before = output_pref({ dt => dt_from_string()->subtract_duration( + DateTime::Duration->new(days => 100)), dateformat => 'iso', dateonly => 1 }); + my $checkoutcalc = Koha::Availability::Checks::Checkout->new; + my $expecting = 'Koha::Exceptions::Checkout::DueDateBeforeNow'; + is(ref($checkoutcalc->invalid_due_date($item, $patron, $before)), $expecting, + "When using duedate that is in the past, then exception $expecting is given."); +}; + +subtest 'invalid_due_date, good due date' => \&t_invalid_due_date_not_invalid; +sub t_invalid_due_date_not_invalid { + plan tests => 1; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + + my $before = output_pref({ dt => dt_from_string()->add_duration( + DateTime::Duration->new(days => 14)), dateformat => 'iso', dateonly => 1 }); + my $checkoutcalc = Koha::Availability::Checks::Checkout->new; + is($checkoutcalc->invalid_due_date($item, $patron, $before), undef, + 'When using a duedate that is in two weeks from now, then no exception is given.'); +}; + +subtest 'no_more_renewals' => \&t_no_more_renewals; +sub t_no_more_renewals { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + + issue_item($item, $patron); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => '*', + categorycode => '*', + renewalsallowed => 0, + })->store; + my $checkoutcalc = Koha::Availability::Checks::Checkout->new; + my $expecting = 'Koha::Exceptions::Checkout::NoMoreRenewals'; + my $issue = Koha::Checkouts->find({ itemnumber => $item->itemnumber}); + is($issue->borrowernumber, $patron->borrowernumber, 'Item seems to be issued to me.'); + is(ref($checkoutcalc->no_more_renewals($issue)), $expecting, + "When checking for no more renewals, then exception $expecting is given."); +}; + +$schema->storage->txn_rollback; + +1; diff --git a/t/db_dependent/Koha/Availability/Checks/IssuingRule.t b/t/db_dependent/Koha/Availability/Checks/IssuingRule.t new file mode 100644 index 0000000..ef0a615 --- /dev/null +++ b/t/db_dependent/Koha/Availability/Checks/IssuingRule.t @@ -0,0 +1,513 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 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 => 13; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use Koha::Database; +use Koha::IssuingRules; +use Koha::Items; +use Koha::ItemTypes; + +use Koha::Availability::Checks::IssuingRule; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $builder = t::lib::TestBuilder->new; + +subtest 'maximum_checkouts_reached' => \&t_maximum_checkouts_reached; +sub t_maximum_checkouts_reached { + plan tests => 5; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + maxissueqty => 1, + })->store; + + issue_item($item, $patron); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Checkout::MaximumCheckoutsReached'; + my $max_checkouts_reached = $issuingcalc->maximum_checkouts_reached; + is($rule->maxissueqty, 1, 'When I look at issuing rules, I see that' + .' maxissueqty is 1 for this itemtype.'); + is(Koha::Checkouts->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one checkout.'); + is(ref($max_checkouts_reached), $expecting, "When I ask if" + ." maximum checkouts is reached, exception $expecting is given."); + is($max_checkouts_reached->max_checkouts_allowed, $rule->maxissueqty, + 'Exception says max checkouts allowed is same as maxissueqty.'); + is($max_checkouts_reached->current_checkout_count, 1, + 'Exception says I currently have one checkout.'); +}; + +subtest 'maximum_checkouts_reached, not reached' => \&t_maximum_checkouts_not_reached; +sub t_maximum_checkouts_not_reached { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + maxissueqty => 2, + })->store; + + issue_item($item, $patron); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + + is($rule->maxissueqty, 2, 'When I look at issuing rules, I see that' + .' maxissueqty is 2 for this itemtype.'); + is(Koha::Checkouts->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one checkout.'); + ok(!$issuingcalc->maximum_checkouts_reached, 'When I ask if' + .' maximum checkouts is reached, no exception is given.'); +}; + +subtest 'maximum_holds_reached' => \&t_maximum_holds_reached; +sub t_maximum_holds_reached { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + reservesallowed => 1, + holds_per_record => 2, + })->store; + + add_item_level_hold($item, $patron, $item->homebranch); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Hold::MaximumHoldsReached'; + + is($rule->reservesallowed, 1, 'When I look at issuing rules, I see that' + .' reservesallowed is 1 for this itemtype.'); + is(Koha::Holds->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one hold.'); + is(ref($issuingcalc->maximum_holds_reached), $expecting, "When I ask if" + ." maximum holds is reached, exception $expecting is given."); +}; + +subtest 'maximum_holds_reached, not reached' => \&t_maximum_holds_not_reached; +sub t_maximum_holds_not_reached { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + reservesallowed => 2, + })->store; + + add_item_level_hold($item, $patron, $patron->branchcode); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Hold::MaximumHoldsReached'; + is($rule->reservesallowed, 2, 'When I look at issuing rules, I see that' + .' reservesallowed is 2 for this itemtype.'); + is(Koha::Holds->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one hold.'); + ok(!$issuingcalc->maximum_holds_reached, 'When I ask if' + .' maximum holds is reached, no exception is given.'); +}; + +subtest 'maximum_holds_for_record_reached' => \&t_maximum_holds_for_record_reached; +sub t_maximum_holds_for_record_reached { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + reservesallowed => 2, + holds_per_record => 1, + })->store; + + add_item_level_hold($item, $patron, $item->homebranch); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Hold::MaximumHoldsForRecordReached'; + + is($rule->holds_per_record, 1, 'When I look at issuing rules, I see that' + .' holds_per_record is 1 for this itemtype.'); + is(Koha::Holds->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one hold.'); + is(ref($issuingcalc->maximum_holds_for_record_reached), $expecting, "When I ask if" + ." maximum holds for record is reached, exception $expecting is given."); +}; + +subtest 'maximum_holds_for_record_reached, not reached' => \&t_maximum_holds_for_record_not_reached; +sub t_maximum_holds_for_record_not_reached { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + reservesallowed => 2, + holds_per_record => 1, + })->store; + + add_item_level_hold($item, $patron, $patron->branchcode); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + is($rule->holds_per_record, 1, 'When I look at issuing rules, I see that' + .' holds_per_record is 1 for this itemtype.'); + is(Koha::Holds->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one hold.'); + ok(!$issuingcalc->maximum_holds_for_recordreached, 'When I ask if' + .' maximum holds for record is reached, no exception is given.'); +}; + +subtest 'on_shelf_holds_forbidden while on shelf holds are allowed' => \&t_on_shelf_holds_forbidden; +sub t_on_shelf_holds_forbidden { + plan tests => 2; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $item2 = build_a_test_item(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + holds_per_record => 1, + reservesallowed => 1, + onshelfholds => 1, + })->store; + + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + is($rule->onshelfholds, 1, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed.'); + ok(!$issuingcalc->on_shelf_holds_forbidden, 'When I check availability calculation' + .' to see if on shelf holds are forbidden, then no exception is given.'); +}; + +subtest 'on_shelf_holds_forbidden if any available' => \&t_on_shelf_holds_forbidden_any_available; +sub t_on_shelf_holds_forbidden_any_available { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + my $biblio = Koha::Biblios->find($item->biblionumber); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + my $item2 = build_a_test_item($biblio, $biblioitem); + $item2->itype($item->itype)->store; + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + holds_per_record => 1, + reservesallowed => 1, + onshelfholds => 2, + })->store; + + my $expecting = 'Koha::Exceptions::Hold::OnShelfNotAllowed'; + subtest 'While there are available items' => sub { + plan tests => 4; + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + is($rule->onshelfholds, 2, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed if all are unavailable.'); + ok(!$item->onloan && !$item2->onloan, 'Both items are available.'); + ok($issuingcalc->on_shelf_holds_forbidden, 'When I check ' + .'availability calculation to see if on shelf holds are forbidden,'); + is(ref($issuingcalc->on_shelf_holds_forbidden), $expecting, 'Then' + ." exception $expecting is given."); + }; + subtest 'While one of two items is available' => sub { + plan tests => 7; + is($rule->onshelfholds, 2, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed if all are unavailable.'); + ok(issue_item($item, $patron), 'We have issued one of two items.'); + my $issuingcalc; + ok($issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item + }), 'First, we will check on shelf hold restrictions for the item that is' + .' unavailable.'); + ok($issuingcalc->on_shelf_holds_forbidden, 'When I check ' + .'availability calculation to see if on shelf holds are forbidden,'); + is(ref($issuingcalc->on_shelf_holds_forbidden), $expecting, 'Then' + ." exception $expecting is given."); + ok($issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item2 + }), 'Then, we will check on shelf hold restrictions for the item that is' + .' available.'); + is(ref($issuingcalc->on_shelf_holds_forbidden), $expecting, 'When I check ' + .'availability calculation to see if on shelf holds are forbidden, then' + ." exception $expecting is given."); + }; + subtest 'While all are unavailable' => sub { + plan tests => 3; + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + is($rule->onshelfholds, 2, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed if all are unavailable.'); + ok(issue_item($item2, $patron), 'Both items are now unavailable.'); + ok(!$issuingcalc->on_shelf_holds_forbidden, 'When I check availability calculation' + .' to see if on shelf holds are forbidden, then no exception is given.'); + }; +}; + +subtest 'on_shelf_holds_forbidden if all available' => \&t_on_shelf_holds_forbidden_all_available; +sub t_on_shelf_holds_forbidden_all_available { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + my $biblio = Koha::Biblios->find($item->biblionumber); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + my $item2 = build_a_test_item($biblio, $biblioitem); + $item2->itype($item->itype)->store; + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + holds_per_record => 1, + reservesallowed => 1, + onshelfholds => 0, + })->store; + + my $expecting = 'Koha::Exceptions::Hold::OnShelfNotAllowed'; + subtest 'While all items are available' => sub { + plan tests => 3; + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + is($rule->onshelfholds, 0, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed if any is unavailable.'); + ok($issuingcalc->on_shelf_holds_forbidden,'When I check ' + .'availability calculation to see if on shelf holds are forbidden,'); + is(ref($issuingcalc->on_shelf_holds_forbidden), $expecting, 'Then' + ." exception $expecting is given."); + }; + subtest 'While one of two items is available' => sub { + plan tests => 7; + is($rule->onshelfholds, 0, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed if any is unavailable.'); + ok(issue_item($item, $patron), 'We have issued one of two items.'); + $item = Koha::Items->find($item->itemnumber); #refresh + my $issuingcalc; + ok($issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item + }), 'First, we will check on shelf hold restrictions for the item that is' + .' unavailable.'); + ok(!$issuingcalc->on_shelf_holds_forbidden, 'When I check availability calculation' + .' to see if on shelf holds are forbidden, then no exception is given.'); + ok($issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item2 + }), 'Then, we will check on shelf hold restrictions for the item that is' + .' available.'); + ok($issuingcalc->on_shelf_holds_forbidden, 'When I check ' + .'availability calculation to see if on shelf holds are forbidden,'); + is(ref($issuingcalc->on_shelf_holds_forbidden), $expecting, 'Then' + ." exception $expecting is given."); + }; + subtest 'While all are unavailable' => sub { + plan tests => 3; + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + is($rule->onshelfholds, 0, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed if all are unavailable.'); + ok(issue_item($item2, $patron), 'Both items are now unavailable.'); + ok(!$issuingcalc->on_shelf_holds_forbidden, 'When I check availability calculation' + .' to see if on shelf holds are forbidden, then no exception is given.'); + }; +}; + +subtest 'opac_item_level_hold_forbidden' => \&t_opac_item_level_hold_forbidden; +sub t_opac_item_level_hold_forbidden { + plan tests => 2; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + holds_per_record => 1, + reservesallowed => 1, + opacitemholds => 'N', + })->store; + + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + my $expecting = 'Koha::Exceptions::Hold::ItemLevelHoldNotAllowed'; + is($rule->opacitemholds, 'N', 'When I look at issuing rules, I see that' + .' opacitemholds is disabled for this itemtype'); + is(ref($issuingcalc->opac_item_level_hold_forbidden), $expecting, "When I ask if" + ." item level holds are allowed, exception $expecting is given."); +}; + +subtest 'opac_item_level_hold_forbidden, not forbidden' => \&t_opac_item_level_hold_not_forbidden; +sub t_opac_item_level_hold_not_forbidden { + plan tests => 2; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + holds_per_record => 1, + reservesallowed => 1, + opacitemholds => 'Y', + })->store; + + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + my $expecting = 'Koha::Exceptions::Hold::ItemLevelHoldNotAllowed'; + is($rule->opacitemholds, 'Y', 'When I look at issuing rules, I see that' + .' opacitemholds is enabled for this itemtype'); + ok(!$issuingcalc->opac_item_level_hold_forbidden, 'When I ask if' + .' item level holds are allowed, then no exception is given.'); +}; + +subtest 'zero_holds_allowed' => \&t_zero_holds_allowed; +sub t_zero_holds_allowed { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + reservesallowed => 0, + })->store; + + add_item_level_hold($item, $patron, $item->homebranch); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Hold::ZeroHoldsAllowed'; + + is($rule->reservesallowed, 0, 'When I look at issuing rules, I see that' + .' zero holds are allowed for this itemtype.'); + is(Koha::Holds->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one hold.'); + is(ref($issuingcalc->zero_holds_allowed), $expecting, "When I ask if" + ." zero holds are allowed, exception $expecting is given."); +}; + +subtest 'zero_checkouts_allowed' => \&t_zero_checkouts_allowed; +sub t_zero_checkouts_allowed { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + maxissueqty => 0, + })->store; + + issue_item($item, $patron); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Checkout::ZeroCheckoutsAllowed'; + + is($rule->maxissueqty, 0, 'When I look at issuing rules, I see that' + .' zero checkouts are allowed for this itemtype.'); + is(Koha::Checkouts->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one checkout.'); + is(ref($issuingcalc->zero_checkouts_allowed), $expecting, "When I ask if" + ." zero checkouts are allowed, exception $expecting is given."); +}; + +$schema->storage->txn_rollback; + +1; diff --git a/t/db_dependent/Koha/Availability/Checks/Item.t b/t/db_dependent/Koha/Availability/Checks/Item.t new file mode 100644 index 0000000..444eb07 --- /dev/null +++ b/t/db_dependent/Koha/Availability/Checks/Item.t @@ -0,0 +1,382 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 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 => 14; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use C4::Members; + +use Koha::Account::Lines; +use Koha::Biblioitems; +use Koha::Database; +use Koha::DateUtils; +use Koha::Items; +use Koha::Item::Transfers; + +use Koha::Availability::Checks::Item; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +subtest 'checked_out' => \&t_checked_out; +sub t_checked_out { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + issue_item($item, $patron); + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::CheckedOut'; + + is(Koha::Checkouts->search({ itemnumber => $item->itemnumber })->count, 1, + 'I found out that item is checked out.'); + is(ref($itemcalc->checked_out), $expecting, "When I check item availability" + ." calculation for checked_out, then exception $expecting is given."); +}; + +subtest 'damaged' => \&t_damaged; +sub t_damaged { + plan tests => 2; + + t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0); + + my $item = build_a_test_item()->set({damaged=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::Damaged'; + + is($item->damaged, 1, 'When I look at the item, I see that it is damaged.'); + is(ref($itemcalc->damaged), $expecting, "When I check item availability" + ." calculation for damaged, then exception $expecting is given."); +}; + + +subtest 'from_another_branch, item from same branch than me' +=> \&t_from_another_library_same_branch; +sub t_from_another_library_same_branch { + plan tests => 3; + + t::lib::Mocks::mock_preference('IndependentBranches', 1); + t::lib::Mocks::mock_preference('HomeOrHoldingBranch', 'homebranch'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, + 'Midway Public Library', undef, '', ''); + + my $item = build_a_test_item(); + $item->homebranch($branchcode)->store; + + my $itemcalc = Koha::Availability::Checks::Item->new($item); + + is(C4::Context->userenv->{branch}, $item->homebranch, + 'Patron is from same branch as me.'); + ok(C4::Context->preference('IndependentBranches'), + 'IndependentBranches system preference is on.'); + ok(!$itemcalc->from_another_library, 'When I check if item is' + .' from same branch as me, then no exception is given.'); +} + +subtest 'from_another_library, item from different branch than me' +=> \&t_from_another_library; +sub t_from_another_library { + plan tests => 3; + + t::lib::Mocks::mock_preference('IndependentBranches', 1); + t::lib::Mocks::mock_preference('HomeOrHoldingBranch', 'homebranch'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + my $branchcode2 = $builder->build({ source => 'Branch' })->{'branchcode'}; + + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, + 'Midway Public Library', undef, '', ''); + + my $item = build_a_test_item(); + $item->homebranch($branchcode2)->store; + + my $itemcalc= Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::FromAnotherLibrary'; + + isnt(C4::Context->userenv->{branch}, $item->homebranch, 'Item is not from' + .' same branch as me.'); + ok(C4::Context->preference('IndependentBranches'), 'IndependentBranches' + .' system preference is on.'); + is(ref($itemcalc->from_another_library), $expecting, 'When I check if item is' + ." from same branch as me, then $expecting is given."); +} + +subtest 'held' => \&t_held; +sub t_held { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + add_item_level_hold($item, $patron, $patron->branchcode); + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::Held'; + + is(Koha::Holds->search({biblionumber => $item->biblionumber})->count, 1, + 'I found out that item is held.'); + is(ref($itemcalc->held), $expecting, "When I check item availability " + ."calculation for held, then exception $expecting is given."); +}; + +subtest 'held_by_patron' => \&t_held_by_patron; +sub t_held_by_patron { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + add_item_level_hold($item, $patron, $patron->branchcode); + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::AlreadyHeldForThisPatron'; + + is(Koha::Holds->search({biblionumber => $item->biblionumber})->count, 1, + 'I found out that item is held.'); + is(ref($itemcalc->held_by_patron($patron)), $expecting, "When I check item " + ."availability calculation for held, then exception $expecting is given."); +}; + +subtest 'lost' => \&t_lost; +sub t_lost { + plan tests => 2; + + my $item = build_a_test_item()->set({itemlost=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::Lost'; + + is($item->itemlost, 1, 'When I look at the item, I see that it is lost.'); + is(ref($itemcalc->lost), $expecting, "When I check availability calculation" + ." for item lost, then exception $expecting is given."); +}; + +subtest 'notforloan' => \&t_notforloan; +sub t_notforloan { + plan tests => 2; + + subtest 'Given item is notforloan > 0' => sub { + plan tests => 2; + + my $item = build_a_test_item()->set({notforloan=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::NotForLoan'; + + is($item->notforloan, 1, 'When I look at the item, I see that it is not' + .' for loan.'); + is(ref($itemcalc->notforloan), $expecting, + "When I look availability calculation for at item notforloan, then " + ." exception $expecting is given."); + }; + + subtest 'Given item type is notforloan > 0' => sub { + subtest 'Given item-level_itypes is on (item-itemtype)' + => \&t_itemlevel_itemtype_notforloan_item_level_itypes_on; + subtest 'Given item-level_itypes is off (biblioitem-itemtype)' + => \&t_itemlevel_itemtype_notforloan_item_level_itypes_off; + sub t_itemlevel_itemtype_notforloan_item_level_itypes_on { + plan tests => 3; + + t::lib::Mocks::mock_preference('item-level_itypes', 1); + + my $item = build_a_test_item(); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + my $itemtype = Koha::ItemTypes->find($item->itype); + $itemtype->set({notforloan=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::ItemType::NotForLoan'; + + is(Koha::ItemTypes->find($biblioitem->itemtype)->notforloan, 0, + 'Biblioitem itemtype is for loan.'); + is(Koha::ItemTypes->find($item->itype)->notforloan, 1, + 'Item itemtype is not for loan.'); + is(ref($itemcalc->notforloan), $expecting, "When I look at " + ."availability calculation for item notforloan, then " + ."exception $expecting is given."); + }; + sub t_itemlevel_itemtype_notforloan_item_level_itypes_off { + plan tests => 3; + + t::lib::Mocks::mock_preference('item-level_itypes', 0); + + my $item = build_a_test_item(); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + my $itemtype = Koha::ItemTypes->find($biblioitem->itemtype); + $itemtype->set({notforloan=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::ItemType::NotForLoan'; + + is(Koha::ItemTypes->find($biblioitem->itemtype)->notforloan, 1, + 'Biblioitem itemtype is not for loan.'); + is(Koha::ItemTypes->find($item->itype)->notforloan, 0, + 'Item itemtype is for loan.'); + is(ref($itemcalc->notforloan), $expecting, "When I look at " + ."availability calculation for item notforloan, then " + ."exception $expecting is given."); + }; + }; +}; + +subtest 'onloan' => \&t_onloan; +sub t_onloan { + plan tests => 2; + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + issue_item($item, $patron); + $item = Koha::Items->find($item->itemnumber); + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::CheckedOut'; + ok($item->onloan, 'When I look at the item, I see that it is ' + .'on loan.'); + is(ref($itemcalc->onloan), $expecting, "When I check availability " + ."calculation for item onloan, then exception $expecting is given."); +}; + +subtest 'restricted' => \&t_restricted; +sub t_restricted { + plan tests => 2; + + my $item = build_a_test_item()->set({restricted=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::Restricted'; + + is($item->restricted, 1, 'When I look at the item, I see that it is ' + .'restricted.'); + is(ref($itemcalc->restricted), $expecting, "When I check availability " + ."calculation for item restricted, then exception $expecting is given."); +}; + +subtest 'transfer' => \&t_transfer; +sub t_transfer { + plan tests => 4; + + my $item = build_a_test_item(); + my $item2 = build_a_test_item(); + my $transfer = Koha::Item::Transfer->new({ + itemnumber => $item->itemnumber, + datesent => '2000-12-12 12:12:12', + frombranch => $item->homebranch, + tobranch => $item2->homebranch, + comments => 'Very transfer', + })->store; + + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::Transfer'; + ok($transfer, 'Item is in transfer.'); + is(ref($itemcalc->transfer), $expecting, "When I check availability" + ." calculation for item transfer, then exception $expecting is given."); + $itemcalc = Koha::Availability::Checks::Item->new($item2); + ok(!Koha::Item::Transfers->find({ itemnumber => $item2->itemnumber}), + 'Another item is not in transfer.'); + ok(!$itemcalc->transfer, "When I check availability calculation for another" + ." item transfer, then no exception is given."); +}; + +subtest 'transfer_limit' => \&t_transfer_limit; +sub t_transfer_limit { + plan tests => 8; + + t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1); + t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype'); + + my $item = build_a_test_item(); + my $another_branch = Koha::Libraries->find( + $builder->build({ source => 'Branch' })->{'branchcode'}); + my $third_branch = Koha::Libraries->find( + $builder->build({ source => 'Branch' })->{'branchcode'}); + my $expecting = 'Koha::Exceptions::Item::CannotBeTransferred'; + + is(C4::Circulation::CreateBranchTransferLimit( + $another_branch->branchcode, + $item->holdingbranch, + $item->effective_itemtype + ), 1, 'There is a branch transfer limit for itemtype from ' + .$item->holdingbranch.' to '.$another_branch->branchcode .'.'); + my $itemcalc = Koha::Availability::Checks::Item->new($item); + + is(ref($itemcalc->transfer_limit($another_branch->branchcode)), $expecting, + "When I check availability calculation for transfer limit, then $expecting is given."); + ok(!$itemcalc->transfer_limit($third_branch->branchcode), + 'However, item can be transferred from' + .' a library to another library when there are no branch transfer limits.'); + + t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'ccode'); + ok(!$itemcalc->transfer_limit($another_branch->branchcode), + 'When we change limit to ccode, previous' + .' transfer limit is not in action anymore.'); + is(C4::Circulation::CreateBranchTransferLimit( + $another_branch->branchcode, + $item->holdingbranch, + $item->ccode + ), 1, 'Then we added a branch transfer limit by ccode.'); + is(ref($itemcalc->transfer_limit($another_branch->branchcode)), $expecting, + "When I check availability calculation for for transfer limit," + ." then $expecting is given."); + + t::lib::Mocks::mock_preference('UseBranchTransferLimits', 0); + is(C4::Context->preference('UseBranchTransferLimits'), 0, + 'Then, we switched UseBranchTransferLimits off.'); + ok(!$itemcalc->transfer_limit($another_branch->branchcode), + 'Then no exception is given anymore.'); +} + +subtest 'unknown_barcode' => \&t_unknown_barcode; +sub t_unknown_barcode { + plan tests => 2; + + my $item = build_a_test_item()->set({barcode=>undef})->store; + my $expecting = 'Koha::Exceptions::Item::UnknownBarcode'; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + + is($item->barcode, undef, 'When I look at the item, we see that it has ' + .'undefined barcode.'); + is(ref($itemcalc->unknown_barcode), $expecting, + "When I check availability calculation for for unknown barcode, " + ."then $expecting is given."); +}; + +subtest 'withdrawn' => \&t_withdrawn; +sub t_withdrawn { + plan tests => 2; + + my $item = build_a_test_item()->set({withdrawn=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::Withdrawn'; + + is($item->withdrawn, 1, 'When I look at the item, I see that it is ' + .'withdrawn.'); + is(ref($itemcalc->withdrawn), $expecting, "When I check availability" + ." calculation for item withdrawn, then exception $expecting is given."); +}; + +$schema->storage->txn_rollback; + +1; diff --git a/t/db_dependent/Koha/Availability/Checks/LibraryItemRule.t b/t/db_dependent/Koha/Availability/Checks/LibraryItemRule.t new file mode 100644 index 0000000..c74cb84 --- /dev/null +++ b/t/db_dependent/Koha/Availability/Checks/LibraryItemRule.t @@ -0,0 +1,312 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 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 => 3; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use Koha::Database; +use Koha::Items; + +use Koha::Availability::Checks::LibraryItemRule; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new; + +subtest 'Given CircControl is configured as ItemHomeLibrary' => sub { + plan tests => 3; + + subtest 'hold_not_allowed_by_library, item home library' => \&t_hold_not_allowed_item_home_library_item; + sub t_hold_not_allowed_item_home_library_item { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary'); + + my $item = build_a_test_item(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $item->homebranch, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from item home library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item + }); + my $expecting = 'Koha::Exceptions::Hold::NotAllowedByLibrary'; + + is(C4::Context->preference('CircControl'), 'ItemHomeLibrary', 'CircControl system preference' + .' is configured as ItemHomeLibrary.'); + is(ref($libitemcalc->hold_not_allowed_by_library), $expecting, + "When I check if hold is allowed by library item rules, exception $expecting is given."); + }; + + subtest 'hold_not_allowed_by_library, patron library' => \&t_hold_not_allowed_item_home_library_patron; + sub t_hold_not_allowed_item_home_library_patron { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary'); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $patron->branchcode, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from patron library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron, + }); + + is(C4::Context->preference('CircControl'), 'ItemHomeLibrary', 'CircControl system preference' + .' is configured as ItemHomeLibrary.'); + ok(!$libitemcalc->hold_not_allowed_by_library, + 'When I check if hold is allowed by library item rules, then no exception is given.'); + }; + + subtest 'hold_not_allowed_by_library, logged in library' => \&t_hold_not_allowed_item_home_library_logged_in; + sub t_hold_not_allowed_item_home_library_logged_in { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', undef, '', ''); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $branchcode, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from logged in library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron + }); + + is(C4::Context->preference('CircControl'), 'ItemHomeLibrary', 'CircControl system preference' + .' is configured as ItemHomeLibrary.'); + ok(!$libitemcalc->hold_not_allowed_by_library, + 'When I check if hold is allowed by library item rules, then no exception is given.'); + }; +}; + +subtest 'Given CircControl is configured as PatronLibrary' => sub { + plan tests => 3; + + subtest 'hold_not_allowed_by_library, item home library' => \&t_hold_not_allowed_patron_library_item; + sub t_hold_not_allowed_patron_library_item { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary'); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $item->homebranch, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from item home library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron + }); + + is(C4::Context->preference('CircControl'), 'PatronLibrary', 'CircControl system preference' + .' is configured as PatronLibrary.'); + ok(!$libitemcalc->hold_not_allowed_by_library, + 'When I check if hold is allowed by library item rules, then no exception is given.'); + }; + + subtest 'hold_not_allowed_by_library, patron library' => \&t_hold_not_allowed_patron_library_patron; + sub t_hold_not_allowed_patron_library_patron { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary'); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $patron->branchcode, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from patron library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron + }); + my $expecting = 'Koha::Exceptions::Hold::NotAllowedByLibrary'; + + is(C4::Context->preference('CircControl'), 'PatronLibrary', 'CircControl system preference' + .' is configured as PatronLibrary.'); + is(ref($libitemcalc->hold_not_allowed_by_library), $expecting, + "When I check if hold is allowed by library item rules, exception $expecting is given."); + }; + + subtest 'hold_not_allowed_by_library, logged in library' => \&t_hold_not_allowed_patron_library_logged_in; + sub t_hold_not_allowed_patron_library_logged_in { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', undef, '', ''); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $branchcode, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from logged in library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron + }); + + is(C4::Context->preference('CircControl'), 'PatronLibrary', 'CircControl system preference' + .' is configured as PatronLibrary.'); + ok(!$libitemcalc->hold_not_allowed_by_library, + 'When I check if hold is allowed by library item rules, then no exception is given.'); + }; +}; + +subtest 'Given CircControl is configured as PickupLibrary' => sub { + plan tests => 3; + + subtest 'hold_not_allowed_by_library, item home library' => \&t_hold_not_allowed_pickup_library_item; + sub t_hold_not_allowed_pickup_library_item { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', undef, '', ''); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $item->homebranch, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from item home library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron + }); + + is(C4::Context->preference('CircControl'), 'PickupLibrary', 'CircControl system preference' + .' is configured as PickupLibrary.'); + ok(!$libitemcalc->hold_not_allowed_by_library, + 'When I check if hold is allowed by library item rules, then no exception is given.'); + }; + + subtest 'hold_not_allowed_by_library, patron library' => \&t_hold_not_allowed_pickup_library_patron; + sub t_hold_not_allowed_pickup_library_patron { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', undef, '', ''); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $patron->branchcode, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from patron library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron, + }); + + is(C4::Context->preference('CircControl'), 'PickupLibrary', 'CircControl system preference' + .' is configured as PickupLibrary.'); + ok(!$libitemcalc->hold_not_allowed_by_library, + 'When I check if hold is allowed by library item rules, then no exception is given.'); + }; + subtest 'hold_not_allowed_by_library for patron library, CircControl = PickupLibrary' => \&t_hold_not_allowed_pickup_library_logged_in; + sub t_hold_not_allowed_pickup_library_logged_in { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', undef, '', ''); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $branchcode, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from logged in library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Hold::NotAllowedByLibrary'; + + is(C4::Context->preference('CircControl'), 'PickupLibrary', 'CircControl system preference' + .' is configured as PickupLibrary.'); + is(ref($libitemcalc->hold_not_allowed_by_library), $expecting, + "When I check if hold is allowed by library item rules, exception $expecting is given."); + }; +}; + +$schema->storage->txn_rollback; + +1; diff --git a/t/db_dependent/Koha/Availability/Checks/Patron.t b/t/db_dependent/Koha/Availability/Checks/Patron.t new file mode 100644 index 0000000..1e7db0a --- /dev/null +++ b/t/db_dependent/Koha/Availability/Checks/Patron.t @@ -0,0 +1,452 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 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 => 20; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use C4::Members; + +use Koha::Account::Lines; +use Koha::Biblioitems; +use Koha::Database; +use Koha::DateUtils; +use Koha::Items; + +use Koha::Availability::Checks::Patron; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +subtest 'debarred, given patron is not debarred' => \&t_not_debarred; +sub t_not_debarred { + plan tests => 3; + + my $patron = build_a_test_patron(); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $expecting = 'Koha::Exceptions::Patron::Debarred'; + + is($patron->debarred, undef, 'Patron is not debarred.'); + is($patron->debarredcomment, undef, 'Patron does not have debarment comment.'); + ok(!$patroncalc->debarred, 'When I check patron debarment, then no exception is given.'); +}; + +subtest 'debarred, given patron is debarred' => \&t_debarred; +sub t_debarred { + plan tests => 5; + + my $patron = build_a_test_patron(); + my $hundred_days = output_pref({ dt => dt_from_string()->add_duration( + DateTime::Duration->new(days => 100)), dateformat => 'iso', dateonly => 1 }); + $patron->set({ debarred => $hundred_days, debarredcomment => 'DONT EVER COME BACK' })->store; + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $expecting = 'Koha::Exceptions::Patron::Debarred'; + my $debarred = $patroncalc->debarred; + + is($patron->debarred, $hundred_days, "Patron is debarred until $hundred_days."); + is($patron->debarredcomment, 'DONT EVER COME BACK', 'Library does not want them to ever come back.'); + is(ref($debarred), $expecting, "When I check patron debarment, then $expecting is given."); + is($debarred->expiration, $patron->debarred, 'Then, from the status, I can see the expiration date.'); + is($debarred->comment, $patron->debarredcomment, 'Then, from the status, I can see that patron should never come back.'); +}; + +subtest 'debt_checkout, given patron has less than noissuescharge' => \&t_fines_checkout_less_than_max; +sub t_fines_checkout_less_than_max { + plan tests => 8; + + t::lib::Mocks::mock_preference('noissuescharge', 9001); + t::lib::Mocks::mock_preference('AllFinesNeedOverride', 0); + + my $patron = build_a_test_patron(); + my $line = Koha::Account::Line->new({ + borrowernumber => $patron->borrowernumber, + amountoutstanding => 9000, + })->store; + my ($outstanding) = C4::Members::GetMemberAccountRecords($patron->borrowernumber); + my $maxoutstanding = C4::Context->preference('noissuescharge'); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_checkout; + + is(C4::Context->preference('AllFinesNeedOverride'), 0, 'Not all fines need override.'); + ok($maxoutstanding, 'When I look at system preferences, I see that maximum allowed outstanding fines is set.'); + ok($maxoutstanding > $outstanding, 'When I check patron\'s balance, I found out they have less outstanding fines than maximum allowed.'); + ok(!$debt, 'When I check patron debt, then no exception is given.'); + t::lib::Mocks::mock_preference('AllFinesNeedOverride', 1); + is(C4::Context->preference('AllFinesNeedOverride'), 1, 'Given we configure' + .' all fines needing override.'); + $debt = $patroncalc->debt_checkout; + my $expecting = 'Koha::Exceptions::Patron::Debt'; + is(ref($debt), $expecting, "When I check patron debt, then $expecting is given."); + is($debt->max_outstanding, sprintf("%.2f", $maxoutstanding), 'Then I can see the status showing me how much' + .' outstanding total can be at maximum.'); + is($debt->current_outstanding, sprintf("%.2f", $outstanding), 'Then I can see the status showing me how much' + .' outstanding fines patron has right now.'); +}; + +subtest 'debt_checkout, given patron has more than noissuescharge' => \&t_fines_checkout_more_than_max; +sub t_fines_checkout_more_than_max { + plan tests => 5; + + t::lib::Mocks::mock_preference('noissuescharge', 5); + + my $patron = build_a_test_patron(); + my $line = Koha::Account::Line->new({ + borrowernumber => $patron->borrowernumber, + amountoutstanding => 9001, + })->store; + my ($outstanding) = C4::Members::GetMemberAccountRecords($patron->borrowernumber); + my $maxoutstanding = C4::Context->preference('noissuescharge'); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_checkout; + my $expecting = 'Koha::Exceptions::Patron::Debt'; + + ok($maxoutstanding, 'When I look at system preferences, I see that maximum allowed outstanding fines is set.'); + ok($maxoutstanding < $outstanding, 'When I check patron\'s balance, I found out they have more outstanding fines than allowed.'); + is(ref($debt), $expecting, "When I check patron debt, then $expecting is given."); + is($debt->max_outstanding, sprintf("%.2f", $maxoutstanding), 'Then I can see the status showing me how much' + .' outstanding total can be at maximum.'); + is($debt->current_outstanding, sprintf("%.2f", $outstanding), 'Then I can see the status showing me how much' + .' outstanding fines patron has right now.'); +}; + +subtest 'debt_hold, given patron has less than maxoutstanding' => \&t_fines_hold_less_than_max; +sub t_fines_hold_less_than_max { + plan tests => 3; + + t::lib::Mocks::mock_preference('maxoutstanding', 9001); + + my $patron = build_a_test_patron(); + my $line = Koha::Account::Line->new({ + borrowernumber => $patron->borrowernumber, + amountoutstanding => 9000, + })->store; + my ($outstanding) = C4::Members::GetMemberAccountRecords($patron->borrowernumber); + my $maxoutstanding = C4::Context->preference('maxoutstanding'); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_hold; + + ok($maxoutstanding, 'When I look at system preferences, I see that maximum allowed outstanding fines is set.'); + ok($maxoutstanding > $outstanding, 'When I check patron\'s balance, I found out they have less outstanding fines than maximum allowed.'); + ok(!$debt, 'When I check patron debt, then no exception is given.'); +}; + +subtest 'debt_hold, given patron has more than maxoutstanding' => \&t_fines_hold_more_than_max; +sub t_fines_hold_more_than_max { + plan tests => 5; + + t::lib::Mocks::mock_preference('maxoutstanding', 5); + + my $patron = build_a_test_patron(); + my $line = Koha::Account::Line->new({ + borrowernumber => $patron->borrowernumber, + amountoutstanding => 9001, + })->store; + my ($outstanding) = C4::Members::GetMemberAccountRecords($patron->borrowernumber); + my $maxoutstanding = C4::Context->preference('maxoutstanding'); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_hold; + my $expecting = 'Koha::Exceptions::Patron::Debt'; + + ok($maxoutstanding, 'When I look at system preferences, I see that maximum allowed outstanding fines is set.'); + ok($maxoutstanding < $outstanding, 'When I check patron\'s balance, I found out they have more outstanding fines than allowed.'); + is(ref($debt), $expecting, "When I check patron debt, then $expecting is given."); + is($debt->max_outstanding, sprintf("%.2f", $maxoutstanding), 'Then I can see the status showing me how much' + .' outstanding total can be at maximum.'); + is($debt->current_outstanding, sprintf("%.2f", $outstanding), 'Then I can see the status showing me how much' + .' outstanding fines patron has right now.'); +}; + +subtest 'debt_renew_opac, given patron has less than OPACFineNoRenewals' => \&t_fines_renew_opac_less_than_max; +sub t_fines_renew_opac_less_than_max { + plan tests => 3; + + t::lib::Mocks::mock_preference('OPACFineNoRenewals', 9001); + + my $patron = build_a_test_patron(); + my $line = Koha::Account::Line->new({ + borrowernumber => $patron->borrowernumber, + amountoutstanding => 9000, + })->store; + my ($outstanding) = C4::Members::GetMemberAccountRecords($patron->borrowernumber); + my $maxoutstanding = C4::Context->preference('OPACFineNoRenewals'); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_renew_opac; + + ok($maxoutstanding, 'When I look at system preferences, I see that maximum allowed outstanding fines is set.'); + ok($maxoutstanding > $outstanding, 'When I check patron\'s balance, I found out they have less outstanding fines than maximum allowed.'); + ok(!$debt, 'When I check patron debt, then no exception is given.'); +}; + +subtest 'debt_renew_opac, given patron has more than OPACFineNoRenewals' => \&t_fines_renew_opac_more_than_max; +sub t_fines_renew_opac_more_than_max { + plan tests => 5; + + t::lib::Mocks::mock_preference('OPACFineNoRenewals', 5); + + my $patron = build_a_test_patron(); + my $line = Koha::Account::Line->new({ + borrowernumber => $patron->borrowernumber, + amountoutstanding => 9001, + })->store; + my ($outstanding) = C4::Members::GetMemberAccountRecords($patron->borrowernumber); + my $maxoutstanding = C4::Context->preference('OPACFineNoRenewals'); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_renew_opac; + my $expecting = 'Koha::Exceptions::Patron::Debt'; + + ok($maxoutstanding, 'When I look at system preferences, I see that maximum allowed outstanding fines is set.'); + ok($maxoutstanding < $outstanding, 'When I check patron\'s balance, I found out they have more outstanding fines than allowed.'); + is(ref($debt), $expecting, "When I check patron debt, then $expecting is given."); + is($debt->max_outstanding, sprintf("%.2f", $maxoutstanding), 'Then I can see the status showing me how much' + .' outstanding total can be at maximum.'); + is($debt->current_outstanding, sprintf("%.2f", $outstanding), 'Then I can see the status showing me how much' + .' outstanding fines patron has right now.'); +}; + +subtest 'debt_checkout_guarantees, given patron\'s guarantees have less than NoIssuesChargeGuarantees' => \&t_guarantees_fines_checkout_less_than_max; +sub t_guarantees_fines_checkout_less_than_max { + plan tests => 3; + + t::lib::Mocks::mock_preference('NoIssuesChargeGuarantees', 5); + + my $patron = build_a_test_patron(); + my $guarantee1 = build_a_test_patron(); + $guarantee1->guarantorid($patron->borrowernumber)->store; + my $guarantee2 = build_a_test_patron(); + $guarantee2->guarantorid($patron->borrowernumber)->store; + my $line1 = Koha::Account::Line->new({ + borrowernumber => $guarantee1->borrowernumber, + amountoutstanding => 2, + })->store; + my $line2 = Koha::Account::Line->new({ + borrowernumber => $guarantee2->borrowernumber, + amountoutstanding => 2, + })->store; + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_checkout_guarantees; + + ok($line1, 'We have added very small fines to patron\'s guarantees.'); + ok($line1->amountoutstanding+$line2->amountoutstanding < C4::Context->preference('NoIssuesChargeGuarantees'), + 'These fines total is less than NoIssuesChargeGuarantees'); + ok(!$debt, 'When I check patron guarantees debt, then no exception is given.'); +}; + +subtest 'debt_checkout_guarantees, given patron\s guarantees have more than NoIssuesChargeGuarantees' => \&t_guarantees_fines_checkout_more_than_max; +sub t_guarantees_fines_checkout_more_than_max { + plan tests => 5; + + t::lib::Mocks::mock_preference('NoIssuesChargeGuarantees', 5); + + my $patron = build_a_test_patron(); + my $guarantee1 = build_a_test_patron(); + $guarantee1->guarantorid($patron->borrowernumber)->store; + my $guarantee2 = build_a_test_patron(); + $guarantee2->guarantorid($patron->borrowernumber)->store; + my $line1 = Koha::Account::Line->new({ + borrowernumber => $guarantee1->borrowernumber, + amountoutstanding => 3, + })->store; + my $line2 = Koha::Account::Line->new({ + borrowernumber => $guarantee2->borrowernumber, + amountoutstanding => 3, + })->store; + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_checkout_guarantees; + my $expecting = 'Koha::Exceptions::Patron::DebtGuarantees'; + + ok($line1, 'We have added very small fines to patron\'s guarantees.'); + ok($line1->amountoutstanding+$line2->amountoutstanding > C4::Context->preference('NoIssuesChargeGuarantees'), + 'These fines total is more than NoIssuesChargeGuarantees'); + is(ref($debt), $expecting, "When I check patron guarantees debt, then $expecting is given."); + is($debt->max_outstanding, "5.00", 'Then I can see the status showing me how much' + .' outstanding total can be at maximum.'); + is($debt->current_outstanding, "6.00", 'Then I can see the status showing me how much' + .' outstanding fines patron guarantees have right now.'); +}; + +subtest 'exceeded_maxreserves, given patron not exceeding max reserves' => \&t_exceeded_maxreserves_not; +sub t_exceeded_maxreserves_not { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $holds = Koha::Holds->search({ borrowernumber => $patron->borrowernumber }); + + is($holds->count, 0, 'Patron has no holds.'); + ok(!$patroncalc->exceeded_maxreserves, 'When I check if patron exceeded maxreserves, then no exception is given.'); +}; + +subtest 'exceeded_maxreserves, given patron exceeding max reserves' => \&t_exceeded_maxreserves; +sub t_exceeded_maxreserves { + plan tests => 5; + + t::lib::Mocks::mock_preference('maxreserves', 1); + + my $item = build_a_test_item(); + my $item2 = build_a_test_item(); + my $patron = build_a_test_patron(); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + add_biblio_level_hold($item, $patron, $patron->branchcode); + add_biblio_level_hold($item2, $patron, $patron->branchcode); + my $holds = Koha::Holds->search({ borrowernumber => $patron->borrowernumber }); + my $expecting = 'Koha::Exceptions::Hold::MaximumHoldsReached'; + my $exceeded = $patroncalc->exceeded_maxreserves; + + is(C4::Context->preference('maxreserves'), 1, 'Maximum number of holds allowed is one.'); + is($holds->count, 2, 'Patron has two holds.'); + is(ref($exceeded), $expecting, "When I check patron expiration, then $expecting is given."); + is($exceeded->max_holds_allowed, 1, 'Then I can see the status showing me how many' + .' holds are allowed at max.'); + is($exceeded->current_hold_count, 2, 'Then I can see the status showing me how many' + .' holds patron has right now.'); +}; + +subtest 'expired, given patron is not expired' => \&t_not_expired; +sub t_not_expired { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $dt_expiry = dt_from_string($patron->dateexpiry); + my $now = DateTime->now(time_zone => C4::Context->tz); + + is(DateTime->compare($dt_expiry, $now), 1, 'Patron is not expired.'); + ok(!$patroncalc->expired, 'When I check patron expiration, then no exception is given.'); +}; + +subtest 'expired, given patron is expired' => \&t_expired; +sub t_expired { + plan tests => 2; + + my $patron = build_a_test_patron(); + $patron->dateexpiry('1999-10-10')->store; + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $dt_expiry = dt_from_string($patron->dateexpiry); + my $now = DateTime->now(time_zone => C4::Context->tz); + my $expecting = 'Koha::Exceptions::Patron::CardExpired'; + + is(DateTime->compare($now, $dt_expiry), 1, 'Patron finds out their card is expired.'); + is(ref($patroncalc->expired), $expecting, "When I check patron expiration, then $expecting is given."); +}; + +subtest 'gonenoaddress, patron not gonenoaddress' => \&t_not_gonenoaddress; +sub t_not_gonenoaddress { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + + ok(!$patron->gonenoaddress, 'Patron is not gonenoaddress.'); + ok(!$patroncalc->gonenoaddress, 'When I check patron gonenoaddress, then no exception is given.'); +}; + +subtest 'gonenoaddress, patron gonenoaddress' => \&t_gonenoaddress; +sub t_gonenoaddress { + plan tests => 2; + + my $patron = build_a_test_patron(); + $patron->gonenoaddress('1')->store; + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $expecting = 'Koha::Exceptions::Patron::GoneNoAddress'; + + is($patron->gonenoaddress, 1, 'Patron is gonenoaddress.'); + is(ref($patroncalc->gonenoaddress), $expecting, "When I check patron gonenoaddress, then $expecting is given."); +}; + +subtest 'lost, patron card not lost' => \&t_not_lost; +sub t_not_lost { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + + ok(!$patron->lost, 'Patron card is not lost.'); + ok(!$patroncalc->lost, 'When I check if patron card is lost, then no exception is given.'); +}; + +subtest 'lost, patron card lost' => \&t_lost; +sub t_lost { + plan tests => 2; + + my $patron = build_a_test_patron(); + $patron->lost('1')->store; + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $expecting = 'Koha::Exceptions::Patron::CardLost'; + + is($patron->lost, 1, 'Patron card is lost.'); + is(ref($patroncalc->lost), $expecting, "When I check if patron card is lost, then $expecting is given."); +}; + +subtest 'from_another_library, patron from same branch than me' => \&t_from_another_library_same_branch; +sub t_from_another_library_same_branch { + plan tests => 2; + + t::lib::Mocks::mock_preference('IndependentBranches', 0); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', '', '', ''); + + my $patron = build_a_test_patron(); + $patron->branchcode($branchcode)->store; + + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + + is(C4::Context->userenv->{branch}, $patron->branchcode, 'Patron is from same branch as me.'); + ok(!$patroncalc->from_another_library, 'When I check if patron is' + .' from same branch as me, then no exception is given.'); +} + +subtest 'from_another_library, patron from different branch than me' => \&t_from_another_library; +sub t_from_another_library { + plan tests => 2; + + t::lib::Mocks::mock_preference('IndependentBranches', 1); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + my $branchcode2 = $builder->build({ source => 'Branch' })->{'branchcode'}; + + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', undef, '', ''); + + my $patron = build_a_test_patron(); + $patron->branchcode($branchcode2)->store; + + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $expecting = 'Koha::Exceptions::Patron::FromAnotherLibrary'; + + isnt(C4::Context->userenv->{branch}, $patron->branchcode, 'Patron is not from same branch as me.'); + is(ref($patroncalc->from_another_library), $expecting, "When I check if patron is" + ." from same branch as me, then $expecting is given."); +} + +$schema->storage->txn_rollback; + +1; diff --git a/t/db_dependent/Koha/Availability/Helpers.pm b/t/db_dependent/Koha/Availability/Helpers.pm new file mode 100644 index 0000000..12daa02 --- /dev/null +++ b/t/db_dependent/Koha/Availability/Helpers.pm @@ -0,0 +1,165 @@ +use t::lib::Mocks; +use t::lib::TestBuilder; + +use C4::Circulation; +use C4::Context; +use C4::Reserves; + +use Koha::Biblios; +use Koha::Biblioitems; +use Koha::DateUtils; +use Koha::IssuingRules; +use Koha::Items; +use Koha::Patrons; + +sub build_a_test_item { + my ($biblio, $biblioitem) = @_; + + my $builder = t::lib::TestBuilder->new; + my $branch_built = $builder->build({source => 'Branch'}); + my $itemtype_built = $builder->build({ + source => 'Itemtype', + value => { + notforloan => 0, + rentalcharge => 0, + } + }); + my $another_itemtype_built = $builder->build({ + source => 'Itemtype', + value => { + notforloan => 0, + rentalcharge => 0, + } + }); + my $bib = MARC::Record->new(); + my $title = 'Silence in the library'; + if( C4::Context->preference('marcflavour') eq 'UNIMARC' ) { + $bib->append_fields( + MARC::Field->new('600', '', '1', a => 'Moffat, Steven'), + MARC::Field->new('200', '', '', a => $title), + ); + } + else { + $bib->append_fields( + MARC::Field->new('100', '', '', a => 'Moffat, Steven'), + MARC::Field->new('245', '', '', a => $title), + ); + } + my ($bibnum, $bibitemnum) = C4::Biblio::AddBiblio($bib, ''); + $biblio ||= Koha::Biblios->find($bibnum); + $biblioitem ||= Koha::Biblioitems->find($bibitemnum); + $biblioitem->itemtype($another_itemtype_built->{'itemtype'})->store; + my $item = Koha::Items->find($builder->build({ + source => 'Item', + value => { + notforloan => 0, + damaged => 0, + biblioitemnumber => $biblioitem->biblioitemnumber, + biblionumber => $biblio->biblionumber, + itype => $itemtype_built->{'itemtype'}, + itemlost => 0, + withdrawn => 0, + onloan => undef, + restricted => 0, + homebranch => $branch_built->{'branchcode'}, + holdingbranch => $branch_built->{'branchcode'}, + } + })->{'itemnumber'}); + + return $item; +} + +sub build_a_test_patron { + my ($params) = @_; + + my $builder = t::lib::TestBuilder->new; + my $branch_built = $builder->build({ source => 'Branch' }); + my $cat_built = $builder->build({ source => 'Category' }); + return Koha::Patrons->find($builder->build({ + source => 'Borrower', + value => { + categorycode => $cat_built->{'categorycode'}, + branchcode => $branch_built->{'branchcode'}, + debarred => undef, + debarredcomment => undef, + lost => undef, + gonenoaddress => undef, + dateexpiry => output_pref({ dt => dt_from_string()->add_duration( # expires in 100 days + DateTime::Duration->new(days => 100)), dateformat => 'iso', dateonly => 1 }), + dateofbirth => '1950-10-10', + } + })->{'borrowernumber'}); +} + +sub set_default_circulation_rules { + my ($params) = @_; + + my $dbh = C4::Context->dbh; + + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => '*', + categorycode => '*', + maxissueqty => 3, + renewalsallowed => 1, + holds_per_record => 1, + reservesallowed => 1, + opacitemholds => 'Y', + })->store; + $dbh->do('DELETE FROM branch_item_rules'); + $dbh->do('DELETE FROM default_branch_circ_rules'); + $dbh->do('DELETE FROM default_branch_item_rules'); + $dbh->do('DELETE FROM default_circ_rules'); +} + +sub set_default_system_preferences { + t::lib::Mocks::mock_preference('item-level_itypes', 1); + t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 1); + t::lib::Mocks::mock_preference('ReservesControlBranch', 'ItemHomeLibrary'); + t::lib::Mocks::mock_preference('BlockExpiredPatronOpacActions', 0); + t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary'); + t::lib::Mocks::mock_preference('canreservefromotherbranches', 1); + t::lib::Mocks::mock_preference('IndependentBranches', 0); + t::lib::Mocks::mock_preference('HomeOrHoldingBranch', 'holdingbranch'); + t::lib::Mocks::mock_preference('maxoutstanding', 5); + t::lib::Mocks::mock_preference('AgeRestrictionMarker', 'PEGI'); + t::lib::Mocks::mock_preference('maxreserves', 50); + t::lib::Mocks::mock_preference('NoIssuesChargeGuarantees', 5); +} + +sub add_item_level_hold { + my ($item, $patron, $branch) = @_; + + return C4::Reserves::AddReserve( + $branch, + $patron->borrowernumber, + $item->biblionumber, '', + 1, undef, undef, undef, + undef, $item->itemnumber, + ); +} + +sub add_biblio_level_hold { + my ($item, $patron, $branch) = @_; + + return C4::Reserves::AddReserve( + $branch, + $patron->borrowernumber, + $item->biblionumber, '', + 1, + ); +} + +sub issue_item { + my ($item, $patron) = @_; + + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $patron->branchcode, 'Midway Public Library', '', '', ''); + return C4::Circulation::AddIssue( + $patron->unblessed, + $item->barcode + ); +} + +1; -- 1.9.1