@@ -, +, @@ biblio => $biblio, patron => $patron, to_branch => 'CPL', --- Koha/Availability/Checks/Biblio.pm | 144 ++++++ Koha/Availability/Checks/Biblioitem.pm | 86 ++++ Koha/Availability/Checks/Checkout.pm | 108 +++++ Koha/Availability/Checks/IssuingRule.pm | 411 +++++++++++++++++ Koha/Availability/Checks/Item.pm | 457 ++++++++++++++++++ Koha/Availability/Checks/LibraryItemRule.pm | 144 ++++++ Koha/Availability/Checks/Patron.pm | 323 +++++++++++++ Koha/Schema/Result/Reserve.pm | 22 + t/db_dependent/Koha/Availability/Checks/Biblio.t | 165 +++++++ .../Koha/Availability/Checks/Biblioitem.t | 83 ++++ t/db_dependent/Koha/Availability/Checks/Checkout.t | 113 +++++ .../Koha/Availability/Checks/IssuingRule.t | 513 +++++++++++++++++++++ t/db_dependent/Koha/Availability/Checks/Item.t | 382 +++++++++++++++ .../Koha/Availability/Checks/LibraryItemRule.t | 312 +++++++++++++ t/db_dependent/Koha/Availability/Checks/Patron.t | 452 ++++++++++++++++++ t/db_dependent/Koha/Availability/Helpers.pm | 165 +++++++ 16 files changed, 3880 insertions(+) create mode 100644 Koha/Availability/Checks/Biblio.pm create mode 100644 Koha/Availability/Checks/Biblioitem.pm create mode 100644 Koha/Availability/Checks/Checkout.pm create mode 100644 Koha/Availability/Checks/IssuingRule.pm create mode 100644 Koha/Availability/Checks/Item.pm create mode 100644 Koha/Availability/Checks/LibraryItemRule.pm create mode 100644 Koha/Availability/Checks/Patron.pm create mode 100644 t/db_dependent/Koha/Availability/Checks/Biblio.t create mode 100644 t/db_dependent/Koha/Availability/Checks/Biblioitem.t create mode 100644 t/db_dependent/Koha/Availability/Checks/Checkout.t create mode 100644 t/db_dependent/Koha/Availability/Checks/IssuingRule.t create mode 100644 t/db_dependent/Koha/Availability/Checks/Item.t create mode 100644 t/db_dependent/Koha/Availability/Checks/LibraryItemRule.t create mode 100644 t/db_dependent/Koha/Availability/Checks/Patron.t create mode 100644 t/db_dependent/Koha/Availability/Helpers.pm --- a/Koha/Availability/Checks/Biblio.pm +++ a/Koha/Availability/Checks/Biblio.pm @@ -0,0 +1,144 @@ +package Koha::Availability::Checks::Biblio; + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use base qw(Koha::Availability::Checks); + +use C4::Context; +use C4::Circulation; +use C4::Serials; + +use Koha::Biblios; +use Koha::Patrons; + +use Koha::Exceptions::Biblio; + +sub new { + my ($class, $biblio) = @_; + + unless ($biblio) { + Koha::Exceptions::MissingParameter->throw( + error => 'Class must be instantiated by providing a Koha::Biblio object.' + ); + } + unless (ref($biblio) eq 'Koha::Biblio') { + Koha::Exceptions::BadParameter->throw( + error => 'Biblio must be a Koha::Biblio object.' + ); + } + + my $self = { + biblio => $biblio, + }; + + bless $self, $class; +} + +=head3 another_item_checked_out + +Returns Koha::Exceptions::Biblio::AnotherItemCheckedOut if another item from +the same biblio is checked out. + +=cut + +sub another_item_checked_out { + my ($self, $patron) = @_; + + unless ($patron) { + Koha::Exceptions::MissingParameter->throw( + error => 'Patron related holdability checks require a patron. Not given.' + ); + } + unless (ref($patron) eq 'Koha::Patron') { + Koha::Exceptions::BadParameter->throw( + error => 'Patron must be a Koha::Patron object.' + ); + } + + my $issued = C4::Circulation::CheckIfIssuedToPatron( + $patron->borrowernumber, + $self->biblio->biblionumber, + ); + if ($issued) { + return Koha::Exceptions::Biblio::AnotherItemCheckedOut->new; + } + return; +} + +=head3 checked_out + +Returns Koha::Exceptions::Biblio::CheckedOut if biblio is checked out. + +=cut + +sub checked_out { + my ($self, $patron) = @_; + + my $biblio = $self->biblio; + my $issues = C4::Circulation::GetIssues({ + borrowernumber => $patron->borrowernumber, + biblionumber => $biblio->biblionumber, + }); + my @issues = $issues ? @$issues : (); + if (scalar @issues > 0) { + return Koha::Exceptions::Biblio::CheckedOut->new( + biblionumber => $biblio->biblionumber, + ); + } + return; +} + +=head3 forbid_holds_on_patrons_possessions + +Returns Koha::Exceptions::Biblio::AnotherItemCheckedOut if system preference +AllowHoldsOnPatronsPossessions is disabled and another item from the same biblio +is checked out. + +=cut + +sub forbid_holds_on_patrons_possessions { + my ($self, $patron) = @_; + + if (!C4::Context->preference('AllowHoldsOnPatronsPossessions')) { + return $self->another_item_checked_out($patron); + } + return; +} + +=head3 forbid_multiple_issues + +Returns Koha::Exceptions::Biblio::CheckedOut if system preference +AllowMultipleIssuesOnABiblio is disabled and an item from biblio is checked out. + +=cut + +sub forbid_multiple_issues { + my ($self, $patron) = @_; + + if (!C4::Context->preference('AllowMultipleIssuesOnABiblio')) { + my $biblionumber = $self->biblio->biblionumber; + unless (C4::Serials::CountSubscriptionFromBiblionumber($biblionumber)) { + return $self->checked_out($patron); + } + } + return; +} + +1; --- a/Koha/Availability/Checks/Biblioitem.pm +++ a/Koha/Availability/Checks/Biblioitem.pm @@ -0,0 +1,86 @@ +package Koha::Availability::Checks::Biblioitem; + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use base qw(Koha::Availability::Checks); + +use C4::Context; +use C4::Members; + +use Koha::Biblioitems; + +use Koha::Exceptions::Patron; + +sub new { + my ($class, $biblioitem) = @_; + + unless ($biblioitem) { + Koha::Exceptions::MissingParameter->throw( + error => 'Biblioitem related holdability checks require a biblioitem. Not given.' + ); + } + unless (ref($biblioitem) eq 'Koha::Biblioitem') { + Koha::Exceptions::BadParameter->throw( + error => 'Biblioitem must be a Koha::Biblioitem object.' + ); + } + + my $self = { + biblioitem => $biblioitem, + }; + + bless $self, $class; +} + +=head3 age_restricted + +Returns Koha::Exceptions::Patron::AgeRestricted if biblioitem age restriction +applies for patron. + +=cut + +sub age_restricted { + my ($self, $patron) = @_; + + unless ($patron) { + Koha::Exceptions::MissingParameter->throw( + error => 'Patron related holdability checks require a patron. Not given.' + ); + } + unless (ref($patron) eq 'Koha::Patron') { + Koha::Exceptions::BadParameter->throw( + error => 'Patron must be a Koha::Patron object.' + ); + } + + my $biblioitem = $self->biblioitem; + my $agerestriction = $biblioitem->agerestriction; + my ($restriction_age, $daysToAgeRestriction) = + C4::Circulation::GetAgeRestriction($agerestriction, $patron->unblessed); + my $restricted = $daysToAgeRestriction && $daysToAgeRestriction > 0 ? 1:0; + if ($restricted) { + return Koha::Exceptions::Patron::AgeRestricted->new( + age_restriction => $agerestriction, + ); + } + return; +} + +1; --- a/Koha/Availability/Checks/Checkout.pm +++ a/Koha/Availability/Checks/Checkout.pm @@ -0,0 +1,108 @@ +package Koha::Availability::Checks::Checkout; + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use base qw(Koha::Availability::Checks); + +use C4::Circulation; +use C4::Context; + +use Koha::DateUtils; +use Koha::Items; + +use Koha::Exceptions::Biblio; +use Koha::Exceptions::Checkout; + +sub new { + my ($class) = @_; + + my $self = {}; + + bless $self, $class; +} + +=head3 invalid_due_date + +Returns Koha::Exceptions::Checkout::InvalidDueDate if given due date is invalid. + +Returns Koha::Exceptions::Checkout::DueDateBeforeNow if given due date is in the +past. + +=cut + +sub invalid_due_date { + my ($self, $item, $patron, $duedate) = @_; + + if ($duedate && ref $duedate ne 'DateTime') { + eval { $duedate = dt_from_string($duedate); }; + if ($@) { + return Koha::Exceptions::Checkout::InvalidDueDate->new( + duedate => $duedate, + ); + } + } + + my $now = DateTime->now(time_zone => C4::Context->tz()); + unless ($duedate) { + my $issuedate = $now->clone(); + + my $branch = C4::Circulation::_GetCircControlBranch($item, $patron); + $duedate = C4::Circulation::CalcDateDue + ( + $issuedate, $item->effective_itemtype, $branch, $patron->unblessed + ); + } + if ($duedate) { + my $today = $now->clone->truncate(to => 'minute'); + if (DateTime->compare($duedate,$today) == -1 ) { + # duedate cannot be before now + return Koha::Exceptions::Checkout::DueDateBeforeNow->new( + duedate => output_pref($duedate), + now => output_pref($now), + ); + } + } else { + return Koha::Exceptions::Checkout::InvalidDueDate->new( + duedate => output_pref($duedate), + ); + } + return; +} + +=head3 no_more_renewals + +Returns Koha::Exceptions::Checkout::NoMoreRenewals if no more renewals are +allowed for given checkout. + +=cut + +sub no_more_renewals { + my ($self, $issue) = @_; + + return unless $issue; + my ($status) = C4::Circulation::CanBookBeRenewed($issue->borrowernumber, + $issue->itemnumber); + if ($status == 0) { + return Koha::Exceptions::Checkout::NoMoreRenewals->new; + } + return; +} + +1; --- a/Koha/Availability/Checks/IssuingRule.pm +++ a/Koha/Availability/Checks/IssuingRule.pm @@ -0,0 +1,411 @@ +package Koha::Availability::Checks::IssuingRule; + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use base qw(Koha::Availability::Checks); + +use C4::Circulation; +use C4::Context; +use C4::Reserves; + +use Koha::IssuingRules; +use Koha::Items; + +use Koha::Exceptions; +use Koha::Exceptions::Checkout; +use Koha::Exceptions::Hold; + +=head3 new + +OPTIONAL PARAMETERS: + +* categorycode Attempts to match issuing rule with given categorycode +* rule_itemtype Attempts to match issuing rule with given itemtype +* branchcode Attempts to match issuing rule with given branchcode + +* patron Stores patron into the object for reusability. Also + attempts to match issuing rule with item's itemtype + unless specificed with "rule_itemtype" parameter. +* item stores item into the object for reusability. Also + Attempts to match issuing rule with patron's categorycode + unless specified with "categorycode" parameter. +* biblioitem Attempts to match issuing rule with itemtype from given + biblioitem as a fallback + +Stores the effective issuing rule into class variable effective_issuing_rule +for reusability. This means the methods in this class are performed on the +stored effective issuing rule and multiple queries into issuingrules table +are avoided. + +Caches issuing rule momentarily to help performance in biblio availability +calculation. This is helpful because a biblio may have multiple items matching +the same issuing rule and this lets us avoid multiple, unneccessary queries into +the issuingrule table. + +=cut + +sub new { + my $class = shift; + my ($params) = @_; + + my $self = $class->SUPER::new(@_); + + my $categorycode = $params->{'categorycode'}; + my $rule_itemtype = $params->{'rule_itemtype'}; + my $branchcode = $params->{'branchcode'}; + + my $patron = $self->_validate_parameter($params, + 'patron', 'Koha::Patron'); + my $item = $self->_validate_parameter($params, + 'item', 'Koha::Item'); + my $biblioitem = $self->_validate_parameter($params, + 'biblioitem', 'Koha::Biblioitem'); + + unless ($rule_itemtype) { + $rule_itemtype = $item + ? $item->effective_itemtype + : $biblioitem + ? $biblioitem->itemtype + : undef; + } + unless ($categorycode) { + $categorycode = $patron ? $patron->categorycode : undef; + } + if ($params->{'use_cache'}) { + $self->{'use_cache'} = 1; + } + + # Get a matching issuing rule + my $rule; + my $cache; + if ($self->use_cache) { + $cache = Koha::Caches->get_instance('availability'); + my $cached = $cache->get_from_cache('issuingrule-.' + .($categorycode?$categorycode:'*').'-' + .($rule_itemtype?$rule_itemtype:'*').'-' + .($branchcode?$branchcode:'*')); + if ($cached) { + $rule = Koha::IssuingRule->new->set($cached); + } + } + + unless ($rule) { + $rule = Koha::IssuingRules->get_effective_issuing_rule({ + categorycode => $categorycode, + itemtype => $rule_itemtype, + branchcode => $branchcode, + }); + if ($rule && $self->use_cache) { + $cache->set_in_cache('issuingrule-.' + .($categorycode?$categorycode:'*').'-' + .($rule_itemtype?$rule_itemtype:'*').'-' + .($branchcode?$branchcode:'*'), + $rule->unblessed, { expiry => 10 }); + } + } + + # Store effective issuing rule into object + $self->{'effective_issuing_rule'} = $rule; + # Store patron into object + $self->{'patron'} = $patron; + # Store item into object + $self->{'item'} = $item; + + bless $self, $class; +} + +=head3 maximum_checkouts_reached + +Returns Koha::Exceptions::Checkout::MaximumCheckoutsReached if maximum number +of checkouts have been reached by patron. + +=cut + +sub maximum_checkouts_reached { + my ($self, $item, $patron) = @_; + + return unless $patron ||= $self->patron; + return unless $item ||= $self->item; + + my $item_unblessed = $item->unblessed; + $item_unblessed->{'itemtype'} = $item->effective_itemtype; + my $toomany = C4::Circulation::TooMany( + $patron->unblessed, + $item->biblionumber, + $item_unblessed, + ); + + if ($toomany) { + return Koha::Exceptions::Checkout::MaximumCheckoutsReached->new( + error => $toomany->{reason}, + max_checkouts_allowed => $toomany->{max_allowed}, + current_checkout_count => $toomany->{count}, + ); + } + return; +} + +=head3 maximum_holds_for_record_reached + +Returns Koha::Exceptions::Hold::MaximumHoldsForRecordReached if maximum number +of holds on biblio have been reached by patron. + +Returns Koha::Exceptions::Hold::ZeroHoldsAllowed if no holds are allowed at all. + +OPTIONAL PARAMETERS: +nonfound_holds Allows you to pass Koha::Holds object to improve performance + by avoiding another query to reserves table. + (Found holds don't count against a patron's holds limit) +biblionumber Allows you to specify biblionumber; if not given, item's + biblionumber will be used (recommended; but requires you to + provide item while instantiating this class). + +=cut + +sub maximum_holds_for_record_reached { + my ($self, $params) = @_; + + return unless my $rule = $self->effective_issuing_rule; + return unless my $item = $self->item; + + my $biblionumber = $params->{'biblionumber'} ? $params->{'biblionumber'} + : $item->biblionumber; + if ($rule->holds_per_record > 0 && $rule->reservesallowed > 0) { + my $holds_on_this_record; + unless (exists $params->{'nonfound_holds'}) { + $holds_on_this_record = Koha::Holds->search({ + borrowernumber => $self->patron->borrowernumber, + biblionumber => $biblionumber, + found => undef, + })->count; + } else { + $holds_on_this_record = @{$params->{'nonfound_holds'}}; + } + if ($holds_on_this_record >= $rule->holds_per_record) { + return Koha::Exceptions::Hold::MaximumHoldsForRecordReached->new( + max_holds_allowed => $rule->holds_per_record, + current_hold_count => $holds_on_this_record, + ); + } + } else { + return $self->zero_holds; + } + return; +} + +=head3 maximum_holds_reached + +Returns Koha::Exceptions::Hold::MaximumHoldsReached if maximum number +of holds have been reached by patron. + +Returns Koha::Exceptions::Hold::ZeroHoldsAllowed if no holds are allowed at all. + +=cut + +sub maximum_holds_reached { + my ($self) = @_; + + return unless my $rule = $self->effective_issuing_rule; + my $rule_itemtype = $rule->itemtype; + my $controlbranch = $self->controlbranch; + if ($rule->holds_per_record > 0 && $rule->reservesallowed > 0) { + # Get patron's hold count for holds that match the found issuing rule + my $hold_count = $self->_patron_hold_count($rule_itemtype, $controlbranch); + if ($hold_count >= $rule->reservesallowed) { + return Koha::Exceptions::Hold::MaximumHoldsReached->new( + max_holds_allowed => $rule->reservesallowed, + current_hold_count => $hold_count, + ); + } + } else { + return $self->zero_holds; + } + return; +} + +=head3 on_shelf_holds_forbidden + +Returns Koha::Exceptions::Hold::OnShelfNotAllowed if effective issuing rule +restricts on-shelf holds. + +=cut + +sub on_shelf_holds_forbidden { + my ($self) = @_; + + return unless my $rule = $self->effective_issuing_rule; + return unless my $item = $self->item; + my $on_shelf_holds = $rule->onshelfholds; + + if ($on_shelf_holds == 0) { + my $hold_waiting = Koha::Holds->search({ + found => 'W', + itemnumber => $item->itemnumber, + priority => 0 + })->count; + if (!$item->onloan && !$hold_waiting) { + return Koha::Exceptions::Hold::OnShelfNotAllowed->new; + } + return; + } elsif ($on_shelf_holds == 1) { + return; + } elsif ($on_shelf_holds == 2) { + my @items = Koha::Items->search({ biblionumber => $item->biblionumber }); + + my $any_available = 0; + + foreach my $i (@items) { + unless ($i->itemlost + || $i->notforloan > 0 + || $i->withdrawn + || $i->onloan + || C4::Reserves::IsItemOnHoldAndFound( $i->id ) + || ( $i->damaged + && !C4::Context->preference('AllowHoldsOnDamagedItems') ) + || Koha::ItemTypes->find( $i->effective_itemtype() )->notforloan) { + $any_available = 1; + last; + } + } + return Koha::Exceptions::Hold::OnShelfNotAllowed->new if $any_available; + } + return; +} + +=head3 opac_item_level_hold_forbidden + +Returns Koha::Exceptions::Hold::ItemLevelHoldNotAllowed if item-level holds are +forbidden in OPAC. + +=cut + +sub opac_item_level_hold_forbidden { + my ($self) = @_; + + return unless my $rule = $self->effective_issuing_rule; + if (defined $rule->opacitemholds && $rule->opacitemholds eq 'N') { + return Koha::Exceptions::Hold::ItemLevelHoldNotAllowed->new; + } + return; +} + +=head3 zero_checkouts_allowed + +Returns Koha::Exceptions::Checkout::ZeroCheckoutsAllowed if checkouts are not +allowed at all. + +=cut + +sub zero_checkouts_allowed { + my ($self) = @_; + + return unless my $rule = $self->effective_issuing_rule; + if (defined $rule->maxissueqty && $rule->maxissueqty == 0) { + return Koha::Exceptions::Checkout::ZeroCheckoutsAllowed->new; + } + return; +} + +=head3 zero_holds_allowed + +Returns Koha::Exceptions::Hold::ZeroHoldsAllowed if holds are not +allowed at all. + +This will inspect both "reservesallowed" and "holds_per_record" value in effective +issuing rule. + +=cut + +sub zero_holds_allowed { + my ($self) = @_; + + return unless my $rule = $self->effective_issuing_rule; + if (defined $rule->reservesallowed && $rule->reservesallowed == 0 + || defined $rule->holds_per_record && $rule->holds_per_record == 0) { + return Koha::Exceptions::Hold::ZeroHoldsAllowed->new; + } + return; +} + +sub _patron_hold_count { + my ($self, $itemtype, $controlbranch) = @_; + + $itemtype ||= '*'; + my $branchcode; + my $branchfield = 'me.branchcode'; + $controlbranch ||= C4::Context->preference('ReservesControlBranch'); + my $patron = $self->patron; + if ($self->patron && $controlbranch eq 'PatronLibrary') { + $branchfield = 'borrower.branchcode'; + $branchcode = $patron->branchcode; + } elsif ($self->item && $controlbranch eq 'ItemHomeLibrary') { + $branchfield = 'item.homebranch'; + $branchcode = $self->item->homebranch; + } + + my $cache; + if ($self->use_cache) { + $cache = Koha::Caches->get_instance('availability'); + my $cached = $cache->get_from_cache('holds_of_'.$patron->borrowernumber.'-' + .$itemtype.'-'.$branchcode); + if (defined $cached) { + return $cached; + } + } + + my $holds = Koha::Holds->search({ + 'me.borrowernumber' => $patron->borrowernumber, + $branchfield => $branchcode, + '-and' => [ + '-or' => [ + $itemtype ne '*' && C4::Context->preference('item-level_itypes') == 1 ? [ + { 'item.itype' => $itemtype }, + { 'biblioitem.itemtype' => $itemtype } + ] : [ { 'biblioitem.itemtype' => $itemtype } ] + ] + ]}, { + join => ['borrower', 'biblioitem', 'item'], + '+select' => [ 'borrower.branchcode', 'item.homebranch' ], + '+as' => ['borrower.branchcode', 'item.homebranch' ] + })->count; + + if ($self->use_cache) { + $cache->set_in_cache('holds_of_'.$patron->borrowernumber.'-' + .$itemtype.'-'.$branchcode, $holds, { expiry => 10 }); + } + + return $holds; +} + +sub _validate_parameter { + my ($self, $params, $key, $ref) = @_; + + if (exists $params->{$key}) { + if (ref($params->{$key}) eq $ref) { + return $params->{$key}; + } else { + Koha::Exceptions::BadParameter->throw( + "Parameter $key must be a $ref object." + ); + } + } +} + +1; --- a/Koha/Availability/Checks/Item.pm +++ a/Koha/Availability/Checks/Item.pm @@ -0,0 +1,457 @@ +package Koha::Availability::Checks::Item; + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use base qw(Koha::Availability::Checks); + +use C4::Circulation; +use C4::Context; +use C4::Reserves; + +use Koha::AuthorisedValues; +use Koha::DateUtils; +use Koha::ItemTypes; +use Koha::Items; +use Koha::Item::Transfers; + +use Koha::Exceptions::Item; +use Koha::Exceptions::ItemType; + +sub new { + my ($class, $item) = @_; + + unless ($item) { + Koha::Exceptions::MissingParameter->throw( + error => 'Class must be instantiated by providing a Koha::Item object.' + ); + } + unless (ref($item) eq 'Koha::Item') { + Koha::Exceptions::BadParameter->throw( + error => 'Item must be a Koha::Item object.' + ); + } + + my $self = { + item => $item, + }; + + bless $self, $class; +} + +=head3 checked_out + +Returns Koha::Exceptions::Item::CheckedOut if item is checked out. + +=cut + +sub checked_out { + my ($self, $issue) = @_; + + $issue ||= C4::Circulation::GetItemIssue($self->item->itemnumber); + if (ref($issue) eq 'Koha::Checkout') { + $issue = $issue->unblessed; + } + if ($issue) { + return Koha::Exceptions::Item::CheckedOut->new( + borrowernumber => $issue->{borrowernumber}, + date_due => $issue->{date_due}, + ); + } + return; +} + +=head3 checked_out + +Returns Koha::Exceptions::Checkout::Fee if checking out an item will cause +a checkout fee. + +Koha::Exceptions::Checkout::Fee additional fields: + amount # defines the amount of checkout fee + +=cut + +sub checkout_fee { + my ($self, $patron) = @_; + + my ($rentalCharge) = C4::Circulation::GetIssuingCharges + ( + $self->item->itemnumber, + $patron ? $patron->borrowernumber : undef + ); + if ($rentalCharge > 0){ + return Koha::Exceptions::Checkout::Fee->new( + amount => sprintf("%.02f", $rentalCharge), + ); + } + return; +} + +=head3 damaged + +Returns Koha::Exceptions::Item::Damaged if item is damaged and holds are not +allowed on damaged items. + +=cut + +sub damaged { + my ($self) = @_; + + if ($self->item->damaged + && !C4::Context->preference('AllowHoldsOnDamagedItems')) { + return Koha::Exceptions::Item::Damaged->new; + } + return; +} + +=head3 from_another_library + +Returns Koha::Exceptions::Item::FromAnotherLibrary if IndependentBranches is on, +and item is from another branch than the user currently logged in. + +Koha::Exceptions::Item::FromAnotherLibrary additional fields: + from_library # item's library (according to HomeOrHoldingBranch) + current_library # the library of logged-in user + +=cut + +sub from_another_library { + my ($self) = @_; + + my $item = $self->item; + if (C4::Context->preference("IndependentBranches")) { + return unless my $userenv = C4::Context->userenv; + unless (C4::Context->IsSuperLibrarian()) { + my $homeorholding = C4::Context->preference("HomeOrHoldingBranch"); + if ($userenv->{branch} && $item->$homeorholding ne $userenv->{branch}){ + return Koha::Exceptions::Item::FromAnotherLibrary->new( + from_library => $item->$homeorholding, + current_library => $userenv->{branch}, + ); + } + } + } + return; +} + +=head3 held + +Returns Koha::Exceptions::Item::Held item is held. + +Koha::Exceptions::Item::Held additional fields: + borrowernumber # item's library (according to HomeOrHoldingBranch) + status # the library of logged-in user + hold_queue_length # hold queue length for the item + +=cut + +sub held { + my ($self) = @_; + + my $item = $self->item; + if (my ($s, $reserve) = C4::Reserves::CheckReserves($item->itemnumber)) { + if ($reserve) { + return Koha::Exceptions::Item::Held->new( + borrowernumber => $reserve->{'borrowernumber'}, + status => $s, + hold_queue_length => $item->hold_queue_length, + ); + } + } + return; +} + +=head3 held_by_patron + +Returns Koha::Exceptions::Item::AlreadyHeldForThisPatron if item is already +held by given patron. + +OPTIONAL PARAMETERS +holds # list of Koha::Hold objects to inspect the item's held-status from. + # If not given, a query is made for selecting the holds from database. + # Useful in optimizing biblio-level availability by selecting all holds + # of a biblio and then passing it for this function instead of querying + # reserves table multiple times for each item. + +=cut + +sub held_by_patron { + my ($self, $patron, $params) = @_; + + my $item = $self->item; + my $holds; + if (!exists $params->{'holds'}) { + $holds = Koha::Holds->search({ + borrowernumber => $patron->borrowernumber, + itemnumber => $item->itemnumber, + })->count(); + } else { + foreach my $hold (@{$params->{'holds'}}) { + if ($hold->itemnumber == $item->itemnumber) { + $holds++; + } + } + } + if ($holds) { + return Koha::Exceptions::Item::AlreadyHeldForThisPatron->new + } + return; +} + +=head3 high_hold + +Returns Koha::Exceptions::Item::HighHolds if item is a high-held item and +decreaseLoanHighHolds is enabled. + +=cut + +sub high_hold { + my ($self, $patron) = @_; + + return unless C4::Context->preference('decreaseLoanHighHolds'); + + my $item = $self->item; + my $check = C4::Circulation::checkHighHolds( + $item->unblessed, + $patron->unblessed + ); + + if ($check->{exceeded}) { + return Koha::Exceptions::Item::HighHolds->new( + num_holds => $check->{outstanding}, + duration => $check->{duration}, + returndate => output_pref({ + dt => $check->{due_date}, + dateformat => 'iso', + dateonly => 1 + }), + ); + } + return; +} + +=head3 lost + +Returns Koha::Exceptions::Item::Lost if item is lost. + +=cut + +sub lost { + my ($self) = @_; + + my $item = $self->item; + if ($self->item->itemlost) { + my $av = Koha::AuthorisedValues->search({ + category => 'LOST', + authorised_value => $item->itemlost + }); + my $code = $av->count ? $av->next->lib : ''; + return Koha::Exceptions::Item::Lost->new( + status => $item->itemlost, + code => $code, + ); + } + return; +} + +=head3 notforloan + +Returns Koha::Exceptions::Item::NotForLoan if item is not for loan, and +additionally Koha::Exceptions::ItemType::NotForLoan if itemtype is not for loan. + +=cut + +sub notforloan { + my ($self) = @_; + + my $item = $self->item; + my $cache = Koha::Caches->get_instance('availability'); + my $cached = $cache->get_from_cache('itemtype-'.$item->effective_itemtype); + my $itemtype; + if ($cached) { + $itemtype = Koha::ItemType->new->set($cached); + } else { + $itemtype = Koha::ItemTypes->find($item->effective_itemtype); + $cache->set_in_cache('itemtype-'.$item->effective_itemtype, + $itemtype->unblessed, { expiry => 10 }) if $itemtype; + } + + if ($item->notforloan != 0 || $itemtype && $itemtype->notforloan != 0) { + my $av = Koha::AuthorisedValues->search({ + category => 'NOT_LOAN', + authorised_value => $item->notforloan + }); + my $code = $av->count ? $av->next->lib : ''; + if ($item->notforloan > 0) { + return Koha::Exceptions::Item::NotForLoan->new( + status => $item->notforloan, + code => $code, + ); + } elsif ($itemtype && $itemtype->notforloan > 0) { + return Koha::Exceptions::ItemType::NotForLoan->new( + status => $itemtype->notforloan, + code => $code, + itemtype => $itemtype->itemtype, + ); + } elsif ($item->notforloan < 0) { + return Koha::Exceptions::Item::NotForLoan->new( + status => $item->notforloan, + code => $code, + ); + } + } + return; +} + +=head3 onloan + +Returns Koha::Exceptions::Item::CheckedOut if item is onloan. + +This does not query issues table, but simply checks item's onloan-column. + +=cut + +sub onloan { + my ($self) = @_; + + # This simply checks item's onloan-column to determine item's checked out + # -status. Use C to perform an actual query for checkouts. + if ($self->item->onloan) { + return Koha::Exceptions::Item::CheckedOut->new; + } +} + +=head3 restricted + +Returns Koha::Exceptions::Item::Restricted if item is restricted. + +=cut + +sub restricted { + my ($self) = @_; + + if ($self->item->restricted) { + return Koha::Exceptions::Item::Restricted->new; + } + return; +} + +=head3 transfer + +Returns Koha::Exceptions::Item::Transfer if item is in transfer. + +Koha::Exceptions::Item::Transfer additional fields: + from_library + to_library + datesent + +=cut + +sub transfer { + my ($self) = @_; + + my $transfer = Koha::Item::Transfers->find({ + itemnumber => $self->item->itemnumber, + datesent => { '!=', undef }, + }); + if ($transfer) { + return Koha::Exceptions::Item::Transfer->new( + from_library => $transfer->frombranch, + to_library => $transfer->tobranch, + datesent => $transfer->datesent, + ); + } + return; +} + +=head3 transfer_limit + +Returns Koha::Exceptions::Item::CannotBeTransferred a transfer limit applies +for item. + +Koha::Exceptions::Item::CannotBeTransferred additional parameters: + from_library + to_library + +=cut + +sub transfer_limit { + my ($self, $to_branch) = @_; + + return unless C4::Context->preference('UseBranchTransferLimits'); + my $item = $self->item; + my $limit_type = C4::Context->preference('BranchTransferLimitsType'); + my $code; + if ($limit_type eq 'itemtype') { + $code = $item->effective_itemtype; + } elsif ($limit_type eq 'ccode') { + $code = $item->ccode; + } else { + Koha::Exceptions::BadParameter->throw( + error => 'System preference BranchTransferLimitsType has an' + .' unrecognized value.' + ); + } + + my $allowed = C4::Circulation::IsBranchTransferAllowed( + $to_branch, + $item->holdingbranch, + $code + ); + if (!$allowed) { + return Koha::Exceptions::Item::CannotBeTransferred->new( + from_library => $item->holdingbranch, + to_library => $to_branch, + ); + } + return; +} + +=head3 unknown_barcode + +Returns Koha::Exceptions::Item::UnknownBarcode if item has no barcode. + +=cut + +sub unknown_barcode { + my ($self) = @_; + + my $item = $self->item; + unless ($item->barcode) { + return Koha::Exceptions::Item::UnknownBarcode->new; + } + return; +} + +=head3 withdrawn + +Returns Koha::Exceptions::Item::Withdrawn if item is withdrawn. + +=cut + +sub withdrawn { + my ($self) = @_; + + if ($self->item->withdrawn) { + return Koha::Exceptions::Item::Withdrawn->new; + } + return; +} + +1; --- a/Koha/Availability/Checks/LibraryItemRule.pm +++ a/Koha/Availability/Checks/LibraryItemRule.pm @@ -0,0 +1,144 @@ +package Koha::Availability::Checks::LibraryItemRule; + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use base qw(Koha::Availability::Checks); + +use C4::Circulation; + +use Koha::Exceptions::Hold; + +=head3 new + +With given parameters item (Koha::Item) and patron (Koha::Patron), selects the +effective library item rule and stores it into this object. Further calls to +methods of this object will use this library item rule. + +PARAMETERS: +item # Koha::Item object +patron # Koha::Patron object + +=cut + +sub new { + my $class = shift; + my ($params) = @_; + + my $self = $class->SUPER::new(@_); + + my $independentBranch = C4::Context->preference('IndependentBranches'); + my $circcontrol = C4::Context->preference('CircControl'); + my $patron = $self->{'patron'} = $self->_validate_parameter($params, + 'patron', 'Koha::Patron'); + my $item = $self->{'item'} = $self->_validate_parameter($params, + 'item', 'Koha::Item'); + + my $circ_control_branch; + my $branchitemrule; + if ($circcontrol eq 'ItemHomeLibrary' && $item) { + $circ_control_branch = C4::Circulation::_GetCircControlBranch( + $item->unblessed, undef); + } elsif ($circcontrol eq 'PatronLibrary' && $patron) { + $circ_control_branch = C4::Circulation::_GetCircControlBranch( + undef, $patron->unblessed); + } elsif ($circcontrol eq 'PickupLibrary') { + $circ_control_branch = C4::Circulation::_GetCircControlBranch( + undef, undef); + } else { + bless $self, $class; + return $self; + } + + $self->{'branchitemrule'} = C4::Circulation::GetBranchItemRule( + $circ_control_branch, $item->effective_itemtype); + + bless $self, $class; +} + +=head3 hold_not_allowed_by_library + +Returns Koha::Exceptions::Hold::NotAllowedByLibrary if library item rules does +not allow holds. + +=cut + +sub hold_not_allowed_by_library { + my ($self) = @_; + + return unless my $rule = $self->branchitemrule; + + if (!$rule->{holdallowed}) { + return Koha::Exceptions::Hold::NotAllowedByLibrary->new + } + return; +} + +=head3 hold_not_allowed_by_library + +Returns Koha::Exceptions::Hold::NotAllowedFromOtherLibraries if library item rules +define restrictions for holds on items that are from another library than patron. + +=cut + +sub hold_not_allowed_by_other_library { + my ($self) = @_; + + return unless my $rule = $self->branchitemrule; + return unless my $item = $self->item; + + my $fromotherbranches = C4::Context->preference('canreservefromotherbranches'); + my $independentBranch = C4::Context->preference('IndependentBranches'); + + my $patron = $self->patron; + if ($rule->{holdallowed} == 1) { + if (!$patron) { + # Since we don't know who is asking for item and from which + # library, return NotAllowedFromOtherLibraries, but it should + # be set only as an additional availability note + return Koha::Exceptions::Hold::NotAllowedFromOtherLibraries->new; + } elsif ($patron && $patron->branchcode ne $item->homebranch) { + return Koha::Exceptions::Hold::NotAllowedFromOtherLibraries->new; + } + } elsif ($independentBranch && !$fromotherbranches) { + if (!$patron) { + # This should be stored as an additional note + return Koha::Exceptions::Hold::NotAllowedFromOtherLibraries->new + } elsif ($patron && $item->homebranch ne $patron->branchcode) { + return Koha::Exceptions::Hold::NotAllowedFromOtherLibraries->new + } + } + return; +} + +sub _validate_parameter { + my ($self, $params, $key, $ref) = @_; + + if (exists $params->{$key}) { + if (ref($params->{$key}) eq $ref) { + return $params->{$key}; + } else { + Koha::Exceptions::BadParameter->throw( + "Parameter $key must be a $ref object." + ); + } + } +} + +1; --- a/Koha/Availability/Checks/Patron.pm +++ a/Koha/Availability/Checks/Patron.pm @@ -0,0 +1,323 @@ +package Koha::Availability::Checks::Patron; + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use base qw(Koha::Availability::Checks); + +use Scalar::Util qw(looks_like_number); + +use C4::Context; +use C4::Members; + +use Koha::Exceptions::Hold; +use Koha::Exceptions::Patron; + +sub new { + my ($class, $patron) = @_; + + unless ($patron) { + Koha::Exceptions::MissingParameter->throw( + error => 'Patron related holdability checks require a patron. Not given.' + ); + } + unless (ref($patron) eq 'Koha::Patron') { + Koha::Exceptions::BadParameter->throw( + error => 'Patron must be a Koha::Patron object.' + ); + } + + my $self = { + patron => $patron, + }; + + bless $self, $class; +} + +=head3 debarred + +Returns Koha::Exceptions::Patron::Debarred if patron is debarred. + +Koha::Exceptions::Patron::Debarred additional fields: + expiration # expiration date of debarment + comment # comment for this debarment + +=cut + +sub debarred { + my ($self) = @_; + + my $patron = $self->patron; + if ($patron->is_debarred) { + return Koha::Exceptions::Patron::Debarred->new( + expiration => $patron->debarred, + comment => $patron->debarredcomment, + ); + } + return; +} + +=head3 debt_checkout + +Returns Koha::Exceptions::Patron::Debt if patron has outstanding fines that +exceed the amount defined in "noissuescharge" system preference. + +Koha::Exceptions::Patron::Debt additional fields: + max_outstanding # maximum allowed amount of outstanding fines + current_outstanding # current amount of outstanding fines + +=cut + +sub debt_checkout { + my ($self) = @_; + + my ($amount) = C4::Members::GetMemberAccountRecords( + $self->patron->borrowernumber + ); + my $maxoutstanding = C4::Context->preference("noissuescharge"); + if (C4::Context->preference('AllFinesNeedOverride') && $amount > 0) { + # All fines need override, so return Koha::Exceptions::Patron::Debt. + return Koha::Exceptions::Patron::Debt->new( + max_outstanding => sprintf("%.2f", $maxoutstanding), + current_outstanding => sprintf("%.2f", $amount), + ); + } else { + return $self->_debt($amount, $maxoutstanding); + } +} + +=head3 debt_checkout_guarantees + +Returns Koha::Exceptions::Patron::DebtGuarantees if patron's guarantees are +exceeding the amount defined in "NoIssuesChargeGuarantees" system preference. + +Koha::Exceptions::Patron::DebtGuarantees additional fields: + max_outstanding # maximum allowed amount of outstanding fines + current_outstanding # current amount of outstanding fines + +=cut + +sub debt_checkout_guarantees { + my ($self) = @_; + + my $patron = $self->patron; + my $max_charges = C4::Context->preference("NoIssuesChargeGuarantees"); + $max_charges = undef unless looks_like_number($max_charges); + my @guarantees = $patron->guarantees; + return unless scalar(@guarantees); + my $guarantees_non_issues_charges; + foreach my $g (@guarantees) { + my ($b, $n, $o) = C4::Members::GetMemberAccountBalance($g->id); + $guarantees_non_issues_charges += $n; + } + if ($guarantees_non_issues_charges > $max_charges) { + return Koha::Exceptions::Patron::DebtGuarantees->new( + max_outstanding => sprintf("%.2f", $max_charges), + current_outstanding => sprintf("%.2f", $guarantees_non_issues_charges), + ); + } + return; +} + +=head3 debt_hold + +Returns Koha::Exceptions::Patron::Debt if patron has outstanding fines that +exceed the amount defined in "maxoutstanding" system preference. + +Koha::Exceptions::Patron::Debt additional fields: + max_outstanding # maximum allowed amount of outstanding fines + current_outstanding # current amount of outstanding fines + +=cut + +sub debt_hold { + my ($self) = @_; + + my ($amount) = C4::Members::GetMemberAccountRecords( + $self->patron->borrowernumber + ); + my $maxoutstanding = C4::Context->preference("maxoutstanding"); + return $self->_debt($amount, $maxoutstanding); +} + +=head3 debt_checkout + +Returns Koha::Exceptions::Patron::Debt if patron has outstanding fines that +exceed the amount defined in "OPACFineNoRenewals" system preference. + +Koha::Exceptions::Patron::Debt additional fields: + max_outstanding # maximum allowed amount of outstanding fines + current_outstanding # current amount of outstanding fines + +=cut + +sub debt_renew_opac { + my ($self) = @_; + + my ($amount) = C4::Members::GetMemberAccountRecords( + $self->patron->borrowernumber + ); + my $maxoutstanding = C4::Context->preference("OPACFineNoRenewals"); + return $self->_debt($amount, $maxoutstanding); +} + +=head3 exceeded_maxreserves + +Returns Koha::Exceptions::Hold::MaximumHoldsReached if the total amount of +patron's holds exceeds the amount defined in "maxreserves" system preference. + +Koha::Exceptions::Hold::MaximumHoldsReached additional fields: + max_holds_allowed # maximum amount of allowed holds (maxreserves preference) + current_hold_count # total amount of patron's current (non-found) holds + +=cut + +sub exceeded_maxreserves { + my ($self) = @_; + + my $patron = $self->patron; + my $max = C4::Context->preference('maxreserves'); + my $holds = Koha::Holds->search({ + borrowernumber => $patron->borrowernumber, + found => undef, + })->count(); + if ($holds && $max && $holds >= $max) { + return Koha::Exceptions::Hold::MaximumHoldsReached->new( + max_holds_allowed => $max, + current_hold_count => $holds, + ); + } + return; +} + +=head3 expired + +Returns Koha::Exceptions::Patron::CardExpired if patron's card has been expired. + +Koha::Exceptions::Patron::CardExpired additional fields: + expiration_date + +=cut + +sub expired { + my ($self) = @_; + + my $patron = $self->patron; + if ($patron->is_expired) { + return Koha::Exceptions::Patron::CardExpired->new( + expiration_date => $patron->dateexpiry + ); + } + return; +} + +=head3 from_another_library + +Returns Koha::Exceptions::Patron::FromAnotherLibrary if patron is from another +library than currently logged-in user. System preference "IndependentBranches" +is considered for this method. + +Koha::Exceptions::Patron::FromAnotherLibrary additional fields: + patron_branch # patron's library + current_branch # library of currently logged-in user + +=cut + +sub from_another_library { + my ($self) = @_; + + my $patron = $self->patron; + if (C4::Context->preference("IndependentBranches")) { + my $userenv = C4::Context->userenv; + unless (C4::Context->IsSuperLibrarian()) { + if ($patron->branchcode ne $userenv->{branch}) { + return Koha::Exceptions::Patron::FromAnotherLibrary->new( + patron_branch => $patron->branchcode, + current_branch => $userenv->{branch}, + ); + } + } + } + return; +} + +=head3 gonenoaddress + +Returns Koha::Exceptions::Patron::GoneNoAddress if patron is gonenoaddress. + +=cut + +sub gonenoaddress { + my ($self) = @_; + + if ($self->patron->gonenoaddress) { + return Koha::Exceptions::Patron::GoneNoAddress->new; + } + return; +} + +=head3 lost + +Returns Koha::Exceptions::Patron::CardLost if patron's card is marked as lost. + +=cut + +sub lost { + my ($self) = @_; + + if ($self->patron->lost) { + return Koha::Exceptions::Patron::CardLost->new; + } + return; +} + +=head3 overdue_checkouts + +Returns Koha::Exceptions::Patron::DebarredOverdue if patron is debarred because +they have overdue checkouts. + +Koha::Exceptions::Patron::DebarredOverdue additional fields: + number_of_overdues + +=cut + +sub overdue_checkouts { + my ($self) = @_; + + my $number; + if ($number = $self->patron->has_overdues) { + return Koha::Exceptions::Patron::DebarredOverdue->new( + number_of_overdues => $number, + ); + } + return; +} + +sub _debt { + my ($self, $amount, $maxoutstanding) = @_; + if ($amount && $maxoutstanding && $amount > $maxoutstanding) { + return Koha::Exceptions::Patron::Debt->new( + max_outstanding => sprintf("%.2f", $maxoutstanding), + current_outstanding => sprintf("%.2f", $amount), + ); + } + return; +} + +1; --- a/Koha/Schema/Result/Reserve.pm +++ a/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; --- a/t/db_dependent/Koha/Availability/Checks/Biblio.t +++ a/t/db_dependent/Koha/Availability/Checks/Biblio.t @@ -0,0 +1,165 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Test::More tests => 4; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use C4::Members; + +use Koha::Biblios; +use Koha::Database; +use Koha::Items; + +use Koha::Availability::Checks::Biblio; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +subtest 'another_item_checked_out' => \&t_another_item_checked_out; +sub t_another_item_checked_out { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $biblio = Koha::Biblios->find($item->biblionumber); + my $item2 = build_a_test_item(); + $item2->set({ + biblionumber => $item->biblionumber, + biblioitemnumber => $item->biblioitemnumber, + })->store; + issue_item($item, $patron); + my $bibcalc = Koha::Availability::Checks::Biblio->new($biblio); + my $expecting = 'Koha::Exceptions::Biblio::AnotherItemCheckedOut'; + + is(Koha::Checkouts->search({ itemnumber => $item->itemnumber })->count, 1, + 'I found out that item is checked out.'); + is(ref($bibcalc->another_item_checked_out($patron)), $expecting, $expecting); +}; + +subtest 'checked_out' => \&t_checked_out; +sub t_checked_out { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $biblio = Koha::Biblios->find($item->biblionumber); + my $item2 = build_a_test_item(); + $item2->set({ + biblionumber => $item->biblionumber, + biblioitemnumber => $item->biblioitemnumber, + })->store; + issue_item($item, $patron); + my $bibcalc = Koha::Availability::Checks::Biblio->new($biblio); + my $expecting = 'Koha::Exceptions::Biblio::CheckedOut'; + + is(Koha::Checkouts->search({ itemnumber => $item->itemnumber })->count, 1, + 'I found out that item is checked out.'); + is(ref($bibcalc->checked_out($patron)), $expecting, $expecting); +}; + +subtest 'forbid_holds_on_patrons_possessions' => \&t_forbid_holds_on_patrons_possessions; +sub t_forbid_holds_on_patrons_possessions { + plan tests => 5; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $biblio = Koha::Biblios->find($item->biblionumber); + my $item2 = build_a_test_item(); + $item2->set({ + biblionumber => $item->biblionumber, + biblioitemnumber => $item->biblioitemnumber, + })->store; + issue_item($item, $patron); + my $bibcalc = Koha::Availability::Checks::Biblio->new($biblio); + my $expecting = 'Koha::Exceptions::Biblio::AnotherItemCheckedOut'; + + t::lib::Mocks::mock_preference('AllowHoldsOnPatronsPossessions', 0); + is(C4::Context->preference('AllowHoldsOnPatronsPossessions'), 0, + 'System preference AllowHoldsOnPatronsPossessions is disabled.'); + is(Koha::Checkouts->search({ itemnumber => $item->itemnumber })->count, 1, + 'I found out that item is checked out.'); + is(ref($bibcalc->forbid_holds_on_patrons_possessions($patron)), $expecting, + $expecting); + t::lib::Mocks::mock_preference('AllowHoldsOnPatronsPossessions', 1); + is(C4::Context->preference('AllowHoldsOnPatronsPossessions'), 1, + 'Given system preference AllowHoldsOnPatronsPossessions is enabled,'); + ok(!$bibcalc->forbid_holds_on_patrons_possessions($patron), 'When I check' + .' biblio forbid holds on patrons possesions, no exception is given.'); +}; + +subtest 'forbid_multiple_issues' => \&t_forbid_multiple_issues; +sub t_forbid_multiple_issues { + plan tests => 4; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + my $biblio = Koha::Biblios->find($item->biblionumber); + my $item2 = build_a_test_item(); + $item2->set({ + biblionumber => $item->biblionumber, + biblioitemnumber => $item->biblioitemnumber, + })->store; + issue_item($item2, $patron); + my $checkoutcalc = Koha::Availability::Checks::Biblio->new($biblio); + + is($item->biblionumber, $item2->biblionumber, 'Item one and item two belong' + .' to the same biblio.'); + is(Koha::Checkouts->find({ itemnumber => $item2->itemnumber})->borrowernumber, + $patron->borrowernumber, 'Item two seems to be issued to me.'); + + subtest 'Given AllowMultipleIssuesOnABiblio is enabled' => sub { + plan tests => 2; + + t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 1); + is(C4::Context->preference('AllowMultipleIssuesOnABiblio'), 1, + 'System preference AllowMultipleIssuesOnABiblio is enabled.'); + is($checkoutcalc->forbid_multiple_issues($patron), undef, + 'When I ask if there is another biblio already checked out,' + .' then no exception is returned.'); + }; + + subtest 'Given AllowMultipleIssuesOnABiblio is disabled' => sub { + plan tests => 3; + + my $expecting = 'Koha::Exceptions::Biblio::CheckedOut'; + my $returned; + + t::lib::Mocks::mock_preference('AllowMultipleIssuesOnABiblio', 0); + is(C4::Context->preference('AllowMultipleIssuesOnABiblio'), 0, + 'System preference AllowMultipleIssuesOnABiblio is disabled.'); + ok($returned = ref($checkoutcalc->forbid_multiple_issues($patron)), + 'When I ask if there is another biblio already checked out,'); + is ($returned, $expecting, "then exception $expecting is returned."); + }; +}; + +$schema->storage->txn_rollback; + +1; --- a/t/db_dependent/Koha/Availability/Checks/Biblioitem.t +++ a/t/db_dependent/Koha/Availability/Checks/Biblioitem.t @@ -0,0 +1,83 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Test::More tests => 2; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use Koha::Biblioitems; +use Koha::Database; +use Koha::DateUtils; +use Koha::Items; + +use Koha::Availability::Checks::Biblioitem; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +subtest 'age_restricted, given patron is old enough' => \&t_old_enough; +sub t_old_enough { + plan tests => 3; + + my $item = build_a_test_item(); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + $biblioitem->set({ agerestriction => 'PEGI 18' })->store; + my $patron = build_a_test_patron(); + my $bibitemcalc = Koha::Availability::Checks::Biblioitem->new($biblioitem); + + is($patron->dateofbirth, '1950-10-10', 'Patron says he is born in the 1950s.'); + is($biblioitem->agerestriction, 'PEGI 18', 'Item is restricted for under 18.'); + ok(!$bibitemcalc->age_restricted($patron), 'When I check for age restriction, then no exception is given.'); +}; + +subtest 'age_restricted, given patron is too young' => \&t_too_young; +sub t_too_young { + plan tests => 4; + + my $item = build_a_test_item(); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + $biblioitem->set({ agerestriction => 'PEGI 18' })->store; + my $patron = build_a_test_patron(); + my $now = output_pref({ dt => dt_from_string, dateformat => 'iso', dateonly => 1 }); + $patron->set({ dateofbirth => $now })->store; + my $bibitemcalc = Koha::Availability::Checks::Biblioitem->new($biblioitem); + my $expecting = 'Koha::Exceptions::Patron::AgeRestricted'; + + is($patron->dateofbirth, $now, 'Patron says he is born today.'); + my $reason = $bibitemcalc->age_restricted($patron); + is($biblioitem->agerestriction, 'PEGI 18', 'Biblio item is restricted for under 18.'); + is(ref($reason), $expecting, + "When I check for age restriction, then exception $expecting is given."); + is($reason->age_restriction, 'PEGI 18', 'The reason also specifies the' + .' age restriction, PEGI 18.'); +}; + +$schema->storage->txn_rollback; + +1; --- a/t/db_dependent/Koha/Availability/Checks/Checkout.t +++ a/t/db_dependent/Koha/Availability/Checks/Checkout.t @@ -0,0 +1,113 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Test::More tests => 4; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use C4::Members; + +use Koha::Biblios; +use Koha::Database; +use Koha::Items; + +use Koha::Availability::Checks::Checkout; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +subtest 'invalid_due_date, invalid due date' => \&t_invalid_due_date; +sub t_invalid_due_date { + plan tests => 1; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + + my $duedate = 'invalid'; + my $checkoutcalc = Koha::Availability::Checks::Checkout->new; + my $expecting = 'Koha::Exceptions::Checkout::InvalidDueDate'; + is(ref($checkoutcalc->invalid_due_date($item, $patron, $duedate)), $expecting, + "When using duedate 'invalid', then exception $expecting is given."); +}; + +subtest 'invalid_due_date, due date before now' => \&t_invalid_due_date_before_now; +sub t_invalid_due_date_before_now { + plan tests => 1; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + + my $before = output_pref({ dt => dt_from_string()->subtract_duration( + DateTime::Duration->new(days => 100)), dateformat => 'iso', dateonly => 1 }); + my $checkoutcalc = Koha::Availability::Checks::Checkout->new; + my $expecting = 'Koha::Exceptions::Checkout::DueDateBeforeNow'; + is(ref($checkoutcalc->invalid_due_date($item, $patron, $before)), $expecting, + "When using duedate that is in the past, then exception $expecting is given."); +}; + +subtest 'invalid_due_date, good due date' => \&t_invalid_due_date_not_invalid; +sub t_invalid_due_date_not_invalid { + plan tests => 1; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + + my $before = output_pref({ dt => dt_from_string()->add_duration( + DateTime::Duration->new(days => 14)), dateformat => 'iso', dateonly => 1 }); + my $checkoutcalc = Koha::Availability::Checks::Checkout->new; + is($checkoutcalc->invalid_due_date($item, $patron, $before), undef, + 'When using a duedate that is in two weeks from now, then no exception is given.'); +}; + +subtest 'no_more_renewals' => \&t_no_more_renewals; +sub t_no_more_renewals { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + + issue_item($item, $patron); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => '*', + categorycode => '*', + renewalsallowed => 0, + })->store; + my $checkoutcalc = Koha::Availability::Checks::Checkout->new; + my $expecting = 'Koha::Exceptions::Checkout::NoMoreRenewals'; + my $issue = Koha::Checkouts->find({ itemnumber => $item->itemnumber}); + is($issue->borrowernumber, $patron->borrowernumber, 'Item seems to be issued to me.'); + is(ref($checkoutcalc->no_more_renewals($issue)), $expecting, + "When checking for no more renewals, then exception $expecting is given."); +}; + +$schema->storage->txn_rollback; + +1; --- a/t/db_dependent/Koha/Availability/Checks/IssuingRule.t +++ a/t/db_dependent/Koha/Availability/Checks/IssuingRule.t @@ -0,0 +1,513 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Test::More tests => 13; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use Koha::Database; +use Koha::IssuingRules; +use Koha::Items; +use Koha::ItemTypes; + +use Koha::Availability::Checks::IssuingRule; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $builder = t::lib::TestBuilder->new; + +subtest 'maximum_checkouts_reached' => \&t_maximum_checkouts_reached; +sub t_maximum_checkouts_reached { + plan tests => 5; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + maxissueqty => 1, + })->store; + + issue_item($item, $patron); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Checkout::MaximumCheckoutsReached'; + my $max_checkouts_reached = $issuingcalc->maximum_checkouts_reached; + is($rule->maxissueqty, 1, 'When I look at issuing rules, I see that' + .' maxissueqty is 1 for this itemtype.'); + is(Koha::Checkouts->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one checkout.'); + is(ref($max_checkouts_reached), $expecting, "When I ask if" + ." maximum checkouts is reached, exception $expecting is given."); + is($max_checkouts_reached->max_checkouts_allowed, $rule->maxissueqty, + 'Exception says max checkouts allowed is same as maxissueqty.'); + is($max_checkouts_reached->current_checkout_count, 1, + 'Exception says I currently have one checkout.'); +}; + +subtest 'maximum_checkouts_reached, not reached' => \&t_maximum_checkouts_not_reached; +sub t_maximum_checkouts_not_reached { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + maxissueqty => 2, + })->store; + + issue_item($item, $patron); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + + is($rule->maxissueqty, 2, 'When I look at issuing rules, I see that' + .' maxissueqty is 2 for this itemtype.'); + is(Koha::Checkouts->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one checkout.'); + ok(!$issuingcalc->maximum_checkouts_reached, 'When I ask if' + .' maximum checkouts is reached, no exception is given.'); +}; + +subtest 'maximum_holds_reached' => \&t_maximum_holds_reached; +sub t_maximum_holds_reached { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + reservesallowed => 1, + holds_per_record => 2, + })->store; + + add_item_level_hold($item, $patron, $item->homebranch); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Hold::MaximumHoldsReached'; + + is($rule->reservesallowed, 1, 'When I look at issuing rules, I see that' + .' reservesallowed is 1 for this itemtype.'); + is(Koha::Holds->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one hold.'); + is(ref($issuingcalc->maximum_holds_reached), $expecting, "When I ask if" + ." maximum holds is reached, exception $expecting is given."); +}; + +subtest 'maximum_holds_reached, not reached' => \&t_maximum_holds_not_reached; +sub t_maximum_holds_not_reached { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + reservesallowed => 2, + })->store; + + add_item_level_hold($item, $patron, $patron->branchcode); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Hold::MaximumHoldsReached'; + is($rule->reservesallowed, 2, 'When I look at issuing rules, I see that' + .' reservesallowed is 2 for this itemtype.'); + is(Koha::Holds->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one hold.'); + ok(!$issuingcalc->maximum_holds_reached, 'When I ask if' + .' maximum holds is reached, no exception is given.'); +}; + +subtest 'maximum_holds_for_record_reached' => \&t_maximum_holds_for_record_reached; +sub t_maximum_holds_for_record_reached { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + reservesallowed => 2, + holds_per_record => 1, + })->store; + + add_item_level_hold($item, $patron, $item->homebranch); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Hold::MaximumHoldsForRecordReached'; + + is($rule->holds_per_record, 1, 'When I look at issuing rules, I see that' + .' holds_per_record is 1 for this itemtype.'); + is(Koha::Holds->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one hold.'); + is(ref($issuingcalc->maximum_holds_for_record_reached), $expecting, "When I ask if" + ." maximum holds for record is reached, exception $expecting is given."); +}; + +subtest 'maximum_holds_for_record_reached, not reached' => \&t_maximum_holds_for_record_not_reached; +sub t_maximum_holds_for_record_not_reached { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + reservesallowed => 2, + holds_per_record => 1, + })->store; + + add_item_level_hold($item, $patron, $patron->branchcode); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + is($rule->holds_per_record, 1, 'When I look at issuing rules, I see that' + .' holds_per_record is 1 for this itemtype.'); + is(Koha::Holds->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one hold.'); + ok(!$issuingcalc->maximum_holds_for_recordreached, 'When I ask if' + .' maximum holds for record is reached, no exception is given.'); +}; + +subtest 'on_shelf_holds_forbidden while on shelf holds are allowed' => \&t_on_shelf_holds_forbidden; +sub t_on_shelf_holds_forbidden { + plan tests => 2; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $item2 = build_a_test_item(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + holds_per_record => 1, + reservesallowed => 1, + onshelfholds => 1, + })->store; + + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + is($rule->onshelfholds, 1, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed.'); + ok(!$issuingcalc->on_shelf_holds_forbidden, 'When I check availability calculation' + .' to see if on shelf holds are forbidden, then no exception is given.'); +}; + +subtest 'on_shelf_holds_forbidden if any available' => \&t_on_shelf_holds_forbidden_any_available; +sub t_on_shelf_holds_forbidden_any_available { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + my $biblio = Koha::Biblios->find($item->biblionumber); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + my $item2 = build_a_test_item($biblio, $biblioitem); + $item2->itype($item->itype)->store; + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + holds_per_record => 1, + reservesallowed => 1, + onshelfholds => 2, + })->store; + + my $expecting = 'Koha::Exceptions::Hold::OnShelfNotAllowed'; + subtest 'While there are available items' => sub { + plan tests => 4; + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + is($rule->onshelfholds, 2, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed if all are unavailable.'); + ok(!$item->onloan && !$item2->onloan, 'Both items are available.'); + ok($issuingcalc->on_shelf_holds_forbidden, 'When I check ' + .'availability calculation to see if on shelf holds are forbidden,'); + is(ref($issuingcalc->on_shelf_holds_forbidden), $expecting, 'Then' + ." exception $expecting is given."); + }; + subtest 'While one of two items is available' => sub { + plan tests => 7; + is($rule->onshelfholds, 2, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed if all are unavailable.'); + ok(issue_item($item, $patron), 'We have issued one of two items.'); + my $issuingcalc; + ok($issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item + }), 'First, we will check on shelf hold restrictions for the item that is' + .' unavailable.'); + ok($issuingcalc->on_shelf_holds_forbidden, 'When I check ' + .'availability calculation to see if on shelf holds are forbidden,'); + is(ref($issuingcalc->on_shelf_holds_forbidden), $expecting, 'Then' + ." exception $expecting is given."); + ok($issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item2 + }), 'Then, we will check on shelf hold restrictions for the item that is' + .' available.'); + is(ref($issuingcalc->on_shelf_holds_forbidden), $expecting, 'When I check ' + .'availability calculation to see if on shelf holds are forbidden, then' + ." exception $expecting is given."); + }; + subtest 'While all are unavailable' => sub { + plan tests => 3; + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + is($rule->onshelfholds, 2, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed if all are unavailable.'); + ok(issue_item($item2, $patron), 'Both items are now unavailable.'); + ok(!$issuingcalc->on_shelf_holds_forbidden, 'When I check availability calculation' + .' to see if on shelf holds are forbidden, then no exception is given.'); + }; +}; + +subtest 'on_shelf_holds_forbidden if all available' => \&t_on_shelf_holds_forbidden_all_available; +sub t_on_shelf_holds_forbidden_all_available { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + my $biblio = Koha::Biblios->find($item->biblionumber); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + my $item2 = build_a_test_item($biblio, $biblioitem); + $item2->itype($item->itype)->store; + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + holds_per_record => 1, + reservesallowed => 1, + onshelfholds => 0, + })->store; + + my $expecting = 'Koha::Exceptions::Hold::OnShelfNotAllowed'; + subtest 'While all items are available' => sub { + plan tests => 3; + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + is($rule->onshelfholds, 0, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed if any is unavailable.'); + ok($issuingcalc->on_shelf_holds_forbidden,'When I check ' + .'availability calculation to see if on shelf holds are forbidden,'); + is(ref($issuingcalc->on_shelf_holds_forbidden), $expecting, 'Then' + ." exception $expecting is given."); + }; + subtest 'While one of two items is available' => sub { + plan tests => 7; + is($rule->onshelfholds, 0, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed if any is unavailable.'); + ok(issue_item($item, $patron), 'We have issued one of two items.'); + $item = Koha::Items->find($item->itemnumber); #refresh + my $issuingcalc; + ok($issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item + }), 'First, we will check on shelf hold restrictions for the item that is' + .' unavailable.'); + ok(!$issuingcalc->on_shelf_holds_forbidden, 'When I check availability calculation' + .' to see if on shelf holds are forbidden, then no exception is given.'); + ok($issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item2 + }), 'Then, we will check on shelf hold restrictions for the item that is' + .' available.'); + ok($issuingcalc->on_shelf_holds_forbidden, 'When I check ' + .'availability calculation to see if on shelf holds are forbidden,'); + is(ref($issuingcalc->on_shelf_holds_forbidden), $expecting, 'Then' + ." exception $expecting is given."); + }; + subtest 'While all are unavailable' => sub { + plan tests => 3; + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + is($rule->onshelfholds, 0, 'When I look at issuing rules, I see that' + .' onshelfholds are allowed if all are unavailable.'); + ok(issue_item($item2, $patron), 'Both items are now unavailable.'); + ok(!$issuingcalc->on_shelf_holds_forbidden, 'When I check availability calculation' + .' to see if on shelf holds are forbidden, then no exception is given.'); + }; +}; + +subtest 'opac_item_level_hold_forbidden' => \&t_opac_item_level_hold_forbidden; +sub t_opac_item_level_hold_forbidden { + plan tests => 2; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + holds_per_record => 1, + reservesallowed => 1, + opacitemholds => 'N', + })->store; + + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + my $expecting = 'Koha::Exceptions::Hold::ItemLevelHoldNotAllowed'; + is($rule->opacitemholds, 'N', 'When I look at issuing rules, I see that' + .' opacitemholds is disabled for this itemtype'); + is(ref($issuingcalc->opac_item_level_hold_forbidden), $expecting, "When I ask if" + ." item level holds are allowed, exception $expecting is given."); +}; + +subtest 'opac_item_level_hold_forbidden, not forbidden' => \&t_opac_item_level_hold_not_forbidden; +sub t_opac_item_level_hold_not_forbidden { + plan tests => 2; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + holds_per_record => 1, + reservesallowed => 1, + opacitemholds => 'Y', + })->store; + + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + }); + my $expecting = 'Koha::Exceptions::Hold::ItemLevelHoldNotAllowed'; + is($rule->opacitemholds, 'Y', 'When I look at issuing rules, I see that' + .' opacitemholds is enabled for this itemtype'); + ok(!$issuingcalc->opac_item_level_hold_forbidden, 'When I ask if' + .' item level holds are allowed, then no exception is given.'); +}; + +subtest 'zero_holds_allowed' => \&t_zero_holds_allowed; +sub t_zero_holds_allowed { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + reservesallowed => 0, + })->store; + + add_item_level_hold($item, $patron, $item->homebranch); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Hold::ZeroHoldsAllowed'; + + is($rule->reservesallowed, 0, 'When I look at issuing rules, I see that' + .' zero holds are allowed for this itemtype.'); + is(Koha::Holds->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one hold.'); + is(ref($issuingcalc->zero_holds_allowed), $expecting, "When I ask if" + ." zero holds are allowed, exception $expecting is given."); +}; + +subtest 'zero_checkouts_allowed' => \&t_zero_checkouts_allowed; +sub t_zero_checkouts_allowed { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + Koha::IssuingRules->search->delete; + my $rule = Koha::IssuingRule->new({ + branchcode => '*', + itemtype => $item->effective_itemtype, + categorycode => '*', + maxissueqty => 0, + })->store; + + issue_item($item, $patron); + my $issuingcalc = Koha::Availability::Checks::IssuingRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Checkout::ZeroCheckoutsAllowed'; + + is($rule->maxissueqty, 0, 'When I look at issuing rules, I see that' + .' zero checkouts are allowed for this itemtype.'); + is(Koha::Checkouts->search({borrowernumber=>$patron->borrowernumber})->count, + 1, 'I see that I have one checkout.'); + is(ref($issuingcalc->zero_checkouts_allowed), $expecting, "When I ask if" + ." zero checkouts are allowed, exception $expecting is given."); +}; + +$schema->storage->txn_rollback; + +1; --- a/t/db_dependent/Koha/Availability/Checks/Item.t +++ a/t/db_dependent/Koha/Availability/Checks/Item.t @@ -0,0 +1,382 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Test::More tests => 14; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use C4::Members; + +use Koha::Account::Lines; +use Koha::Biblioitems; +use Koha::Database; +use Koha::DateUtils; +use Koha::Items; +use Koha::Item::Transfers; + +use Koha::Availability::Checks::Item; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +subtest 'checked_out' => \&t_checked_out; +sub t_checked_out { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + issue_item($item, $patron); + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::CheckedOut'; + + is(Koha::Checkouts->search({ itemnumber => $item->itemnumber })->count, 1, + 'I found out that item is checked out.'); + is(ref($itemcalc->checked_out), $expecting, "When I check item availability" + ." calculation for checked_out, then exception $expecting is given."); +}; + +subtest 'damaged' => \&t_damaged; +sub t_damaged { + plan tests => 2; + + t::lib::Mocks::mock_preference('AllowHoldsOnDamagedItems', 0); + + my $item = build_a_test_item()->set({damaged=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::Damaged'; + + is($item->damaged, 1, 'When I look at the item, I see that it is damaged.'); + is(ref($itemcalc->damaged), $expecting, "When I check item availability" + ." calculation for damaged, then exception $expecting is given."); +}; + + +subtest 'from_another_branch, item from same branch than me' +=> \&t_from_another_library_same_branch; +sub t_from_another_library_same_branch { + plan tests => 3; + + t::lib::Mocks::mock_preference('IndependentBranches', 1); + t::lib::Mocks::mock_preference('HomeOrHoldingBranch', 'homebranch'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, + 'Midway Public Library', undef, '', ''); + + my $item = build_a_test_item(); + $item->homebranch($branchcode)->store; + + my $itemcalc = Koha::Availability::Checks::Item->new($item); + + is(C4::Context->userenv->{branch}, $item->homebranch, + 'Patron is from same branch as me.'); + ok(C4::Context->preference('IndependentBranches'), + 'IndependentBranches system preference is on.'); + ok(!$itemcalc->from_another_library, 'When I check if item is' + .' from same branch as me, then no exception is given.'); +} + +subtest 'from_another_library, item from different branch than me' +=> \&t_from_another_library; +sub t_from_another_library { + plan tests => 3; + + t::lib::Mocks::mock_preference('IndependentBranches', 1); + t::lib::Mocks::mock_preference('HomeOrHoldingBranch', 'homebranch'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + my $branchcode2 = $builder->build({ source => 'Branch' })->{'branchcode'}; + + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, + 'Midway Public Library', undef, '', ''); + + my $item = build_a_test_item(); + $item->homebranch($branchcode2)->store; + + my $itemcalc= Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::FromAnotherLibrary'; + + isnt(C4::Context->userenv->{branch}, $item->homebranch, 'Item is not from' + .' same branch as me.'); + ok(C4::Context->preference('IndependentBranches'), 'IndependentBranches' + .' system preference is on.'); + is(ref($itemcalc->from_another_library), $expecting, 'When I check if item is' + ." from same branch as me, then $expecting is given."); +} + +subtest 'held' => \&t_held; +sub t_held { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + add_item_level_hold($item, $patron, $patron->branchcode); + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::Held'; + + is(Koha::Holds->search({biblionumber => $item->biblionumber})->count, 1, + 'I found out that item is held.'); + is(ref($itemcalc->held), $expecting, "When I check item availability " + ."calculation for held, then exception $expecting is given."); +}; + +subtest 'held_by_patron' => \&t_held_by_patron; +sub t_held_by_patron { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $item = build_a_test_item(); + add_item_level_hold($item, $patron, $patron->branchcode); + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::AlreadyHeldForThisPatron'; + + is(Koha::Holds->search({biblionumber => $item->biblionumber})->count, 1, + 'I found out that item is held.'); + is(ref($itemcalc->held_by_patron($patron)), $expecting, "When I check item " + ."availability calculation for held, then exception $expecting is given."); +}; + +subtest 'lost' => \&t_lost; +sub t_lost { + plan tests => 2; + + my $item = build_a_test_item()->set({itemlost=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::Lost'; + + is($item->itemlost, 1, 'When I look at the item, I see that it is lost.'); + is(ref($itemcalc->lost), $expecting, "When I check availability calculation" + ." for item lost, then exception $expecting is given."); +}; + +subtest 'notforloan' => \&t_notforloan; +sub t_notforloan { + plan tests => 2; + + subtest 'Given item is notforloan > 0' => sub { + plan tests => 2; + + my $item = build_a_test_item()->set({notforloan=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::NotForLoan'; + + is($item->notforloan, 1, 'When I look at the item, I see that it is not' + .' for loan.'); + is(ref($itemcalc->notforloan), $expecting, + "When I look availability calculation for at item notforloan, then " + ." exception $expecting is given."); + }; + + subtest 'Given item type is notforloan > 0' => sub { + subtest 'Given item-level_itypes is on (item-itemtype)' + => \&t_itemlevel_itemtype_notforloan_item_level_itypes_on; + subtest 'Given item-level_itypes is off (biblioitem-itemtype)' + => \&t_itemlevel_itemtype_notforloan_item_level_itypes_off; + sub t_itemlevel_itemtype_notforloan_item_level_itypes_on { + plan tests => 3; + + t::lib::Mocks::mock_preference('item-level_itypes', 1); + + my $item = build_a_test_item(); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + my $itemtype = Koha::ItemTypes->find($item->itype); + $itemtype->set({notforloan=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::ItemType::NotForLoan'; + + is(Koha::ItemTypes->find($biblioitem->itemtype)->notforloan, 0, + 'Biblioitem itemtype is for loan.'); + is(Koha::ItemTypes->find($item->itype)->notforloan, 1, + 'Item itemtype is not for loan.'); + is(ref($itemcalc->notforloan), $expecting, "When I look at " + ."availability calculation for item notforloan, then " + ."exception $expecting is given."); + }; + sub t_itemlevel_itemtype_notforloan_item_level_itypes_off { + plan tests => 3; + + t::lib::Mocks::mock_preference('item-level_itypes', 0); + + my $item = build_a_test_item(); + my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber); + my $itemtype = Koha::ItemTypes->find($biblioitem->itemtype); + $itemtype->set({notforloan=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::ItemType::NotForLoan'; + + is(Koha::ItemTypes->find($biblioitem->itemtype)->notforloan, 1, + 'Biblioitem itemtype is not for loan.'); + is(Koha::ItemTypes->find($item->itype)->notforloan, 0, + 'Item itemtype is for loan.'); + is(ref($itemcalc->notforloan), $expecting, "When I look at " + ."availability calculation for item notforloan, then " + ."exception $expecting is given."); + }; + }; +}; + +subtest 'onloan' => \&t_onloan; +sub t_onloan { + plan tests => 2; + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + issue_item($item, $patron); + $item = Koha::Items->find($item->itemnumber); + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::CheckedOut'; + ok($item->onloan, 'When I look at the item, I see that it is ' + .'on loan.'); + is(ref($itemcalc->onloan), $expecting, "When I check availability " + ."calculation for item onloan, then exception $expecting is given."); +}; + +subtest 'restricted' => \&t_restricted; +sub t_restricted { + plan tests => 2; + + my $item = build_a_test_item()->set({restricted=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::Restricted'; + + is($item->restricted, 1, 'When I look at the item, I see that it is ' + .'restricted.'); + is(ref($itemcalc->restricted), $expecting, "When I check availability " + ."calculation for item restricted, then exception $expecting is given."); +}; + +subtest 'transfer' => \&t_transfer; +sub t_transfer { + plan tests => 4; + + my $item = build_a_test_item(); + my $item2 = build_a_test_item(); + my $transfer = Koha::Item::Transfer->new({ + itemnumber => $item->itemnumber, + datesent => '2000-12-12 12:12:12', + frombranch => $item->homebranch, + tobranch => $item2->homebranch, + comments => 'Very transfer', + })->store; + + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::Transfer'; + ok($transfer, 'Item is in transfer.'); + is(ref($itemcalc->transfer), $expecting, "When I check availability" + ." calculation for item transfer, then exception $expecting is given."); + $itemcalc = Koha::Availability::Checks::Item->new($item2); + ok(!Koha::Item::Transfers->find({ itemnumber => $item2->itemnumber}), + 'Another item is not in transfer.'); + ok(!$itemcalc->transfer, "When I check availability calculation for another" + ." item transfer, then no exception is given."); +}; + +subtest 'transfer_limit' => \&t_transfer_limit; +sub t_transfer_limit { + plan tests => 8; + + t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1); + t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype'); + + my $item = build_a_test_item(); + my $another_branch = Koha::Libraries->find( + $builder->build({ source => 'Branch' })->{'branchcode'}); + my $third_branch = Koha::Libraries->find( + $builder->build({ source => 'Branch' })->{'branchcode'}); + my $expecting = 'Koha::Exceptions::Item::CannotBeTransferred'; + + is(C4::Circulation::CreateBranchTransferLimit( + $another_branch->branchcode, + $item->holdingbranch, + $item->effective_itemtype + ), 1, 'There is a branch transfer limit for itemtype from ' + .$item->holdingbranch.' to '.$another_branch->branchcode .'.'); + my $itemcalc = Koha::Availability::Checks::Item->new($item); + + is(ref($itemcalc->transfer_limit($another_branch->branchcode)), $expecting, + "When I check availability calculation for transfer limit, then $expecting is given."); + ok(!$itemcalc->transfer_limit($third_branch->branchcode), + 'However, item can be transferred from' + .' a library to another library when there are no branch transfer limits.'); + + t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'ccode'); + ok(!$itemcalc->transfer_limit($another_branch->branchcode), + 'When we change limit to ccode, previous' + .' transfer limit is not in action anymore.'); + is(C4::Circulation::CreateBranchTransferLimit( + $another_branch->branchcode, + $item->holdingbranch, + $item->ccode + ), 1, 'Then we added a branch transfer limit by ccode.'); + is(ref($itemcalc->transfer_limit($another_branch->branchcode)), $expecting, + "When I check availability calculation for for transfer limit," + ." then $expecting is given."); + + t::lib::Mocks::mock_preference('UseBranchTransferLimits', 0); + is(C4::Context->preference('UseBranchTransferLimits'), 0, + 'Then, we switched UseBranchTransferLimits off.'); + ok(!$itemcalc->transfer_limit($another_branch->branchcode), + 'Then no exception is given anymore.'); +} + +subtest 'unknown_barcode' => \&t_unknown_barcode; +sub t_unknown_barcode { + plan tests => 2; + + my $item = build_a_test_item()->set({barcode=>undef})->store; + my $expecting = 'Koha::Exceptions::Item::UnknownBarcode'; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + + is($item->barcode, undef, 'When I look at the item, we see that it has ' + .'undefined barcode.'); + is(ref($itemcalc->unknown_barcode), $expecting, + "When I check availability calculation for for unknown barcode, " + ."then $expecting is given."); +}; + +subtest 'withdrawn' => \&t_withdrawn; +sub t_withdrawn { + plan tests => 2; + + my $item = build_a_test_item()->set({withdrawn=>1})->store; + my $itemcalc = Koha::Availability::Checks::Item->new($item); + my $expecting = 'Koha::Exceptions::Item::Withdrawn'; + + is($item->withdrawn, 1, 'When I look at the item, I see that it is ' + .'withdrawn.'); + is(ref($itemcalc->withdrawn), $expecting, "When I check availability" + ." calculation for item withdrawn, then exception $expecting is given."); +}; + +$schema->storage->txn_rollback; + +1; --- a/t/db_dependent/Koha/Availability/Checks/LibraryItemRule.t +++ a/t/db_dependent/Koha/Availability/Checks/LibraryItemRule.t @@ -0,0 +1,312 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Test::More tests => 3; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use Koha::Database; +use Koha::Items; + +use Koha::Availability::Checks::LibraryItemRule; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new; + +subtest 'Given CircControl is configured as ItemHomeLibrary' => sub { + plan tests => 3; + + subtest 'hold_not_allowed_by_library, item home library' => \&t_hold_not_allowed_item_home_library_item; + sub t_hold_not_allowed_item_home_library_item { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary'); + + my $item = build_a_test_item(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $item->homebranch, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from item home library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item + }); + my $expecting = 'Koha::Exceptions::Hold::NotAllowedByLibrary'; + + is(C4::Context->preference('CircControl'), 'ItemHomeLibrary', 'CircControl system preference' + .' is configured as ItemHomeLibrary.'); + is(ref($libitemcalc->hold_not_allowed_by_library), $expecting, + "When I check if hold is allowed by library item rules, exception $expecting is given."); + }; + + subtest 'hold_not_allowed_by_library, patron library' => \&t_hold_not_allowed_item_home_library_patron; + sub t_hold_not_allowed_item_home_library_patron { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary'); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $patron->branchcode, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from patron library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron, + }); + + is(C4::Context->preference('CircControl'), 'ItemHomeLibrary', 'CircControl system preference' + .' is configured as ItemHomeLibrary.'); + ok(!$libitemcalc->hold_not_allowed_by_library, + 'When I check if hold is allowed by library item rules, then no exception is given.'); + }; + + subtest 'hold_not_allowed_by_library, logged in library' => \&t_hold_not_allowed_item_home_library_logged_in; + sub t_hold_not_allowed_item_home_library_logged_in { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'ItemHomeLibrary'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', undef, '', ''); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $branchcode, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from logged in library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron + }); + + is(C4::Context->preference('CircControl'), 'ItemHomeLibrary', 'CircControl system preference' + .' is configured as ItemHomeLibrary.'); + ok(!$libitemcalc->hold_not_allowed_by_library, + 'When I check if hold is allowed by library item rules, then no exception is given.'); + }; +}; + +subtest 'Given CircControl is configured as PatronLibrary' => sub { + plan tests => 3; + + subtest 'hold_not_allowed_by_library, item home library' => \&t_hold_not_allowed_patron_library_item; + sub t_hold_not_allowed_patron_library_item { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary'); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $item->homebranch, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from item home library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron + }); + + is(C4::Context->preference('CircControl'), 'PatronLibrary', 'CircControl system preference' + .' is configured as PatronLibrary.'); + ok(!$libitemcalc->hold_not_allowed_by_library, + 'When I check if hold is allowed by library item rules, then no exception is given.'); + }; + + subtest 'hold_not_allowed_by_library, patron library' => \&t_hold_not_allowed_patron_library_patron; + sub t_hold_not_allowed_patron_library_patron { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary'); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $patron->branchcode, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from patron library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron + }); + my $expecting = 'Koha::Exceptions::Hold::NotAllowedByLibrary'; + + is(C4::Context->preference('CircControl'), 'PatronLibrary', 'CircControl system preference' + .' is configured as PatronLibrary.'); + is(ref($libitemcalc->hold_not_allowed_by_library), $expecting, + "When I check if hold is allowed by library item rules, exception $expecting is given."); + }; + + subtest 'hold_not_allowed_by_library, logged in library' => \&t_hold_not_allowed_patron_library_logged_in; + sub t_hold_not_allowed_patron_library_logged_in { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PatronLibrary'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', undef, '', ''); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $branchcode, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from logged in library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron + }); + + is(C4::Context->preference('CircControl'), 'PatronLibrary', 'CircControl system preference' + .' is configured as PatronLibrary.'); + ok(!$libitemcalc->hold_not_allowed_by_library, + 'When I check if hold is allowed by library item rules, then no exception is given.'); + }; +}; + +subtest 'Given CircControl is configured as PickupLibrary' => sub { + plan tests => 3; + + subtest 'hold_not_allowed_by_library, item home library' => \&t_hold_not_allowed_pickup_library_item; + sub t_hold_not_allowed_pickup_library_item { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', undef, '', ''); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $item->homebranch, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from item home library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron + }); + + is(C4::Context->preference('CircControl'), 'PickupLibrary', 'CircControl system preference' + .' is configured as PickupLibrary.'); + ok(!$libitemcalc->hold_not_allowed_by_library, + 'When I check if hold is allowed by library item rules, then no exception is given.'); + }; + + subtest 'hold_not_allowed_by_library, patron library' => \&t_hold_not_allowed_pickup_library_patron; + sub t_hold_not_allowed_pickup_library_patron { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', undef, '', ''); + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $patron->branchcode, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from patron library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron, + }); + + is(C4::Context->preference('CircControl'), 'PickupLibrary', 'CircControl system preference' + .' is configured as PickupLibrary.'); + ok(!$libitemcalc->hold_not_allowed_by_library, + 'When I check if hold is allowed by library item rules, then no exception is given.'); + }; + subtest 'hold_not_allowed_by_library for patron library, CircControl = PickupLibrary' => \&t_hold_not_allowed_pickup_library_logged_in; + sub t_hold_not_allowed_pickup_library_logged_in { + plan tests => 3; + + set_default_system_preferences(); + set_default_circulation_rules(); + t::lib::Mocks::mock_preference('CircControl', 'PickupLibrary'); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', undef, '', ''); + + my $item = build_a_test_item(); + my $patron = build_a_test_patron(); + ok($dbh->do(q{ + INSERT INTO branch_item_rules (branchcode, itemtype, holdallowed, returnbranch) + VALUES (?, ?, ?, ?) + }, {}, $branchcode, $item->effective_itemtype, 0, 'homebranch'), 'There is a branch item' + .' rule that says holds are not allowed from logged in library.'); + + my $libitemcalc = Koha::Availability::Checks::LibraryItemRule->new({ + item => $item, + patron => $patron, + }); + my $expecting = 'Koha::Exceptions::Hold::NotAllowedByLibrary'; + + is(C4::Context->preference('CircControl'), 'PickupLibrary', 'CircControl system preference' + .' is configured as PickupLibrary.'); + is(ref($libitemcalc->hold_not_allowed_by_library), $expecting, + "When I check if hold is allowed by library item rules, exception $expecting is given."); + }; +}; + +$schema->storage->txn_rollback; + +1; --- a/t/db_dependent/Koha/Availability/Checks/Patron.t +++ a/t/db_dependent/Koha/Availability/Checks/Patron.t @@ -0,0 +1,452 @@ +#!/usr/bin/perl + +# Copyright Koha-Suomi Oy 2016 +# +# This file is part of Koha +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use Test::More tests => 20; +use t::lib::Mocks; +use t::lib::TestBuilder; +require t::db_dependent::Koha::Availability::Helpers; + +use C4::Members; + +use Koha::Account::Lines; +use Koha::Biblioitems; +use Koha::Database; +use Koha::DateUtils; +use Koha::Items; + +use Koha::Availability::Checks::Patron; + +my $schema = Koha::Database->new->schema; +$schema->storage->txn_begin; + +my $dbh = C4::Context->dbh; +$dbh->{RaiseError} = 1; + +my $builder = t::lib::TestBuilder->new; + +set_default_system_preferences(); +set_default_circulation_rules(); + +subtest 'debarred, given patron is not debarred' => \&t_not_debarred; +sub t_not_debarred { + plan tests => 3; + + my $patron = build_a_test_patron(); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $expecting = 'Koha::Exceptions::Patron::Debarred'; + + is($patron->debarred, undef, 'Patron is not debarred.'); + is($patron->debarredcomment, undef, 'Patron does not have debarment comment.'); + ok(!$patroncalc->debarred, 'When I check patron debarment, then no exception is given.'); +}; + +subtest 'debarred, given patron is debarred' => \&t_debarred; +sub t_debarred { + plan tests => 5; + + my $patron = build_a_test_patron(); + my $hundred_days = output_pref({ dt => dt_from_string()->add_duration( + DateTime::Duration->new(days => 100)), dateformat => 'iso', dateonly => 1 }); + $patron->set({ debarred => $hundred_days, debarredcomment => 'DONT EVER COME BACK' })->store; + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $expecting = 'Koha::Exceptions::Patron::Debarred'; + my $debarred = $patroncalc->debarred; + + is($patron->debarred, $hundred_days, "Patron is debarred until $hundred_days."); + is($patron->debarredcomment, 'DONT EVER COME BACK', 'Library does not want them to ever come back.'); + is(ref($debarred), $expecting, "When I check patron debarment, then $expecting is given."); + is($debarred->expiration, $patron->debarred, 'Then, from the status, I can see the expiration date.'); + is($debarred->comment, $patron->debarredcomment, 'Then, from the status, I can see that patron should never come back.'); +}; + +subtest 'debt_checkout, given patron has less than noissuescharge' => \&t_fines_checkout_less_than_max; +sub t_fines_checkout_less_than_max { + plan tests => 8; + + t::lib::Mocks::mock_preference('noissuescharge', 9001); + t::lib::Mocks::mock_preference('AllFinesNeedOverride', 0); + + my $patron = build_a_test_patron(); + my $line = Koha::Account::Line->new({ + borrowernumber => $patron->borrowernumber, + amountoutstanding => 9000, + })->store; + my ($outstanding) = C4::Members::GetMemberAccountRecords($patron->borrowernumber); + my $maxoutstanding = C4::Context->preference('noissuescharge'); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_checkout; + + is(C4::Context->preference('AllFinesNeedOverride'), 0, 'Not all fines need override.'); + ok($maxoutstanding, 'When I look at system preferences, I see that maximum allowed outstanding fines is set.'); + ok($maxoutstanding > $outstanding, 'When I check patron\'s balance, I found out they have less outstanding fines than maximum allowed.'); + ok(!$debt, 'When I check patron debt, then no exception is given.'); + t::lib::Mocks::mock_preference('AllFinesNeedOverride', 1); + is(C4::Context->preference('AllFinesNeedOverride'), 1, 'Given we configure' + .' all fines needing override.'); + $debt = $patroncalc->debt_checkout; + my $expecting = 'Koha::Exceptions::Patron::Debt'; + is(ref($debt), $expecting, "When I check patron debt, then $expecting is given."); + is($debt->max_outstanding, sprintf("%.2f", $maxoutstanding), 'Then I can see the status showing me how much' + .' outstanding total can be at maximum.'); + is($debt->current_outstanding, sprintf("%.2f", $outstanding), 'Then I can see the status showing me how much' + .' outstanding fines patron has right now.'); +}; + +subtest 'debt_checkout, given patron has more than noissuescharge' => \&t_fines_checkout_more_than_max; +sub t_fines_checkout_more_than_max { + plan tests => 5; + + t::lib::Mocks::mock_preference('noissuescharge', 5); + + my $patron = build_a_test_patron(); + my $line = Koha::Account::Line->new({ + borrowernumber => $patron->borrowernumber, + amountoutstanding => 9001, + })->store; + my ($outstanding) = C4::Members::GetMemberAccountRecords($patron->borrowernumber); + my $maxoutstanding = C4::Context->preference('noissuescharge'); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_checkout; + my $expecting = 'Koha::Exceptions::Patron::Debt'; + + ok($maxoutstanding, 'When I look at system preferences, I see that maximum allowed outstanding fines is set.'); + ok($maxoutstanding < $outstanding, 'When I check patron\'s balance, I found out they have more outstanding fines than allowed.'); + is(ref($debt), $expecting, "When I check patron debt, then $expecting is given."); + is($debt->max_outstanding, sprintf("%.2f", $maxoutstanding), 'Then I can see the status showing me how much' + .' outstanding total can be at maximum.'); + is($debt->current_outstanding, sprintf("%.2f", $outstanding), 'Then I can see the status showing me how much' + .' outstanding fines patron has right now.'); +}; + +subtest 'debt_hold, given patron has less than maxoutstanding' => \&t_fines_hold_less_than_max; +sub t_fines_hold_less_than_max { + plan tests => 3; + + t::lib::Mocks::mock_preference('maxoutstanding', 9001); + + my $patron = build_a_test_patron(); + my $line = Koha::Account::Line->new({ + borrowernumber => $patron->borrowernumber, + amountoutstanding => 9000, + })->store; + my ($outstanding) = C4::Members::GetMemberAccountRecords($patron->borrowernumber); + my $maxoutstanding = C4::Context->preference('maxoutstanding'); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_hold; + + ok($maxoutstanding, 'When I look at system preferences, I see that maximum allowed outstanding fines is set.'); + ok($maxoutstanding > $outstanding, 'When I check patron\'s balance, I found out they have less outstanding fines than maximum allowed.'); + ok(!$debt, 'When I check patron debt, then no exception is given.'); +}; + +subtest 'debt_hold, given patron has more than maxoutstanding' => \&t_fines_hold_more_than_max; +sub t_fines_hold_more_than_max { + plan tests => 5; + + t::lib::Mocks::mock_preference('maxoutstanding', 5); + + my $patron = build_a_test_patron(); + my $line = Koha::Account::Line->new({ + borrowernumber => $patron->borrowernumber, + amountoutstanding => 9001, + })->store; + my ($outstanding) = C4::Members::GetMemberAccountRecords($patron->borrowernumber); + my $maxoutstanding = C4::Context->preference('maxoutstanding'); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_hold; + my $expecting = 'Koha::Exceptions::Patron::Debt'; + + ok($maxoutstanding, 'When I look at system preferences, I see that maximum allowed outstanding fines is set.'); + ok($maxoutstanding < $outstanding, 'When I check patron\'s balance, I found out they have more outstanding fines than allowed.'); + is(ref($debt), $expecting, "When I check patron debt, then $expecting is given."); + is($debt->max_outstanding, sprintf("%.2f", $maxoutstanding), 'Then I can see the status showing me how much' + .' outstanding total can be at maximum.'); + is($debt->current_outstanding, sprintf("%.2f", $outstanding), 'Then I can see the status showing me how much' + .' outstanding fines patron has right now.'); +}; + +subtest 'debt_renew_opac, given patron has less than OPACFineNoRenewals' => \&t_fines_renew_opac_less_than_max; +sub t_fines_renew_opac_less_than_max { + plan tests => 3; + + t::lib::Mocks::mock_preference('OPACFineNoRenewals', 9001); + + my $patron = build_a_test_patron(); + my $line = Koha::Account::Line->new({ + borrowernumber => $patron->borrowernumber, + amountoutstanding => 9000, + })->store; + my ($outstanding) = C4::Members::GetMemberAccountRecords($patron->borrowernumber); + my $maxoutstanding = C4::Context->preference('OPACFineNoRenewals'); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_renew_opac; + + ok($maxoutstanding, 'When I look at system preferences, I see that maximum allowed outstanding fines is set.'); + ok($maxoutstanding > $outstanding, 'When I check patron\'s balance, I found out they have less outstanding fines than maximum allowed.'); + ok(!$debt, 'When I check patron debt, then no exception is given.'); +}; + +subtest 'debt_renew_opac, given patron has more than OPACFineNoRenewals' => \&t_fines_renew_opac_more_than_max; +sub t_fines_renew_opac_more_than_max { + plan tests => 5; + + t::lib::Mocks::mock_preference('OPACFineNoRenewals', 5); + + my $patron = build_a_test_patron(); + my $line = Koha::Account::Line->new({ + borrowernumber => $patron->borrowernumber, + amountoutstanding => 9001, + })->store; + my ($outstanding) = C4::Members::GetMemberAccountRecords($patron->borrowernumber); + my $maxoutstanding = C4::Context->preference('OPACFineNoRenewals'); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_renew_opac; + my $expecting = 'Koha::Exceptions::Patron::Debt'; + + ok($maxoutstanding, 'When I look at system preferences, I see that maximum allowed outstanding fines is set.'); + ok($maxoutstanding < $outstanding, 'When I check patron\'s balance, I found out they have more outstanding fines than allowed.'); + is(ref($debt), $expecting, "When I check patron debt, then $expecting is given."); + is($debt->max_outstanding, sprintf("%.2f", $maxoutstanding), 'Then I can see the status showing me how much' + .' outstanding total can be at maximum.'); + is($debt->current_outstanding, sprintf("%.2f", $outstanding), 'Then I can see the status showing me how much' + .' outstanding fines patron has right now.'); +}; + +subtest 'debt_checkout_guarantees, given patron\'s guarantees have less than NoIssuesChargeGuarantees' => \&t_guarantees_fines_checkout_less_than_max; +sub t_guarantees_fines_checkout_less_than_max { + plan tests => 3; + + t::lib::Mocks::mock_preference('NoIssuesChargeGuarantees', 5); + + my $patron = build_a_test_patron(); + my $guarantee1 = build_a_test_patron(); + $guarantee1->guarantorid($patron->borrowernumber)->store; + my $guarantee2 = build_a_test_patron(); + $guarantee2->guarantorid($patron->borrowernumber)->store; + my $line1 = Koha::Account::Line->new({ + borrowernumber => $guarantee1->borrowernumber, + amountoutstanding => 2, + })->store; + my $line2 = Koha::Account::Line->new({ + borrowernumber => $guarantee2->borrowernumber, + amountoutstanding => 2, + })->store; + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_checkout_guarantees; + + ok($line1, 'We have added very small fines to patron\'s guarantees.'); + ok($line1->amountoutstanding+$line2->amountoutstanding < C4::Context->preference('NoIssuesChargeGuarantees'), + 'These fines total is less than NoIssuesChargeGuarantees'); + ok(!$debt, 'When I check patron guarantees debt, then no exception is given.'); +}; + +subtest 'debt_checkout_guarantees, given patron\s guarantees have more than NoIssuesChargeGuarantees' => \&t_guarantees_fines_checkout_more_than_max; +sub t_guarantees_fines_checkout_more_than_max { + plan tests => 5; + + t::lib::Mocks::mock_preference('NoIssuesChargeGuarantees', 5); + + my $patron = build_a_test_patron(); + my $guarantee1 = build_a_test_patron(); + $guarantee1->guarantorid($patron->borrowernumber)->store; + my $guarantee2 = build_a_test_patron(); + $guarantee2->guarantorid($patron->borrowernumber)->store; + my $line1 = Koha::Account::Line->new({ + borrowernumber => $guarantee1->borrowernumber, + amountoutstanding => 3, + })->store; + my $line2 = Koha::Account::Line->new({ + borrowernumber => $guarantee2->borrowernumber, + amountoutstanding => 3, + })->store; + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $debt = $patroncalc->debt_checkout_guarantees; + my $expecting = 'Koha::Exceptions::Patron::DebtGuarantees'; + + ok($line1, 'We have added very small fines to patron\'s guarantees.'); + ok($line1->amountoutstanding+$line2->amountoutstanding > C4::Context->preference('NoIssuesChargeGuarantees'), + 'These fines total is more than NoIssuesChargeGuarantees'); + is(ref($debt), $expecting, "When I check patron guarantees debt, then $expecting is given."); + is($debt->max_outstanding, "5.00", 'Then I can see the status showing me how much' + .' outstanding total can be at maximum.'); + is($debt->current_outstanding, "6.00", 'Then I can see the status showing me how much' + .' outstanding fines patron guarantees have right now.'); +}; + +subtest 'exceeded_maxreserves, given patron not exceeding max reserves' => \&t_exceeded_maxreserves_not; +sub t_exceeded_maxreserves_not { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $holds = Koha::Holds->search({ borrowernumber => $patron->borrowernumber }); + + is($holds->count, 0, 'Patron has no holds.'); + ok(!$patroncalc->exceeded_maxreserves, 'When I check if patron exceeded maxreserves, then no exception is given.'); +}; + +subtest 'exceeded_maxreserves, given patron exceeding max reserves' => \&t_exceeded_maxreserves; +sub t_exceeded_maxreserves { + plan tests => 5; + + t::lib::Mocks::mock_preference('maxreserves', 1); + + my $item = build_a_test_item(); + my $item2 = build_a_test_item(); + my $patron = build_a_test_patron(); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + add_biblio_level_hold($item, $patron, $patron->branchcode); + add_biblio_level_hold($item2, $patron, $patron->branchcode); + my $holds = Koha::Holds->search({ borrowernumber => $patron->borrowernumber }); + my $expecting = 'Koha::Exceptions::Hold::MaximumHoldsReached'; + my $exceeded = $patroncalc->exceeded_maxreserves; + + is(C4::Context->preference('maxreserves'), 1, 'Maximum number of holds allowed is one.'); + is($holds->count, 2, 'Patron has two holds.'); + is(ref($exceeded), $expecting, "When I check patron expiration, then $expecting is given."); + is($exceeded->max_holds_allowed, 1, 'Then I can see the status showing me how many' + .' holds are allowed at max.'); + is($exceeded->current_hold_count, 2, 'Then I can see the status showing me how many' + .' holds patron has right now.'); +}; + +subtest 'expired, given patron is not expired' => \&t_not_expired; +sub t_not_expired { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $dt_expiry = dt_from_string($patron->dateexpiry); + my $now = DateTime->now(time_zone => C4::Context->tz); + + is(DateTime->compare($dt_expiry, $now), 1, 'Patron is not expired.'); + ok(!$patroncalc->expired, 'When I check patron expiration, then no exception is given.'); +}; + +subtest 'expired, given patron is expired' => \&t_expired; +sub t_expired { + plan tests => 2; + + my $patron = build_a_test_patron(); + $patron->dateexpiry('1999-10-10')->store; + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $dt_expiry = dt_from_string($patron->dateexpiry); + my $now = DateTime->now(time_zone => C4::Context->tz); + my $expecting = 'Koha::Exceptions::Patron::CardExpired'; + + is(DateTime->compare($now, $dt_expiry), 1, 'Patron finds out their card is expired.'); + is(ref($patroncalc->expired), $expecting, "When I check patron expiration, then $expecting is given."); +}; + +subtest 'gonenoaddress, patron not gonenoaddress' => \&t_not_gonenoaddress; +sub t_not_gonenoaddress { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + + ok(!$patron->gonenoaddress, 'Patron is not gonenoaddress.'); + ok(!$patroncalc->gonenoaddress, 'When I check patron gonenoaddress, then no exception is given.'); +}; + +subtest 'gonenoaddress, patron gonenoaddress' => \&t_gonenoaddress; +sub t_gonenoaddress { + plan tests => 2; + + my $patron = build_a_test_patron(); + $patron->gonenoaddress('1')->store; + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $expecting = 'Koha::Exceptions::Patron::GoneNoAddress'; + + is($patron->gonenoaddress, 1, 'Patron is gonenoaddress.'); + is(ref($patroncalc->gonenoaddress), $expecting, "When I check patron gonenoaddress, then $expecting is given."); +}; + +subtest 'lost, patron card not lost' => \&t_not_lost; +sub t_not_lost { + plan tests => 2; + + my $patron = build_a_test_patron(); + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + + ok(!$patron->lost, 'Patron card is not lost.'); + ok(!$patroncalc->lost, 'When I check if patron card is lost, then no exception is given.'); +}; + +subtest 'lost, patron card lost' => \&t_lost; +sub t_lost { + plan tests => 2; + + my $patron = build_a_test_patron(); + $patron->lost('1')->store; + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $expecting = 'Koha::Exceptions::Patron::CardLost'; + + is($patron->lost, 1, 'Patron card is lost.'); + is(ref($patroncalc->lost), $expecting, "When I check if patron card is lost, then $expecting is given."); +}; + +subtest 'from_another_library, patron from same branch than me' => \&t_from_another_library_same_branch; +sub t_from_another_library_same_branch { + plan tests => 2; + + t::lib::Mocks::mock_preference('IndependentBranches', 0); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', '', '', ''); + + my $patron = build_a_test_patron(); + $patron->branchcode($branchcode)->store; + + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + + is(C4::Context->userenv->{branch}, $patron->branchcode, 'Patron is from same branch as me.'); + ok(!$patroncalc->from_another_library, 'When I check if patron is' + .' from same branch as me, then no exception is given.'); +} + +subtest 'from_another_library, patron from different branch than me' => \&t_from_another_library; +sub t_from_another_library { + plan tests => 2; + + t::lib::Mocks::mock_preference('IndependentBranches', 1); + + my $branchcode = $builder->build({ source => 'Branch' })->{'branchcode'}; + my $branchcode2 = $builder->build({ source => 'Branch' })->{'branchcode'}; + + C4::Context->_new_userenv('xxx'); + C4::Context->set_userenv(0,0,0,'firstname','surname', $branchcode, 'Midway Public Library', undef, '', ''); + + my $patron = build_a_test_patron(); + $patron->branchcode($branchcode2)->store; + + my $patroncalc = Koha::Availability::Checks::Patron->new($patron); + my $expecting = 'Koha::Exceptions::Patron::FromAnotherLibrary'; + + isnt(C4::Context->userenv->{branch}, $patron->branchcode, 'Patron is not from same branch as me.'); + is(ref($patroncalc->from_another_library), $expecting, "When I check if patron is" + ." from same branch as me, then $expecting is given."); +} + +$schema->storage->txn_rollback; + +1; --- a/t/db_dependent/Koha/Availability/Helpers.pm +++ a/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; --