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 (+404 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
    if ( $self->item_level_recall == 0 ) {
106
        my @itemnumbers = Koha::Items->search({ biblionumber => $self->biblionumber }, { columns => [ 'itemnumber' ] });
107
        my $checkouts = Koha::Checkouts->search({ itemnumber => [ @itemnumbers ] }, { order_by => { -asc => 'date_due' } });
108
        $self->{_checkout} = $checkouts->next;
109
    }
110
111
    return $self->{_checkout};
112
}
113
114
=head3 requested
115
116
    if ( $recall->requested )
117
118
    [% IF recall.requested %]
119
120
Return true if recall status is requested.
121
122
=cut
123
124
sub requested {
125
    my ( $self ) = @_;
126
    my $status = $self->status;
127
    return $status && $status eq 'R';
128
}
129
130
=head3 waiting
131
132
    if ( $recall->waiting )
133
134
    [% IF recall.waiting %]
135
136
Return true if recall is awaiting pickup.
137
138
=cut
139
140
sub waiting {
141
    my ( $self ) = @_;
142
    my $status = $self->status;
143
    return $status && $status eq 'W';
144
}
145
146
=head3 overdue
147
148
    if ( $recall->overdue )
149
150
    [% IF recall.overdue %]
151
152
Return true if recall is overdue to be returned.
153
154
=cut
155
156
sub overdue {
157
    my ( $self ) = @_;
158
    my $status = $self->status;
159
    return $status && $status eq 'O';
160
}
161
162
=head3 in_transit
163
164
    if ( $recall->in_transit )
165
166
    [% IF recall.in_transit %]
167
168
Return true if recall is in transit.
169
170
=cut
171
172
sub in_transit {
173
    my ( $self ) = @_;
174
    my $status = $self->status;
175
    return $status && $status eq 'T';
176
}
177
178
=head3 expired
179
180
    if ( $recall->expired )
181
182
    [% IF recall.expired %]
183
184
Return true if recall has expired.
185
186
=cut
187
188
sub expired {
189
    my ( $self ) = @_;
190
    my $status = $self->status;
191
    return $status && $status eq 'E';
192
}
193
194
=head3 cancelled
195
196
    if ( $recall->cancelled )
197
198
    [% IF recall.cancelled %]
199
200
Return true if recall has been cancelled.
201
202
=cut
203
204
sub cancelled {
205
    my ( $self ) = @_;
206
    my $status = $self->status;
207
    return $status && $status eq 'C';
208
}
209
210
=head3 finished
211
212
    if ( $recall->finished )
213
214
    [% IF recall.finished %]
215
216
Return true if recall is finished and has been fulfilled.
217
218
=cut
219
220
sub finished {
221
    my ( $self ) = @_;
222
    my $status = $self->status;
223
    return $status && $status eq 'F';
224
}
225
226
=head3 calc_expirationdate
227
228
    my $expirationdate = $recall->calc_expirationdate;
229
    $recall->update({ expirationdate => $expirationdate });
230
231
Calculate the expirationdate to set based on circulation rules and system preferences.
232
233
=cut
234
235
sub calc_expirationdate {
236
    my ( $self ) = @_;
237
238
    my $branchcode = C4::Circulation::_GetCircControlBranch( $self->item->unblessed, $self->patron->unblessed );
239
    my $rule = Koha::CirculationRules->get_effective_rule({
240
        categorycode => $self->patron->categorycode,
241
        branchcode => $branchcode,
242
        itemtype => $self->item->effective_itemtype,
243
        rule_name => 'recall_shelf_time'
244
    });
245
246
    my $shelf_time = defined $rule ? $rule->rule_value : C4::Context->preference('RecallsMaxPickUpDelay');
247
248
    my $expirationdate = dt_from_string->add( days => $shelf_time );
249
    return $expirationdate;
250
}
251
252
=head3 start_transfer
253
254
    $recall->start_transfer({ item => $item_object });
