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 (+402 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
    return $self;
347
}
348
349
=head3 set_expired
350
351
    $recall->set_expired;
352
353
Set a recall as expired. This may be done manually or by a cronjob, either when the borrower tha t placed the recall takes more than RecallsMaxPickUpDelay number of days to collect their item, or if the specified expirationdate passes.
354
355
=cut
356
357
sub set_expired {
358
    my ( $self ) = @_;
359
    $self->update({ status => 'E', old => 1, expirationdate => dt_from_string });
360
    C4::Log::logaction( 'RECALLS', 'EXPIRE', $self->recall_id, "Recall expired", 'COMMANDLINE') if ( C4::Context->preference('RecallsLog') );
361
    return $self;
362
}
363
364
=head3 set_cancelled
365
366
    $recall->set_cancelled;
367
368
Set a recall as cancelled. This may be done manually, either by the borrower that placed the recall, or by the library.
369
370
=cut
371
372
sub set_cancelled {
373
    my ( $self ) = @_;
374
    $self->update({ status => 'C', old => 1, cancellationdate => dt_from_string });
375
    C4::Log::logaction( 'RECALLS', 'CANCEL', $self->recall_id, "Recall cancelled", 'INTRANET') if ( C4::Context->preference('RecallsLog') );
376
    return $self;
377
}
378
379
=head3 set_finished
380
381
    $recall->set_finished;
382
383
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.
384
385
=cut
386
387
sub set_finished {
388
    my ( $self ) = @_;
389
    $self->update({ status => 'F', old => 1 });
390
    C4::Log::logaction( 'RECALLS', 'FULFILL', $self->recall_id, "Recall fulfilled", 'INTRANET') if ( C4::Context->preference('RecallsLog') );
391
    return $self;
392
}
393
394
=head3 _type
395
396
=cut
397
398
sub _type {
399
    return 'Recall';
400
}
401
402
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/Recalls.t (-1 / +137 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 => 17;
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;
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
# for testing purposes, pretend the item gets checked out
134
$recall3->set_finished;
135
is( $recall3->finished, 1, "Recall has been fulfilled" );
136
137
$schema->storage->txn_rollback();

Return to bug 19532