Bugzilla – Attachment 60261 Details for
Bug 17712
Move availability calculation to the Koha namespace
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 17712: Centralize availability-related checks
Bug-17712-Centralize-availability-related-checks.patch (text/plain), 145.27 KB, created by
Lari Taskula
on 2017-02-15 16:18:45 UTC
(
hide
)
Description:
Bug 17712: Centralize availability-related checks
Filename:
MIME Type:
Creator:
Lari Taskula
Created:
2017-02-15 16:18:45 UTC
Size:
145.27 KB
patch
obsolete
>From 01335eacb021548a74aea74cc0c53816221be5b1 Mon Sep 17 00:00:00 2001 >From: Lari Taskula <lari.taskula@jns.fi> >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.pm | 57 +++ > Koha/Availability/Checks/Biblio.pm | 116 +++++ > Koha/Availability/Checks/Biblioitem.pm | 86 ++++ > Koha/Availability/Checks/Checkout.pm | 108 +++++ > Koha/Availability/Checks/IssuingRule.pm | 413 +++++++++++++++++ > Koha/Availability/Checks/Item.pm | 456 ++++++++++++++++++ > 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 | 144 ++++++ > .../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 +++++++ > 17 files changed, 3889 insertions(+) > create mode 100644 Koha/Availability/Checks.pm > 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.pm b/Koha/Availability/Checks.pm >new file mode 100644 >index 0000000..8892900 >--- /dev/null >+++ b/Koha/Availability/Checks.pm >@@ -0,0 +1,57 @@ >+package Koha::Availability::Checks; >+ >+# 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 <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use Koha::Exceptions; >+use Koha::Exceptions::Object; >+ >+sub new { >+ my $class = shift; >+ >+ my $self = {}; >+ bless $self, $class; >+ >+ return $self; >+} >+ >+sub AUTOLOAD { >+ my ($self, $params) = @_; >+ >+ my $method = our $AUTOLOAD; >+ $method =~ s/.*://; >+ >+ # Accessor for class parameters >+ if (exists $self->{$method}) { >+ unless (defined $params) { >+ return $self->{$method}; >+ } else { >+ $self->{$method} = $params; >+ return $self; >+ } >+ } elsif ($self->can($method)) { >+ $self->$method($params); >+ } else { >+ Koha::Exceptions::Object::MethodNotFound->throw( >+ "No method $method for " . ref($self) >+ ); >+ } >+} >+ >+1; >diff --git a/Koha/Availability/Checks/Biblio.pm b/Koha/Availability/Checks/Biblio.pm >new file mode 100644 >index 0000000..053a5d1 >--- /dev/null >+++ b/Koha/Availability/Checks/Biblio.pm >@@ -0,0 +1,116 @@ >+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 <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+ >+use base qw(Koha::Availability::Checks); >+ >+use C4::Context; >+use C4::Circulation; >+use C4::Serials; >+ >+use Koha::Biblios; >+use Koha::Checkouts; >+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 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 = Koha::Checkouts->search({ >+ borrowernumber => 0+$patron->borrowernumber, >+ biblionumber => 0+$biblio->biblionumber, >+ }, >+ { >+ join => 'item', >+ }); >+ if ($issues->count > 0) { >+ return Koha::Exceptions::Biblio::CheckedOut->new( >+ biblionumber => 0+$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->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 <http://www.gnu.org/licenses>. >+ >+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..cc4a047 >--- /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 <http://www.gnu.org/licenses>. >+ >+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 => $duedate->strftime('%F %T'), >+ now => $now->strftime('%F %T'), >+ ); >+ } >+ } else { >+ return Koha::Exceptions::Checkout::InvalidDueDate->new( >+ duedate => $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..4211055 >--- /dev/null >+++ b/Koha/Availability/Checks/IssuingRule.pm >@@ -0,0 +1,413 @@ >+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 <http://www.gnu.org/licenses>. >+ >+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; >+ } else { >+ $self->{'use_cache'} = 0; >+ } >+ >+ # 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 => 0+$toomany->{max_allowed}, >+ current_checkout_count => 0+$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 => 0+$self->patron->borrowernumber, >+ biblionumber => 0+$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 => 0+$rule->holds_per_record, >+ current_hold_count => 0+$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 = C4::Context->preference('ReservesControlBranch'); >+ 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 => 0+$rule->reservesallowed, >+ current_hold_count => 0+$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 => 0+$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..a70e3a3 >--- /dev/null >+++ b/Koha/Availability/Checks/Item.pm >@@ -0,0 +1,456 @@ >+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 <http://www.gnu.org/licenses>. >+ >+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::Holds; >+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; >+ $issue->{date_due} = dt_from_string($issue->{date_due}); >+ } >+ if ($issue) { >+ return Koha::Exceptions::Item::CheckedOut->new( >+ borrowernumber => 0+$issue->{borrowernumber}, >+ date_due => $issue->{date_due}->strftime('%F %T'), >+ ); >+ } >+ 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 => 0+$reserve->{'borrowernumber'}, >+ status => $s, >+ hold_queue_length => 0+Koha::Holds->search({ >+ itemnumber => $item->itemnumber })->count, >+ ); >+ } >+ } >+ 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 => 0+$patron->borrowernumber, >+ itemnumber => 0+$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 => 0+$check->{outstanding}, >+ duration => $check->{duration}, >+ returndate => $check->{due_date}->strftime('%F %T'), >+ ); >+ } >+ 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 => 0+$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 => 0+$item->notforloan, >+ code => $code, >+ ); >+ } elsif ($itemtype && $itemtype->notforloan > 0) { >+ return Koha::Exceptions::ItemType::NotForLoan->new( >+ status => 0+$itemtype->notforloan, >+ code => $code, >+ itemtype => $itemtype->itemtype, >+ ); >+ } elsif ($item->notforloan < 0) { >+ return Koha::Exceptions::Item::NotForLoan->new( >+ status => 0+$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<checked_out> 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 <http://www.gnu.org/licenses>. >+ >+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..68f7c55 >--- /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 <http://www.gnu.org/licenses>. >+ >+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 => 0+sprintf("%.2f", $maxoutstanding), >+ current_outstanding => 0+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 => 0+sprintf("%.2f", $max_charges), >+ current_outstanding => 0+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 => 0+$max, >+ current_hold_count => 0+$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 => 0+$number, >+ ); >+ } >+ return; >+} >+ >+sub _debt { >+ my ($self, $amount, $maxoutstanding) = @_; >+ if ($amount && $maxoutstanding && $amount > $maxoutstanding) { >+ return Koha::Exceptions::Patron::Debt->new( >+ max_outstanding => 0+sprintf("%.2f", $maxoutstanding), >+ current_outstanding => 0+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..27dd9ae >--- /dev/null >+++ b/t/db_dependent/Koha/Availability/Checks/Biblio.t >@@ -0,0 +1,144 @@ >+#!/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 <http://www.gnu.org/licenses>. >+ >+use Modern::Perl; >+use Test::More tests => 3; >+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 '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::CheckedOut'; >+ >+ 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 <http://www.gnu.org/licenses>. >+ >+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 <http://www.gnu.org/licenses>. >+ >+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..50bdc36 >--- /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 <http://www.gnu.org/licenses>. >+ >+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 => 2, >+ })->store; >+ >+ add_item_level_hold($item, $patron, $item->homebranch); >+ my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ >+ item => $item, >+ patron => $patron, >+ }); >+ is($rule->holds_per_record, 2, 'When I look at issuing rules, I see that' >+ .' holds_per_record 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_for_record_reached, '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 <http://www.gnu.org/licenses>. >+ >+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 <http://www.gnu.org/licenses>. >+ >+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..d0b7c16 >--- /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 <http://www.gnu.org/licenses>. >+ >+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, $maxoutstanding, 'Then I can see the status showing me how much' >+ .' outstanding total can be at maximum.'); >+ is($debt->current_outstanding, $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, $maxoutstanding, 'Then I can see the status showing me how much' >+ .' outstanding total can be at maximum.'); >+ is($debt->current_outstanding, $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, $maxoutstanding, 'Then I can see the status showing me how much' >+ .' outstanding total can be at maximum.'); >+ is($debt->current_outstanding, $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, $maxoutstanding, 'Then I can see the status showing me how much' >+ .' outstanding total can be at maximum.'); >+ is($debt->current_outstanding, $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, 'Then I can see the status showing me how much' >+ .' outstanding total can be at maximum.'); >+ is($debt->current_outstanding, 6, '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
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 17712
:
57921
|
57923
|
58446
|
58447
|
58449
|
58450
|
58451
|
58452
|
58453
|
58454
|
60147
|
60148
|
60149
|
60150
|
60151
|
60152
|
60153
|
60154
|
60261
|
60262
|
60263
|
60264
|
60265
|
62719
|
62720
|
62721
|
62722
|
62723
|
62724
|
62725
|
62726
|
63343
|
63344
|
63345
|
63346
|
63347
|
63348
|
63349
|
63350
|
84830
|
84831
|
84832
|
84833
|
84834
|
84835
|
84836
|
84837
|
84838
|
84839