255
256
Set the recall as in transit.
257
258
=cut
259
260
sub start_transfer {
261
    my ( $self, $params ) = @_;
262
263
    if ( $self->item_level_recall ) {
264
        # already has an itemnumber
265
        $self->update({ status => 'T' });
266
    } else {
267
        my $itemnumber = $params->{item}->itemnumber;
268
        $self->update({ status => 'T', itemnumber => $itemnumber });
269
    }
270
271
    my $ignore_reserves = 1;
272
    my ( $dotransfer, $messages ) = C4::Circulation::transferbook( $self->branchcode, $self->item->barcode, $ignore_reserves, 'Recalled' );
273
274
    return $self;
275
}
276
277
=head3 set_waiting
278
279
    $recall->set_waiting({
280
        expirationdate => $expirationdate,
281
        item => $item_object
282
    });
283
284
Set the recall as waiting and update expiration date.
285
Notify the recall requester.
286
287
=cut
288
289
sub set_waiting {
290
    my ( $self, $params ) = @_;
291
292
    my $itemnumber;
293
    if ( $self->item_level_recall ) {
294
        $itemnumber = $self->itemnumber;
295
        $self->update({ status => 'W', waitingdate => dt_from_string, expirationdate => $params->{expirationdate} });
296
    } else {
297
        # biblio-level recall with no itemnumber. need to set itemnumber
298
        $itemnumber = $params->{item}->itemnumber;
299
        $self->update({ status => 'W', waitingdate => dt_from_string, expirationdate => $params->{expirationdate}, itemnumber => $itemnumber });
300
    }
301
302
    # send notice to recaller to pick up item
303
    my $letter = C4::Letters::GetPreparedLetter(
304
        module => 'circulation',
305
        letter_code => 'PICKUP_RECALLED_ITEM',
306
        branchcode => $self->branchcode,
307
        want_librarian => 0,
308
        tables => {
309
            biblio => $self->biblionumber,
310
            borrowers => $self->borrowernumber,
311
            items => $itemnumber,
312
            recalls => $self->recall_id,
313
        },
314
    );
315
316
    C4::Message->enqueue($letter, $self->patron->unblessed, 'email');
317
318
    return $self;
319
}
320
321
=head3 revert_waiting
322
323
    $recall->revert_waiting;
324
325
Revert recall waiting status.
326
327
=cut
328
329
sub revert_waiting {
330
    my ( $self ) = @_;
331
    $self->update({ status => 'R', waitingdate => undef });
332
    return $self;
333
}
334
335
=head3 set_overdue
336
337
    $recall->set_overdue;
338
339
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.
340
341
=cut
342
343
sub set_overdue {
344
    my ( $self ) = @_;
345
    $self->update({ status => 'O' });
346
    C4::Log::logaction( 'RECALLS', 'OVERDUE', $self->recall_id, "Recall status set to overdue", 'COMMANDLINE') if ( C4::Context->preference('RecallsLog') );
347
    return $self;
348
}
349
350
=head3 set_expired
351
352
    $recall->set_expired({ location => 'INTRANET' });
353
354
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.
355
356
=cut
357
358
sub set_expired {
359
    my ( $self, $params ) = @_;
360
    my $location = $params->{location} || 'COMMANDLINE';
361
    $self->update({ status => 'E', old => 1, expirationdate => dt_from_string });
362
    C4::Log::logaction( 'RECALLS', 'EXPIRE', $self->recall_id, "Recall expired", $location) if ( C4::Context->preference('RecallsLog') );
363
    return $self;
364
}
365
366
=head3 set_cancelled
367
368
    $recall->set_cancelled;
369
370
Set a recall as cancelled. This may be done manually, either by the borrower that placed the recall, or by the library.
371
372
=cut
373
374
sub set_cancelled {
375
    my ( $self ) = @_;
376
    $self->update({ status => 'C', old => 1, cancellationdate => dt_from_string });
377
    C4::Log::logaction( 'RECALLS', 'CANCEL', $self->recall_id, "Recall cancelled", 'INTRANET') if ( C4::Context->preference('RecallsLog') );
378
    return $self;
379
}
380
381
=head3 set_finished
382
383
    $recall->set_finished;
