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

(-)a/C4/Letters.pm (+1 lines)
Lines 757-762 sub _parseletter_sth { Link Here
757
    ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
757
    ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
758
    ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
758
    ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
759
    ($table eq 'problem_reports') ? "SELECT * FROM $table WHERE reportid = ?" :
759
    ($table eq 'problem_reports') ? "SELECT * FROM $table WHERE reportid = ?" :
760
    ($table eq 'recalls') ? "SELECT * FROM $table WHERE recall_id = ?" :
760
    undef ;
761
    undef ;
761
    unless ($query) {
762
    unless ($query) {
762
        warn "ERROR: No _parseletter_sth query for table '$table'";
763
        warn "ERROR: No _parseletter_sth query for table '$table'";
(-)a/Koha/Recall.pm (+417 lines)
Line 0 Link Here
1
package Koha::Recall;
2
3
# Copyright 2020 Aleisha Amohia <aleisha@catalyst.net.nz>
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 Koha::Database;
23
use Koha::DateUtils;
24
25
use base qw(Koha::Object);
26
27
=head1 NAME
28
29
Koha::Recall - Koha Recall Object class
30
31
=head1 API
32
33
=head2 Internal methods
34
35
=cut
36
37
=head3 biblio
38
39
    my $biblio = $recall->biblio;
40
41
Returns the related Koha::Biblio object for this recall.
42
43
=cut
44
45
sub biblio {
46
    my ( $self ) = @_;
47
    my $biblio_rs = $self->_result->biblio;
48
    return Koha::Biblio->_new_from_dbic( $biblio_rs );
49
}
50
51
=head3 item
52
53
    my $item = $recall->item;
54
55
Returns the related Koha::Item object for this recall.
56
57
=cut
58
59
sub item {
60
    my ( $self ) = @_;
61
    my $item_rs = $self->_result->item;
62
    return Koha::Item->_new_from_dbic( $item_rs );
63
}
64
65
=head3 patron
66
67
    my $patron = $recall->patron;
68
69
Returns the related Koha::Patron object for this recall.
70
71
=cut
72
73
sub patron {
74
    my ( $self ) = @_;
75
    my $patron_rs = $self->_result->borrower;
76
    return Koha::Patron->_new_from_dbic( $patron_rs );
77
}
78
79
=head3 library
80
81
    my $library = $recall->library;
82
83
Returns the related Koha::Library object for this recall.
84
85
=cut
86
87
sub library {
88
    my ( $self ) = @_;
89
    my $library_rs = $self->_result->branch;
90
    return Koha::Library->_new_from_dbic( $library_rs );
91
}
92
93
=head3 checkout
94
95
    my $checkout = $recall->checkout;
96
97
Returns the related Koha::Checkout object for this recall.
98
99
=cut
100
101
sub checkout {
102
    my ( $self ) = @_;
103
    $self->{_checkout} ||= Koha::Checkouts->find({ itemnumber => $self->itemnumber });
104
105
    unless ( $self->item_level_recall ) {
106
        # Only look at checkouts of items that are allowed to be recalled, and get the oldest one
107
        my @items = Koha::Items->search({ biblionumber => $self->biblionumber });
108
        my @itemnumbers;
109
        foreach (@items) {
110
            my $recalls_allowed = Koha::CirculationRules->get_effective_rule({
111
                branchcode => C4::Context->userenv->{'branch'},
112
                categorycode => $self->patron->categorycode,
113
                itemtype => $_->effective_itemtype,
114
                rule_name => 'recalls_allowed',
115
            });
116
            if ( defined $recalls_allowed and $recalls_allowed > 0 ) {
117
                push ( @itemnumbers, $_->itemnumber );
118
            }
119
        }
120
        my $checkouts = Koha::Checkouts->search({ itemnumber => [ @itemnumbers ] }, { order_by => { -asc => 'date_due' } });
121
        $self->{_checkout} = $checkouts->next;
122
    }
123
124
    return $self->{_checkout};
125
}
126
127
=head3 requested
128
129
    if ( $recall->requested )
130
131
    [% IF recall.requested %]
132
133
Return true if recall status is requested.
134
135
=cut
136
137
sub requested {
138
    my ( $self ) = @_;
139
    my $status = $self->status;
140
    return $status && $status eq 'R';
141
}
142
143
=head3 waiting
144
145
    if ( $recall->waiting )
146
147
    [% IF recall.waiting %]
148
149
Return true if recall is awaiting pickup.
150
151
=cut
152
153
sub waiting {
154
    my ( $self ) = @_;
155
    my $status = $self->status;
156
    return $status && $status eq 'W';
157
}
158
159
=head3 overdue
160
161
    if ( $recall->overdue )
162
163
    [% IF recall.overdue %]
164
165
Return true if recall is overdue to be returned.
166
167
=cut
168
169
sub overdue {
170
    my ( $self ) = @_;
171
    my $status = $self->status;
172
    return $status && $status eq 'O';
173
}
174
175
=head3 in_transit
176
177
    if ( $recall->in_transit )
178
179
    [% IF recall.in_transit %]
180
181
Return true if recall is in transit.
182
183
=cut
184
185
sub in_transit {
186
    my ( $self ) = @_;
187
    my $status = $self->status;
188
    return $status && $status eq 'T';
189
}
190
191
=head3 expired
192
193
    if ( $recall->expired )
194
195
    [% IF recall.expired %]
196
197
Return true if recall has expired.
198
199
=cut
200
201
sub expired {
202
    my ( $self ) = @_;
203
    my $status = $self->status;
204
    return $status && $status eq 'E';
205
}
206
207
=head3 cancelled
208
209
    if ( $recall->cancelled )
210
211
    [% IF recall.cancelled %]
212
213
Return true if recall has been cancelled.
214
215
=cut
216
217
sub cancelled {
218
    my ( $self ) = @_;
219
    my $status = $self->status;
220
    return $status && $status eq 'C';
221
}
222
223
=head3 finished
224
225
    if ( $recall->finished )
226
227
    [% IF recall.finished %]
228
229
Return true if recall is finished and has been fulfilled.
230
231
=cut
232
233
sub finished {
234
    my ( $self ) = @_;
235
    my $status = $self->status;
236
    return $status && $status eq 'F';
237
}
238
239
=head3 calc_expirationdate
240
241
    my $expirationdate = $recall->calc_expirationdate;
242
    $recall->update({ expirationdate => $expirationdate });
243
244
Calculate the expirationdate to set based on circulation rules and system preferences.
245
246
=cut
247
248
sub calc_expirationdate {
249
    my ( $self ) = @_;
250
251
    my $branchcode = C4::Circulation::_GetCircControlBranch( $self->item->unblessed, $self->patron->unblessed );
252
    my $rule = Koha::CirculationRules->get_effective_rule({
253
        categorycode => $self->patron->categorycode,
254
        branchcode => $branchcode,
255
        itemtype => $self->item->effective_itemtype,
256
        rule_name => 'recall_shelf_time'
257
    });
258
259
    my $shelf_time = defined $rule ? $rule->rule_value : C4::Context->preference('RecallsMaxPickUpDelay');
260
261
    my $expirationdate = dt_from_string->add( days => $shelf_time );
262
    return $expirationdate;
263
}
264
265
=head3 start_transfer
266
267
    $recall->start_transfer({ item => $item_object });
268
269
Set the recall as in transit.
270
271
=cut
272
273
sub start_transfer {
274
    my ( $self, $params ) = @_;
275
276
    if ( $self->item_level_recall ) {
277
        # already has an itemnumber
278
        $self->update({ status => 'T' });
279
    } else {
280
        my $itemnumber = $params->{item}->itemnumber;
281
        $self->update({ status => 'T', itemnumber => $itemnumber });
282
    }
283
284
    my $ignore_reserves = 1;
285
    my ( $dotransfer, $messages ) = C4::Circulation::transferbook( $self->branchcode, $self->item->barcode, $ignore_reserves, 'Recalled' );
286
287
    return $self;
288
}
289
290
=head3 set_waiting
291
292
    $recall->set_waiting({
293
        expirationdate => $expirationdate,
294
        item => $item_object
295
    });
296
297
Set the recall as waiting and update expiration date.
298
Notify the recall requester.
299
300
=cut
301
302
sub set_waiting {
303
    my ( $self, $params ) = @_;
304
305
    my $itemnumber;
306
    if ( $self->item_level_recall ) {
307
        $itemnumber = $self->itemnumber;
308
        $self->update({ status => 'W', waitingdate => dt_from_string, expirationdate => $params->{expirationdate} });
309
    } else {
310
        # biblio-level recall with no itemnumber. need to set itemnumber
311
        $itemnumber = $params->{item}->itemnumber;
312
        $self->update({ status => 'W', waitingdate => dt_from_string, expirationdate => $params->{expirationdate}, itemnumber => $itemnumber });
313
    }
314
315
    # send notice to recaller to pick up item
316
    my $letter = C4::Letters::GetPreparedLetter(
317
        module => 'circulation',
318
        letter_code => 'PICKUP_RECALLED_ITEM',
319
        branchcode => $self->branchcode,
320
        want_librarian => 0,
321
        tables => {
322
            biblio => $self->biblionumber,
323
            borrowers => $self->borrowernumber,
324
            items => $itemnumber,
325
            recalls => $self->recall_id,
326
        },
327
    );
328
329
    C4::Message->enqueue($letter, $self->patron->unblessed, 'email');
330
331
    return $self;
332
}
333
334
=head3 revert_waiting
335
336
    $recall->revert_waiting;
337
338
Revert recall waiting status.
339
340
=cut
341
342
sub revert_waiting {
343
    my ( $self ) = @_;
344
    $self->update({ status => 'R', waitingdate => undef });
345
    return $self;
346
}
347
348
=head3 set_overdue
349
350
    $recall->set_overdue;
351
352
Set a recall as overdue when the recall has been requested and the borrower who has checked out the recalled item is late to return it. This will only be used by a cron - a recall cannot be set as overdue manually.
353
354
=cut
355
356
sub set_overdue {
357
    my ( $self ) = @_;
358
    $self->update({ status => 'O' });
359
    C4::Log::logaction( 'RECALLS', 'OVERDUE', $self->recall_id, "Recall status set to overdue", 'COMMANDLINE') if ( C4::Context->preference('RecallsLog') );
360
    return $self;
361
}
362
363
=head3 set_expired
364
365
    $recall->set_expired({ location => 'INTRANET' });
366
367
Set a recall as expired. This may be done manually or by a cronjob, either when the borrower that placed the recall takes more than RecallsMaxPickUpDelay number of days to collect their item, or if the specified expirationdate passes. The location is either 'INTRANET' or 'COMMANDLINE' for logging purposes.
368
369
=cut
370
371
sub set_expired {
372
    my ( $self, $params ) = @_;
373
    my $location = $params->{location} || 'COMMANDLINE';
374
    $self->update({ status => 'E', old => 1, expirationdate => dt_from_string });
375
    C4::Log::logaction( 'RECALLS', 'EXPIRE', $self->recall_id, "Recall expired", $location) if ( C4::Context->preference('RecallsLog') );
376
    return $self;
377
}
378
379
=head3 set_cancelled
380
381
    $recall->set_cancelled;
382
383
Set a recall as cancelled. This may be done manually, either by the borrower that placed the recall, or by the library.
384
385
=cut
386
387
sub set_cancelled {
388
    my ( $self ) = @_;
389
    $self->update({ status => 'C', old => 1, cancellationdate => dt_from_string });
390
    C4::Log::logaction( 'RECALLS', 'CANCEL', $self->recall_id, "Recall cancelled", 'INTRANET') if ( C4::Context->preference('RecallsLog') );
391
    return $self;
392
}
393
394
=head3 set_finished
395
396
    $recall->set_finished;
397
398
Set a recall as finished. This should only be called when the item allocated to a recall is checked out to the borrower who requested the recall.
399
400
=cut
401
402
sub set_finished {
403
    my ( $self ) = @_;
404
    $self->update({ status => 'F', old => 1 });
405
    C4::Log::logaction( 'RECALLS', 'FULFILL', $self->recall_id, "Recall fulfilled", 'INTRANET') if ( C4::Context->preference('RecallsLog') );
406
    return $self;
407
}
408
409
=head3 _type
410
411
=cut
412
413
sub _type {
414
    return 'Recall';
415
}
416
417
1;
(-)a/Koha/Recalls.pm (+53 lines)
Line 0 Link Here
1
package Koha::Recalls;
2
3
# Copyright 2020 Aleisha Amohia <aleisha@catalyst.net.nz>
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 Koha::Database;
23
use Koha::Recall;
24
25
use base qw(Koha::Objects);
26
27
=head1 NAME
28
29
Koha::Recalls - Koha Recalls Object set class
30
31
=head1 API
32
33
=head2 Internal methods
34
35
=cut
36
37
=head3 _type
38
39
=cut
40
41
sub _type {
42
    return 'Recall';
43
}
44
45
=head3 object_class
46
47
=cut
48
49
sub object_class {
50
    return 'Koha::Recall';
51
}
52
53
1;
(-)a/Koha/Schema/Result/Recall.pm (+38 lines)
Lines 273-276 __PACKAGE__->add_columns( Link Here
273
    '+item_level_recall' => { is_boolean => 1 },
273
    '+item_level_recall' => { is_boolean => 1 },
274
);
274
);
275
275
276
__PACKAGE__->belongs_to(
277
  "biblio",
278
  "Koha::Schema::Result::Biblio",
279
  { biblionumber => "biblionumber" },
280
  { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
281
);
282
283
__PACKAGE__->belongs_to(
284
  "item",
285
  "Koha::Schema::Result::Item",
286
  { itemnumber => "itemnumber" },
287
  {
288
    is_deferrable => 1,
289
    join_type     => "LEFT",
290
    on_delete     => "CASCADE",
291
    on_update     => "CASCADE",
292
  },
293
);
294
295
__PACKAGE__->belongs_to(
296
  "borrower",
297
  "Koha::Schema::Result::Borrower",
298
  { borrowernumber => "borrowernumber" },
299
  { is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
300
);
301
302
__PACKAGE__->belongs_to(
303
  "branch",
304
  "Koha::Schema::Result::Branch",
305
  { branchcode => "branchcode" },
306
  {
307
    is_deferrable => 1,
308
    join_type     => "LEFT",
309
    on_delete     => "CASCADE",
310
    on_update     => "CASCADE",
311
  },
312
);
313
276
1;
314
1;
(-)a/t/db_dependent/Koha/Recall.t (-1 / +157 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
use Test::More tests => 18;
21
use t::lib::TestBuilder;
22
use t::lib::Mocks;
23
24
use Koha::DateUtils;
25
26
BEGIN {
27
    require_ok('Koha::Recall');
28
    require_ok('Koha::Recalls');
29
}
30
31
# Start transaction
32
33
my $database = Koha::Database->new();
34
my $schema = $database->schema();
35
$schema->storage->txn_begin();
36
my $dbh = C4::Context->dbh;
37
38
my $builder = t::lib::TestBuilder->new;
39
40
# Setup test variables
41
42
my $item1 = $builder->build_sample_item();
43
my $biblio1 = $item1->biblio;
44
my $branch1 = $item1->holdingbranch;
45
my $itemtype1 = $item1->effective_itemtype;
46
47
my $item2 = $builder->build_sample_item();
48
my $biblio2 = $item2->biblio;
49
my $branch2 = $item2->holdingbranch;
50
my $itemtype2 = $item2->effective_itemtype;
51
52
my $category1 = $builder->build({ source => 'Category' })->{ categorycode };
53
my $patron1 = $builder->build_object({ class => 'Koha::Patrons', value => { categorycode => $category1, branchcode => $branch1 } });
54
my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => { categorycode => $category1, branchcode => $branch1 } });
55
t::lib::Mocks::mock_userenv({ patron => $patron1 });
56
57
Koha::CirculationRules->set_rule({
58
    branchcode => undef,
59
    categorycode => undef,
60
    itemtype => undef,
61
    rule_name => 'recalls_allowed',
62
    rule_value => '10',
63
});
64
65
C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode );
66
67
my $recall1 = Koha::Recall->new({
68
    borrowernumber => $patron1->borrowernumber,
69
    recalldate => dt_from_string,
70
    biblionumber => $biblio1->biblionumber,
71
    branchcode => $branch1,
72
    status => 'R',
73
    itemnumber => $item1->itemnumber,
74
    expirationdate => undef,
75
    item_level_recall => 1
76
})->store;
77
78
is( $recall1->biblio->title, $biblio1->title, "Recall biblio relationship correctly linked" );
79
is( $recall1->item->homebranch, $item1->homebranch, "Recall item relationship correctly linked" );
80
is( $recall1->patron->categorycode, $category1, "Recall patron relationship correctly linked" );
81
is( $recall1->library->branchname, Koha::Libraries->find( $branch1 )->branchname, "Recall library relationship correctly linked" );
82
is( $recall1->checkout->itemnumber, $item1->itemnumber, "Recall checkout relationship correctly linked" );
83
is( $recall1->requested, 1, "Recall has been requested" );
84
85
$recall1->set_overdue;
86
is( $recall1->overdue, 1, "Recall is overdue" );
87
88
$recall1->set_cancelled;
89
is( $recall1->cancelled, 1, "Recall is cancelled" );
90
91
my $recall2 = Koha::Recall->new({
92
    borrowernumber => $patron1->borrowernumber,
93
    recalldate => dt_from_string,
94
    biblionumber => $biblio1->biblionumber,
95
    branchcode => $branch1,
96
    status => 'R',
97
    itemnumber => $item1->itemnumber,
98
    expirationdate => undef,
99
    item_level_recall => 1
100
})->store;
101
102
Koha::CirculationRules->set_rule({
103
    branchcode => undef,
104
    categorycode => undef,
105
    itemtype => undef,
106
    rule_name => 'recall_shelf_time',
107
    rule_value => undef,
108
});
109
110
t::lib::Mocks->mock_preference( 'RecallsMaxPickUpDelay', 7 );
111
my $expected_expirationdate = dt_from_string->add({ days => 7 });
112
my $expirationdate = $recall2->calc_expirationdate;
113
is( $expirationdate, $expected_expirationdate, "Expiration date calculated based on system preference as no circulation rules are set" );
114
115
Koha::CirculationRules->set_rule({
116
    branchcode => undef,
117
    categorycode => undef,
118
    itemtype => undef,
119
    rule_name => 'recall_shelf_time',
120
    rule_value => '3',
121
});
122
$expected_expirationdate = dt_from_string->add({ days => 3 });
123
$expirationdate = $recall2->calc_expirationdate;
124
is( $expirationdate, $expected_expirationdate, "Expiration date calculated based on circulation rules" );
125
126
$recall2->set_waiting({ expirationdate => $expirationdate });
127
is( $recall2->waiting, 1, "Recall is waiting" );
128
129
my $notice = C4::Message->find_last_message( $patron1->unblessed, 'PICKUP_RECALLED_ITEM', 'email' );
130
ok( defined $notice, "Patron was notified to pick up waiting recall" );
131
132
$recall2->set_expired({ location => 'COMMANDLINE' });
133
is( $recall2->expired, 1, "Recall has expired" );
134
135
my $old_recalls_count = Koha::Recalls->search({ old => 1 })->count;
136
is( $old_recalls_count, 2, "Recalls have been flagged as old when cancelled or expired" );
137
138
my $recall3 = Koha::Recall->new({
139
    borrowernumber => $patron1->borrowernumber,
140
    recalldate => dt_from_string,
141
    biblionumber => $biblio1->biblionumber,
142
    branchcode => $branch1,
143
    status => 'R',
144
    itemnumber => $item1->itemnumber,
145
    expirationdate => undef,
146
    item_level_recall => 1
147
})->store;
148
149
# test that recall gets T status
150
$recall3->start_transfer;
151
is( $recall3->in_transit, 1, "Recall is in transit" );
152
153
# for testing purposes, pretend the item gets checked out
154
$recall3->set_finished;
155
is( $recall3->finished, 1, "Recall has been fulfilled" );
156
157
$schema->storage->txn_rollback();

Return to bug 19532