View | Details | Raw Unified | Return to bug 17712
Collapse All | Expand All

(-)a/Koha/Availability/Hold.pm (+7 lines)
Lines 19-24 package Koha::Availability::Hold; Link Here
19
19
20
use Modern::Perl;
20
use Modern::Perl;
21
21
22
use Koha::Biblio::Availability::Hold;
22
use Koha::Item::Availability::Hold;
23
use Koha::Item::Availability::Hold;
23
24
24
use Koha::Exceptions;
25
use Koha::Exceptions;
Lines 29-34 sub new { Link Here
29
    bless $params, $class;
30
    bless $params, $class;
30
}
31
}
31
32
33
sub biblio {
34
    my ($self, $params) = @_;
35
36
    return Koha::Biblio::Availability::Hold->new($params);
37
}
38
32
sub item {
39
sub item {
33
    my ($self, $params) = @_;
40
    my ($self, $params) = @_;
34
41
(-)a/Koha/Biblio/Availability/Hold.pm (+251 lines)
Line 0 Link Here
1
package Koha::Biblio::Availability::Hold;
2
3
# Copyright Koha-Suomi Oy 2016
4
#
5
# This file is part of Koha
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
22
use base qw(Koha::Biblio::Availability);
23
24
use Koha::Item::Availability::Hold;
25
26
=head1 NAME
27
28
Koha::Biblio::Availability::Hold - Koha Biblio Availability Hold object class
29
30
=head1 SYNOPSIS
31
32
my $holdability = Koha::Biblio::Availability::Hold->new({
33
    biblio => $biblio,      # which biblio this availability is for
34
    patron => $patron,      # check biblio availability for this patron
35
})
36
37
=head1 DESCRIPTION
38
39
Class for checking biblio hold availability.
40
41
This class contains different levels of "recipes" that determine whether or not
42
a biblio should be considered available.
43
44
=head2 Class Methods
45
46
=cut
47
48
=head3 new
49
50
Constructs an biblio hold availability object. Biblio is always required.
51
52
MANDATORY PARAMETERS
53
54
    biblio (or biblionumber)
55
56
Biblio is a Koha::Biblio -object.
57
58
OPTIONAL PARAMETERS
59
60
    patron (or borrowernumber)
61
    to_branch
62
    limit (check only first n available items)
63
64
Patron is a Koha::Patron -object. To_branch is a branchcode of pickup location.
65
66
Returns a Koha::Biblio::Availability::Hold -object.
67
68
=cut
69
70
sub new {
71
    my ($class, $params) = @_;
72
73
    my $self = $class->SUPER::new($params);
74
75
    # Additionally, consider any transfer limits to pickup library by
76
    # providing to_branch parameter with branchcode of pickup library
77
    $self->{'to_branch'} = $params->{'to_branch'};
78
    # Check only first n available items by providing the value of n
79
    # in parameter 'limit'.
80
    $self->{'limit'} = $params->{'limit'};
81
82
    return $self;
83
}
84
85
sub in_intranet {
86
    my ($self, $params) = @_;
87
88
    $self->reset;
89
90
    my $patron;
91
    unless ($patron = $self->patron) {
92
        Koha::Exceptions::MissingParameter->throw(
93
            error => 'Missing parameter patron. This level of availability query '
94
            .'requires Koha::Biblio::Availability::Hold to have a patron parameter.'
95
        );
96
    }
97
98
    $params->{'intranet'} = 1;
99
    # Item looper
100
    $self->_item_looper($params);
101
102
    return $self;
103
}
104
105
sub in_opac {
106
    my ($self, $params) = @_;
107
108
    $self->reset;
109
110
    my $patron;
111
    unless ($patron = $self->patron) {
112
        Koha::Exceptions::MissingParameter->throw(
113
            error => 'Missing parameter patron. This level of availability query '
114
            .'requires Koha::Biblio::Availability::Hold to have a patron parameter.'
115
        );
116
    }
117
118
    # Check if holds are allowed in OPAC
119
    if (!C4::Context->preference('RequestOnOpac')) {
120
        $self->unavailable(Koha::Exceptions::Hold::NotAllowedInOPAC->new);
121
        return $self;
122
    } else {
123
        # Item looper
124
        $self->_item_looper($params);
125
    }
126
127
    return $self;
128
}
129
130
sub _item_looper {
131
    my ($self, $params) = @_;
132
133
    my $patron = $self->patron;
134
    my $biblio = $self->biblio;
135
    my $biblioitem = $self->biblio->biblioitem;
136
    my @items = $self->biblio->items;
137
    my @hostitemnumbers = C4::Items::get_hostitemnumbers_of($biblio->biblionumber);
138
    if (@hostitemnumbers) {
139
        my @hostitems = Koha::Items->search({
140
            itemnumber => { 'in' => @hostitemnumbers }
141
        });
142
        push @items, @hostitems;
143
    }
144
145
    if (scalar(@items) == 0) {
146
        $self->unavailable(Koha::Exceptions::Biblio::NoAvailableItems->new);
147
        return;
148
    }
149
150
    # Since we only need to check some patron and biblio related availability
151
    # checks once, do it outside the actual loop. To avoid code duplication,
152
    # use same checks as item availability calculation is using. Use the first
153
    # item to check these.
154
    my $first_item = $items[0];
155
    my $first_item_avail = Koha::Item::Availability::Hold->new({
156
        item => $first_item,
157
        patron => $patron,
158
    });
159
    $first_item_avail->common_biblio_checks($biblio);
160
    $first_item_avail->common_biblioitem_checks($biblioitem);
161
    $first_item_avail->common_patron_checks;
162
    if (keys %{$first_item_avail->unavailabilities} > 0) {
163
        $self->available(0);
164
    }
165
    $self->unavailabilities($first_item_avail->unavailabilities);
166
    $self->confirmations($first_item_avail->confirmations);
167
    $self->notes($first_item_avail->notes);
168
169
    # If we are looping in intranet, we can override any unavailabilities
170
    my $intranet = $params->{'intranet'} ? 1:0;
171
    my $override = 0;
172
    if ($intranet && C4::Context->preference('AllowHoldPolicyOverride')) {
173
        $override = 1;
174
    }
175
176
    # Stop calculating item availabilities after $limit available items are found.
177
    # E.g. parameter 'limit' with value 1 will find only one available item and
178
    # return biblio as available if no other unavailabilities are found. If you
179
    # want to calculate availability of every item in this biblio, do not give this
180
    # parameter.
181
    my $limit = $self->limit ? $self->limit : $params->{'limit'};
182
    my $count = 0;
183
184
    my @holds = Koha::Holds->search({ biblionumber => $biblio->biblionumber })->as_list;
185
    my @nonfound_holds = Koha::Holds->search({ biblionumber => $biblio->biblionumber, found => undef })->as_list;
186
    foreach my $item (@items) {
187
        # Break out of loop after $limit items are found available
188
        if (defined $limit && @{$self->{'item_availabilities'}} >= $limit) {
189
            last;
190
        }
191
        my $item_availability = $self->_item_check($item, $patron, \@holds, \@nonfound_holds, $intranet, $override);
192
        if ($item_availability->available) {
193
            push @{$self->{'item_availabilities'}}, $item_availability;
194
            $count++;
195
        } else {
196
            my $unavails = $item_availability->unavailabilities;
197
            # If Item level hold is not allowed and it is the only unavailability
198
            # reason, push the item to item_availabilities.
199
            if ($item_availability->unavailable == 1 && exists
200
                $unavails->{'Koha::Exceptions::Hold::ItemLevelHoldNotAllowed'}){
201
                push @{$self->{'item_availabilities'}}, $item_availability;
202
            } else {
203
                push @{$self->{'item_unavailabilities'}}, $item_availability;
204
            }
205
        }
206
    }
207
208
    # After going through items, if none are found available, set the biblio
209
    # unavailable
210
    if (@{$self->{'item_availabilities'}} == 0) {
211
        $self->unavailable(Koha::Exceptions::Biblio::NoAvailableItems->new);
212
    }
213
214
    return $self;
215
}
216
217
sub _item_check {
218
    my ($self, $item, $patron, $holds, $nonfound_holds, $intranet, $override) = @_;
219
220
    my $item_availability = Koha::Item::Availability::Hold->new({
221
        item => $item,
222
        patron => $patron,
223
        to_branch => $self->to_branch,
224
    });
225
226
    # Check availability without considering patron, biblio and bibitem
227
    # restrictions, since they only need to be checked once.
228
    $item_availability->common_issuing_rule_checks({
229
        use_cache => 1,
230
        nonfound_holds => $nonfound_holds
231
    });
232
    $item_availability->common_item_checks({ holds => $holds });
233
    $item_availability->common_library_item_rule_checks;
234
235
    unless ($intranet) {
236
        $item_availability->opac_specific_issuing_rule_checks;
237
    }
238
    if ($override) {
239
        # If in intranet and AllowHoldPolicyOverride is enabled, convert
240
        # unavailabilities into confirmations.
241
        $item_availability->confirmations({
242
            %{$item_availability->unavailabilities},
243
            %{$item_availability->confirmations}
244
        });
245
        $item_availability->unavailabilities({});
246
        $item_availability->available(1);
247
    }
248
    return $item_availability;
249
}
250
251
1;
(-)a/t/db_dependent/Koha/Biblio/Availability/Hold.t (-1 / +233 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Copyright Koha-Suomi Oy 2016
4
#
5
# This file is part of Koha
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
use Modern::Perl;
21
use Test::More tests => 5;
22
use t::lib::Mocks;
23
use t::lib::TestBuilder;
24
require t::db_dependent::Koha::Availability::Helpers;
25
use Benchmark;
26
27
use Koha::Database;
28
use Koha::IssuingRules;
29
use Koha::Items;
30
use Koha::ItemTypes;
31
32
use Koha::Biblio::Availability::Hold;
33
34
my $schema = Koha::Database->new->schema;
35
$schema->storage->txn_begin;
36
37
my $builder = t::lib::TestBuilder->new;
38
39
set_default_system_preferences();
40
set_default_circulation_rules();
41
42
subtest 'Biblio with zero available items in OPAC' => \&t_no_available_items_opac;
43
sub t_no_available_items_opac {
44
    plan tests => 6;
45
46
    my $item1 = build_a_test_item();
47
    my $biblio = Koha::Biblios->find($item1->biblionumber);
48
    my $item2 = build_a_test_item();
49
    $item2->biblionumber($biblio->biblionumber)->store;
50
    $item2->biblioitemnumber($item1->biblioitemnumber)->store;
51
52
    my $patron = build_a_test_patron();
53
    Koha::IssuingRules->search->delete;
54
    my $rule = Koha::IssuingRule->new({
55
        branchcode   => '*',
56
        itemtype     => $item2->effective_itemtype,
57
        categorycode => '*',
58
        holds_per_record => 0,
59
        reservesallowed => 0,
60
        opacitemholds => 'Y',
61
    })->store;
62
63
    t::lib::Mocks::mock_preference('UseBranchTransferLimits', 1);
64
    t::lib::Mocks::mock_preference('BranchTransferLimitsType', 'itemtype');
65
66
    my $branch2 = Koha::Libraries->find($builder->build({ source => 'Branch' })->{branchcode});
67
    C4::Circulation::CreateBranchTransferLimit(
68
        $branch2->branchcode,
69
        $item1->holdingbranch,
70
        $item1->effective_itemtype
71
    );
72
73
    my $availability = Koha::Biblio::Availability::Hold->new({
74
        biblio => $biblio,
75
        patron => $patron,
76
        to_branch => $branch2->branchcode
77
    })->in_opac;
78
    my $expecting = 'Koha::Exceptions::Biblio::NoAvailableItems';
79
80
    ok(!Koha::Item::Availability::Hold->new({
81
            item => $item1, patron => $patron, to_branch => $branch2->branchcode,
82
        })->in_opac->available,
83
       'When I look at the first item of two in this biblio, it is not available.');
84
    ok(!Koha::Item::Availability::Hold->new({
85
            item => $item2, patron => $patron
86
        })->in_opac->available,
87
       'When I look at the second item of two in this biblio, it is not available.');
88
    ok(!$availability->available, 'Then, the biblio is not available.');
89
    is($availability->unavailable, 1, 'Then, there is exactly one reason for unavailability.');
90
    is(ref($availability->unavailabilities->{$expecting}), $expecting, 'The reason says there are no'
91
       .' available items in this biblio.');
92
    is(@{$availability->item_unavailabilities}, 2, 'There seems to be two items that are unavailable.');
93
};
94
95
subtest 'Biblio with zero available items in intranet' => \&t_no_available_items_intranet;
96
sub t_no_available_items_intranet {
97
    plan tests => 2;
98
99
    my $item1 = build_a_test_item();
100
    my $biblio = Koha::Biblios->find($item1->biblionumber);
101
    my $item2 = build_a_test_item();
102
    $item2->biblionumber($biblio->biblionumber)->store;
103
    $item2->biblioitemnumber($item1->biblioitemnumber)->store;
104
105
    my $patron = build_a_test_patron();
106
    Koha::IssuingRules->search->delete;
107
    my $rule = Koha::IssuingRule->new({
108
        branchcode   => '*',
109
        itemtype     => $item2->effective_itemtype,
110
        categorycode => '*',
111
        holds_per_record => 0,
112
        reservesallowed => 0,
113
        opacitemholds => 'Y',
114
    })->store;
115
    $item1->withdrawn('1')->store;
116
117
    subtest 'While AllowHoldPolicyOverride is disabled' => sub {
118
        plan tests => 3;
119
120
        t::lib::Mocks::mock_preference('AllowHoldPolicyOverride', 0);
121
        my $availability = Koha::Biblio::Availability::Hold->new({
122
            biblio => $biblio, patron => $patron})->in_intranet;
123
        my $i1_avail = Koha::Item::Availability::Hold->new({
124
            item => $item1, patron => $patron })->in_intranet;
125
        my $i2_avail = Koha::Item::Availability::Hold->new({
126
            item => $item2, patron => $patron })->in_intranet;
127
        ok(!$i1_avail->available, 'When I look at the first item of two in this'
128
           .' biblio, it is not available.');
129
        ok(!$i2_avail->available, 'When I look at the second item of two in this'
130
           .' biblio, it is not available.');
131
        ok(!$availability->available, 'Then, the biblio is not available.');
132
    };
133
134
    subtest 'Given AllowHoldPolicyOverride is enabled' => sub {
135
        plan tests => 5;
136
137
        t::lib::Mocks::mock_preference('AllowHoldPolicyOverride', 1);
138
        my $availability = Koha::Biblio::Availability::Hold->new({
139
            biblio => $biblio, patron => $patron})->in_intranet;
140
        my $i1_avail = Koha::Item::Availability::Hold->new({
141
            item => $item1, patron => $patron })->in_intranet;
142
        my $i2_avail = Koha::Item::Availability::Hold->new({
143
            item => $item2, patron => $patron })->in_intranet;
144
        ok($i1_avail->available, 'When I look at the first item of two in this'
145
           .' biblio, it is available.');
146
        is($i1_avail->confirm, 1, 'Then the first item availability seems to have'
147
           .' one reason to ask for confirmation.');
148
        ok($i2_avail->available, 'When I look at the second item of two in this'
149
           .' biblio, it is available.');
150
        is($i2_avail->confirm, 1, 'Then the second item availability seems to'
151
           .' have one reason to ask for confirmation.');
152
        ok($availability->available, 'Then, the biblio is available.');
153
    };
154
155
};
156
157
subtest 'Biblio with one available items out of two' => \&t_one_out_of_two_items_available;
158
sub t_one_out_of_two_items_available {
159
    plan tests => 5;
160
161
    my $item1 = build_a_test_item();
162
    my $biblio = Koha::Biblios->find($item1->biblionumber);
163
    my $item2 = build_a_test_item();
164
    $item2->biblionumber($biblio->biblionumber)->store;
165
    $item2->biblioitemnumber($item1->biblioitemnumber)->store;
166
167
    my $patron = build_a_test_patron();
168
    $item1->withdrawn('1')->store;
169
170
    my $availability = Koha::Biblio::Availability::Hold->new({biblio => $biblio, patron => $patron})->in_opac;
171
    my $item_availabilities = $availability->item_availabilities;
172
    ok(!Koha::Item::Availability::Hold->new({ item => $item1, patron => $patron })->in_opac->available,
173
       'When I look at the first item of two in this biblio, it is not available.');
174
    ok(Koha::Item::Availability::Hold->new({ item => $item2, patron => $patron })->in_opac->available,
175
       'When I look at the second item of two in this biblio, it seems to be available.');
176
    ok($availability->available, 'Then, the biblio is available.');
177
    is(@{$item_availabilities}, 1, 'There seems to be one available item in this biblio.');
178
    is($item_availabilities->[0]->item->itemnumber, $item2->itemnumber, 'Then the only available item'
179
       .'is the second item of this biblio.');
180
};
181
182
subtest 'Biblio with two items out of two available' => \&t_all_items_available;
183
sub t_all_items_available {
184
    plan tests => 4;
185
186
    my $item1 = build_a_test_item();
187
    my $biblio = Koha::Biblios->find($item1->biblionumber);
188
    my $item2 = build_a_test_item();
189
    $item2->biblionumber($biblio->biblionumber)->store;
190
    $item2->biblioitemnumber($item1->biblioitemnumber)->store;
191
192
    my $patron = build_a_test_patron();
193
194
    my $availability = Koha::Biblio::Availability::Hold->new({biblio => $biblio, patron => $patron})->in_opac;
195
    my $item_availabilities = $availability->item_availabilities;
196
    ok(Koha::Item::Availability::Hold->new({ item => $item1, patron => $patron })->in_opac->available,
197
       'When I look at the first item of two in this biblio, it seems to be available.');
198
    ok(Koha::Item::Availability::Hold->new({ item => $item2, patron => $patron })->in_opac->available,
199
       'When I look at the second item of two in this biblio, it seems to be available.');
200
    ok($availability->available, 'Then, the biblio is available.');
201
    is(@{$item_availabilities}, 2, 'There seems to be two available items in this biblio.');
202
};
203
204
subtest 'Performance test' => \&t_performance_test;
205
sub t_performance_test {
206
    plan tests => 2;
207
208
    set_default_circulation_rules();
209
    my $item = build_a_test_item();
210
    my $patron = build_a_test_patron();
211
    my $biblio = Koha::Biblios->find($item->biblionumber);
212
    my $biblioitem = Koha::Biblioitems->find($item->biblioitemnumber);
213
214
    # add some items to biblio
215
    my $bib_iterations = 10;
216
    my $count = 10;
217
    for my $i (1..($count-1)) { # one already built earlier
218
        my $t_item = build_a_test_item($biblio, $biblioitem);
219
        $t_item->itype($item->itype)->store;
220
        $t_item->homebranch($item->homebranch)->store;
221
    }
222
223
    my @items = $biblio->items;
224
    is(@items, $count, "We will get availability for $count items.");
225
    my $res1 = timethis($bib_iterations, sub {
226
        Koha::Biblio::Availability::Hold->new({ biblio => $biblio, patron => $patron })->in_opac;
227
    });
228
    ok($res1, "Calculated search availability $bib_iterations times.");
229
};
230
231
$schema->storage->txn_rollback;
232
233
1;

Return to bug 17712