384
385
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.
386
387
=cut
388
389
sub set_finished {
390
    my ( $self ) = @_;
391
    $self->update({ status => 'F', old => 1 });
392
    C4::Log::logaction( 'RECALLS', 'FULFILL', $self->recall_id, "Recall fulfilled", 'INTRANET') if ( C4::Context->preference('RecallsLog') );
393
    return $self;
394
}
395
396
=head3 _type
397
398
=cut
399
400
sub _type {
401
    return 'Recall';
402
}
403
404
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/Recalls.t (-1 / +141 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
C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode );
58
59
my $recall1 = Koha::Recall->new({
60
    borrowernumber => $patron1->borrowernumber,
61
    recalldate => dt_from_string,
62
    biblionumber => $biblio1->biblionumber,
63
    branchcode => $branch1,
64
    status => 'R',
65
    itemnumber => $item1->itemnumber,
66
    expirationdate => undef,
67
    item_level_recall => 1
68
})->store;
69
70
is( $recall1->biblio->title, $biblio1->title, "Recall biblio relationship correctly linked" );
71
is( $recall1->item->homebranch, $item1->homebranch, "Recall item relationship correctly linked" );
72
is( $recall1->patron->categorycode, $category1, "Recall patron relationship correctly linked" );
73
is( $recall1->library->branchname, Koha::Libraries->find( $branch1 )->branchname, "Recall library relationship correctly linked" );
74
is( $recall1->checkout->itemnumber, $item1->itemnumber, "Recall checkout relationship correctly linked" );
75
is( $recall1->requested, 1, "Recall has been requested" );
76
77
$recall1->set_overdue;
78
is( $recall1->overdue, 1, "Recall is overdue" );
79
80
$recall1->set_cancelled;
81
is( $recall1->cancelled, 1, "Recall is cancelled" );
82
83
my $recall2 = Koha::Recall->new({
84
    borrowernumber => $patron1->borrowernumber,
85
    recalldate => dt_from_string,
86
    biblionumber => $biblio1->biblionumber,
87
    branchcode => $branch1,
88
    status => 'R',
89
    itemnumber => $item1->itemnumber,
90
    expirationdate => undef,
91
    item_level_recall => 1
92
})->store;
93
94
t::lib::Mocks->mock_preference( 'RecallsMaxPickUpDelay', 7 );
95
my $expected_expirationdate = dt_from_string->add({ days => 7 });
96
my $expirationdate = $recall2->calc_expirationdate;
97
is( $expirationdate, $expected_expirationdate, "Expiration date calculated based on system preference as no circulation rules are set" );
98
99
Koha::CirculationRules->set_rule({
100
    branchcode => undef,
101
    categorycode => undef,
102
    itemtype => undef,
103
    rule_name => 'recall_shelf_time',
104
    rule_value => '3',
105
});
106
$expected_expirationdate = dt_from_string->add({ days => 3 });
107
$expirationdate = $recall2->calc_expirationdate;
108
is( $expirationdate, $expected_expirationdate, "Expiration date calculated based on circulation rules" );
109
110
$recall2->set_waiting({ expirationdate => $expirationdate });
111
is( $recall2->waiting, 1, "Recall is waiting" );
112
113
my $notice = C4::Message->find_last_message( $patron1->unblessed, 'PICKUP_RECALLED_ITEM', 'email' );
114
ok( defined $notice, "Patron was notified to pick up waiting recall" );
115
116
$recall2->set_expired({ location => 'COMMANDLINE' });
117
is( $recall2->expired, 1, "Recall has expired" );
118
119
my $old_recalls_count = Koha::Recalls->search({ old => 1 })->count;
120
is( $old_recalls_count, 2, "Recalls have been flagged as old when cancelled or expired" );
121
122
my $recall3 = Koha::Recall->new({
123
    borrowernumber => $patron1->borrowernumber,
124
    recalldate => dt_from_string,
125
    biblionumber => $biblio1->biblionumber,
126
    branchcode => $branch1,
127
    status => 'R',
128
    itemnumber => $item1->itemnumber,
129
    expirationdate => undef,
130
    item_level_recall => 1
131
})->store;
132
133
# test that recall gets T status
134
$recall3->start_transfer;
135
is( $recall3->in_transit, 1, "Recall is in transit" );
136
137
# for testing purposes, pretend the item gets checked out
138
$recall3->set_finished;
139
is( $recall3->finished, 1, "Recall has been fulfilled" );
140
141
$schema->storage->txn_rollback();

Return to bug 19532