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/C4/Stats.pm (-1 / +1 lines)
Lines 83-89 sub UpdateStats { Link Here
83
    return () if ! defined $params;
83
    return () if ! defined $params;
84
# change these arrays if new types of transaction or new parameters are allowed
84
# change these arrays if new types of transaction or new parameters are allowed
85
    my @allowed_keys = qw (type branch amount other itemnumber itemtype borrowernumber ccode location);
85
    my @allowed_keys = qw (type branch amount other itemnumber itemtype borrowernumber ccode location);
86
    my @allowed_circulation_types = qw (renew issue localuse return onsite_checkout);
86
    my @allowed_circulation_types = qw (renew issue localuse return onsite_checkout recall);
87
    my @allowed_accounts_types = qw (writeoff payment);
87
    my @allowed_accounts_types = qw (writeoff payment);
88
    my @circulation_mandatory_keys = qw (type branch borrowernumber itemnumber ccode itemtype);
88
    my @circulation_mandatory_keys = qw (type branch borrowernumber itemnumber ccode itemtype);
89
    my @accounts_mandatory_keys = qw (type branch borrowernumber amount);
89
    my @accounts_mandatory_keys = qw (type branch borrowernumber amount);
(-)a/Koha/Recall.pm (+455 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
    if ( $item_rs ){
63
        return Koha::Item->_new_from_dbic( $item_rs );
64
    }
65
    return;
66
}
67
68
=head3 patron
69
70
    my $patron = $recall->patron;
71
72
Returns the related Koha::Patron object for this recall.
73
74
=cut
75
76
sub patron {
77
    my ( $self ) = @_;
78
    my $patron_rs = $self->_result->borrower;
79
    return Koha::Patron->_new_from_dbic( $patron_rs );
80
}
81
82
=head3 library
83
84
    my $library = $recall->library;
85
86
Returns the related Koha::Library object for this recall.
87
88
=cut
89
90
sub library {
91
    my ( $self ) = @_;
92
    $self->{_library} = Koha::Libraries->find( $self->branchcode );
93
    return $self->{_library};
94
}
95
96
=head3 checkout
97
98
    my $checkout = $recall->checkout;
99
100
Returns the related Koha::Checkout object for this recall.
101
102
=cut
103
104
sub checkout {
105
    my ( $self ) = @_;
106
    $self->{_checkout} ||= Koha::Checkouts->find({ itemnumber => $self->itemnumber });
107
108
    unless ( $self->item_level_recall ) {
109
        # Only look at checkouts of items that are allowed to be recalled, and get the oldest one
110
        my @items = Koha::Items->search({ biblionumber => $self->biblionumber });
111
        my @itemnumbers;
112
        foreach (@items) {
113
            my $recalls_allowed = Koha::CirculationRules->get_effective_rule({
114
                branchcode => C4::Context->userenv->{'branch'},
115
                categorycode => $self->patron->categorycode,
116
                itemtype => $_->effective_itemtype,
117
                rule_name => 'recalls_allowed',
118
            });
119
            if ( defined $recalls_allowed and $recalls_allowed->rule_value > 0 ) {
120
                push ( @itemnumbers, $_->itemnumber );
121
            }
122
        }
123
        my $checkouts = Koha::Checkouts->search({ itemnumber => [ @itemnumbers ] }, { order_by => { -asc => 'date_due' } });
124
        $self->{_checkout} = $checkouts->next;
125
    }
126
127
    return $self->{_checkout};
128
}
129
130
=head3 requested
131
132
    if ( $recall->requested )
133
134
    [% IF recall.requested %]
135
136
Return true if recall status is requested.
137
138
=cut
139
140
sub requested {
141
    my ( $self ) = @_;
142
    my $status = $self->status;
143
    return $status && $status eq 'R';
144
}
145
146
=head3 waiting
147
148
    if ( $recall->waiting )
149
150
    [% IF recall.waiting %]
151
152
Return true if recall is awaiting pickup.
153
154
=cut
155
156
sub waiting {
157
    my ( $self ) = @_;
158
    my $status = $self->status;
159
    return $status && $status eq 'W';
160
}
161
162
=head3 overdue
163
164
    if ( $recall->overdue )
165
166
    [% IF recall.overdue %]
167
168
Return true if recall is overdue to be returned.
169
170
=cut
171
172
sub overdue {
173
    my ( $self ) = @_;
174
    my $status = $self->status;
175
    return $status && $status eq 'O';
176
}
177
178
=head3 in_transit
179
180
    if ( $recall->in_transit )
181
182
    [% IF recall.in_transit %]
183
184
Return true if recall is in transit.
185
186
=cut
187
188
sub in_transit {
189
    my ( $self ) = @_;
190
    my $status = $self->status;
191
    return $status && $status eq 'T';
192
}
193
194
=head3 expired
195
196
    if ( $recall->expired )
197
198
    [% IF recall.expired %]
199
200
Return true if recall has expired.
201
202
=cut
203
204
sub expired {
205
    my ( $self ) = @_;
206
    my $status = $self->status;
207
    return $status && $status eq 'E';
208
}
209
210
=head3 cancelled
211
212
    if ( $recall->cancelled )
213
214
    [% IF recall.cancelled %]
215
216
Return true if recall has been cancelled.
217
218
=cut
219
220
sub cancelled {
221
    my ( $self ) = @_;
222
    my $status = $self->status;
223
    return $status && $status eq 'C';
224
}
225
226
=head3 finished
227
228
    if ( $recall->finished )
229
230
    [% IF recall.finished %]
231
232
Return true if recall is finished and has been fulfilled.
233
234
=cut
235
236
sub finished {
237
    my ( $self ) = @_;
238
    my $status = $self->status;
239
    return $status && $status eq 'F';
240
}
241
242
=head3 calc_expirationdate
243
244
    my $expirationdate = $recall->calc_expirationdate;
245
    $recall->update({ expirationdate => $expirationdate });
246
247
Calculate the expirationdate to set based on circulation rules and system preferences.
248
249
=cut
250
251
sub calc_expirationdate {
252
    my ( $self ) = @_;
253
254
    my $item;
255
    if ( $self->item_level_recall ) {
256
        $item = $self->item;
257
    } elsif ( $self->checkout ) {
258
        $item = $self->checkout->item;
259
    }
260
261
    my $branchcode = $self->patron->branchcode;
262
    if ( $item ) {
263
        $branchcode = C4::Circulation::_GetCircControlBranch( $self->item->unblessed, $self->patron->unblessed );
264
    }
265
266
    my $rule = Koha::CirculationRules->get_effective_rule({
267
        categorycode => $self->patron->categorycode,
268
        branchcode => $branchcode,
269
        itemtype => $item ? $self->item->effective_itemtype : undef,
270
        rule_name => 'recall_shelf_time'
271
    });
272
273
    my $shelf_time = defined $rule ? $rule->rule_value : C4::Context->preference('RecallsMaxPickUpDelay');
274
275
    my $expirationdate = dt_from_string->add( days => $shelf_time );
276
    return $expirationdate;
277
}
278
279
=head3 start_transfer
280
281
    my ( $recall, $dotransfer, $messages ) = $recall->start_transfer({ item => $item_object });
282
283
Set the recall as in transit.
284
285
=cut
286
287
sub start_transfer {
288
    my ( $self, $params ) = @_;
289
290
    if ( $self->item_level_recall ) {
291
        # already has an itemnumber
292
        $self->update({ status => 'T' });
293
    } else {
294
        my $itemnumber = $params->{item}->itemnumber;
295
        $self->update({ status => 'T', itemnumber => $itemnumber });
296
    }
297
298
    my $ignore_reserves = 1;
299
    my ( $dotransfer, $messages ) = C4::Circulation::transferbook( $self->branchcode, $self->item->barcode, $ignore_reserves, 'Recall' );
300
301
    return ( $self, $dotransfer, $messages );
302
}
303
304
=head3 revert_transfer
305
306
    $recall->revert_transfer;
307
308
If a transfer is cancelled, revert the recall to requested.
309
310
=cut
311
312
sub revert_transfer {
313
    my ( $self ) = @_;
314
315
    if ( $self->item_level_recall ) {
316
        $self->update({ status => 'R' });
317
    } else {
318
        $self->update({ status => 'R', itemnumber => undef });
319
    }
320
321
    return $self;
322
}
323
324
=head3 set_waiting
325
326
    $recall->set_waiting({
327
        expirationdate => $expirationdate,
328
        item => $item_object
329
    });
330
331
Set the recall as waiting and update expiration date.
332
Notify the recall requester.
333
334
=cut
335
336
sub set_waiting {
337
    my ( $self, $params ) = @_;
338
339
    my $itemnumber;
340
    if ( $self->item_level_recall ) {
341
        $itemnumber = $self->itemnumber;
342
        $self->update({ status => 'W', waitingdate => dt_from_string, expirationdate => $params->{expirationdate} });
343
    } else {
344
        # biblio-level recall with no itemnumber. need to set itemnumber
345
        $itemnumber = $params->{item}->itemnumber;
346
        $self->update({ status => 'W', waitingdate => dt_from_string, expirationdate => $params->{expirationdate}, itemnumber => $itemnumber });
347
    }
348
349
    # send notice to recaller to pick up item
350
    my $letter = C4::Letters::GetPreparedLetter(
351
        module => 'circulation',
352
        letter_code => 'PICKUP_RECALLED_ITEM',
353
        branchcode => $self->branchcode,
354
        want_librarian => 0,
355
        tables => {
356
            biblio => $self->biblionumber,
357
            borrowers => $self->borrowernumber,
358
            items => $itemnumber,
359
            recalls => $self->recall_id,
360
        },
361
    );
362
363
    C4::Message->enqueue($letter, $self->patron->unblessed, 'email');
364
365
    return $self;
366
}
367
368
=head3 revert_waiting
369
370
    $recall->revert_waiting;
371
372
Revert recall waiting status.
373
374
=cut
375
376
sub revert_waiting {
377
    my ( $self ) = @_;
378
    if ( $self->item_level_recall ){
379
        $self->update({ status => 'R', waitingdate => undef });
380
    } else {
381
        $self->update({ status => 'R', waitingdate => undef, itemnumber => undef });
382
    }
383
    return $self;
384
}
385
386
=head3 set_overdue
387
388
    $recall->set_overdue;
389
390
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.
391
392
=cut
393
394
sub set_overdue {
395
    my ( $self ) = @_;
396
    $self->update({ status => 'O' });
397
    C4::Log::logaction( 'RECALLS', 'OVERDUE', $self->recall_id, "Recall status set to overdue", 'COMMANDLINE' ) if ( C4::Context->preference('RecallsLog') );
398
    return $self;
399
}
400
401
=head3 set_expired
402
403
    $recall->set_expired({ interface => 'INTRANET' });
404
405
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 interface is either 'INTRANET' or 'COMMANDLINE' for logging purposes.
406
407
=cut
408
409
sub set_expired {
410
    my ( $self, $params ) = @_;
411
    my $interface = $params->{interface} || 'COMMANDLINE';
412
    $self->update({ status => 'E', old => 1, expirationdate => dt_from_string });
413
    C4::Log::logaction( 'RECALLS', 'EXPIRE', $self->recall_id, "Recall expired", $interface ) if ( C4::Context->preference('RecallsLog') );
414
    return $self;
415
}
416
417
=head3 set_cancelled
418
419
    $recall->set_cancelled;
420
421
Set a recall as cancelled. This may be done manually, either by the borrower that placed the recall, or by the library.
422
423
=cut
424
425
sub set_cancelled {
426
    my ( $self ) = @_;
427
    $self->update({ status => 'C', old => 1, cancellationdate => dt_from_string });
428
    C4::Log::logaction( 'RECALLS', 'CANCEL', $self->recall_id, "Recall cancelled", 'INTRANET' ) if ( C4::Context->preference('RecallsLog') );
429
    return $self;
430
}
431
432
=head3 set_finished
433
434
    $recall->set_finished;
435
436
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.
437
438
=cut
439
440
sub set_finished {
441
    my ( $self ) = @_;
442
    $self->update({ status => 'F', old => 1 });
443
    C4::Log::logaction( 'RECALLS', 'FULFILL', $self->recall_id, "Recall fulfilled", 'INTRANET' ) if ( C4::Context->preference('RecallsLog') );
444
    return $self;
445
}
446
447
=head3 _type
448
449
=cut
450
451
sub _type {
452
    return 'Recall';
453
}
454
455
1;
(-)a/Koha/Recalls.pm (+210 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
use Koha::DateUtils;
25
26
use C4::Stats;
27
28
use base qw(Koha::Objects);
29
30
=head1 NAME
31
32
Koha::Recalls - Koha Recalls Object set class
33
34
=head1 API
35
36
=head2 Internal methods
37
38
=cut
39
40
=head3 add_recall
41
42
    my ( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({
43
        patron => $patron_object,
44
        biblio => $biblio_object,
45
        branchcode => $branchcode,
46
        item => $item_object,
47
        expirationdate => $expirationdate,
48
        interface => 'OPAC',
49
    });
50
51
Add a new requested recall. We assume at this point that a recall is allowed to be placed on this item or biblio. We are past the checks and are now doing the recall.
52
Interface param is either OPAC or INTRANET
53
Send a RETURN_RECALLED_ITEM notice.
54
Add statistics and logs.
55
#FIXME: Add recallnotes and priority when staff-side recalls is added
56
57
=cut
58
59
sub add_recall {
60
    my ( $self, $params ) = @_;
61
62
    my $patron = $params->{patron};
63
    my $biblio = $params->{biblio};
64
    return if ( !defined($patron) or !defined($biblio) );
65
    my $branchcode = $params->{branchcode};
66
    $branchcode ||= $patron->branchcode;
67
    my $item = $params->{item};
68
    my $itemnumber = $item ? $item->itemnumber : undef;
69
    my $expirationdate = $params->{expirationdate};
70
    my $interface = $params->{interface};
71
72
    if ( $expirationdate ){
73
        my $now = dt_from_string;
74
        $expirationdate = dt_from_string($expirationdate)->set({ hour => $now->hour, minute => $now->minute, second => $now->second });
75
    }
76
77
    my $recall_request = Koha::Recall->new({
78
        borrowernumber => $patron->borrowernumber,
79
        recalldate => dt_from_string(),
80
        biblionumber => $biblio->biblionumber,
81
        branchcode => $branchcode,
82
        status => 'R',
83
        itemnumber => defined $itemnumber ? $itemnumber : undef,
84
        expirationdate => $expirationdate,
85
        item_level_recall => defined $itemnumber ? 1 : 0,
86
    })->store;
87
88
    if (defined $recall_request->recall_id){ # successful recall
89
        my $recall = Koha::Recalls->find( $recall_request->recall_id );
90
91
        # get checkout and adjust due date based on circulation rules
92
        my $checkout = $recall->checkout;
93
        my $recall_due_date_interval = Koha::CirculationRules->get_effective_rule({
94
            categorycode => $checkout->patron->categorycode,
95
            itemtype => $checkout->item->effective_itemtype,
96
            branchcode => $branchcode,
97
            rule_name => 'recall_due_date_interval',
98
        });
99
        my $due_interval = defined $recall_due_date_interval ? $recall_due_date_interval->rule_value : 5;
100
        my $timestamp = dt_from_string( $recall->timestamp );
101
        my $due_date = $timestamp->add( days => $due_interval );
102
        $checkout->update({ date_due => $due_date });
103
104
        # get itemnumber of most relevant checkout if a biblio-level recall
105
        unless ( $recall->item_level_recall ) { $itemnumber = $checkout->itemnumber; }
106
107
        # send notice to user with recalled item checked out
108
        my $letter = C4::Letters::GetPreparedLetter (
109
            module => 'circulation',
110
            letter_code => 'RETURN_RECALLED_ITEM',
111
            branchcode => $recall->branchcode,
112
            tables => {
113
                biblio => $biblio->biblionumber,
114
                borrowers => $checkout->borrowernumber,
115
                items => $itemnumber,
116
                issues => $itemnumber,
117
            },
118
        );
119
120
        C4::Message->enqueue( $letter, $checkout->patron->unblessed, 'email' );
121
122
        $item = Koha::Items->find( $itemnumber );
123
        # add to statistics table
124
        UpdateStats({
125
            branch => C4::Context->userenv->{'branch'},
126
            type => 'recall',
127
            itemnumber => $itemnumber,
128
            borrowernumber => $recall->borrowernumber,
129
            itemtype => $item->effective_itemtype,
130
            ccode => $item->ccode,
131
        });
132
133
        # add action log
134
        C4::Log::logaction( 'RECALLS', 'CREATE', $recall->recall_id, "Recall requested by borrower #" . $recall->borrowernumber, $interface ) if ( C4::Context->preference('RecallsLog') );
135
136
        return ( $recall, $due_interval, $due_date );
137
    }
138
139
    # unable to add recall
140
    return;
141
}
142
143
=head3 move_recall
144
145
    my $message = Koha::Recalls->move_recall({
146
        recall_id = $recall_id,
147
        action => $action,
148
        itemnumber => $itemnumber,
149
        borrowernumber => $borrowernumber,
150
    });
151
152
A patron is attempting to check out an item that has been recalled by another patron. If the recall is requested/overdue, they have the option of cancelling the recall. If the recall is waiting, they also have the option of reverting the waiting status.
153
154
We can also fulfill the recall here if the recall is placed by this borrower.
155
156
recall_id = ID of the recall to perform the action on
157
action = either cancel or revert
158
itemnumber = itemnumber the patron is attempting to check out
159
borrowernumber = borrowernumber of the patron that is attemptig to check out
160
161
=cut
162
163
sub move_recall {
164
    my ( $self, $params ) = @_;
165
166
    my $recall_id = $params->{recall_id};
167
    my $action = $params->{action};
168
    return 'no recall_id provided' if ( !defined $recall_id );
169
    my $itemnumber = $params->{itemnumber};
170
    my $borrowernumber = $params->{borrowernumber};
171
172
    my $message = 'no action provided';
173
174
    if ( $action and $action eq 'cancel' ) {
175
        my $recall = Koha::Recalls->find( $recall_id );
176
        $recall->set_cancelled;
177
        $message = 'cancelled';
178
    } elsif ( $action and $action eq 'revert' ) {
179
        my $recall = Koha::Recalls->find( $recall_id );
180
        $recall->revert_waiting;
181
        $message = 'reverted';
182
    }
183
184
    if ( $message eq 'no action provided' and $itemnumber and $borrowernumber ) {
185
        # move_recall was not called to revert or cancel, but was called to fulfill
186
        my $recall = Koha::Recalls->find({ borrowernumber => $borrowernumber, itemnumber => $itemnumber, old => undef });
187
        $recall->set_finished;
188
        $message = 'fulfilled';
189
    }
190
191
    return $message;
192
}
193
194
=head3 _type
195
196
=cut
197
198
sub _type {
199
    return 'Recall';
200
}
201
202
=head3 object_class
203
204
=cut
205
206
sub object_class {
207
    return 'Koha::Recall';
208
}
209
210
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 (+188 lines)
Line 0 Link Here
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 => 26;
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
my $old_recalls_count = Koha::Recalls->search({ old => 1 })->count;
57
58
Koha::CirculationRules->set_rule({
59
    branchcode => undef,
60
    categorycode => undef,
61
    itemtype => undef,
62
    rule_name => 'recalls_allowed',
63
    rule_value => '10',
64
});
65
66
C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode );
67
68
my $recall1 = Koha::Recall->new({
69
    borrowernumber => $patron1->borrowernumber,
70
    recalldate => dt_from_string,
71
    biblionumber => $biblio1->biblionumber,
72
    branchcode => $branch1,
73
    status => 'R',
74
    itemnumber => $item1->itemnumber,
75
    expirationdate => undef,
76
    item_level_recall => 1
77
})->store;
78
79
is( $recall1->biblio->title, $biblio1->title, "Recall biblio relationship correctly linked" );
80
is( $recall1->item->homebranch, $item1->homebranch, "Recall item relationship correctly linked" );
81
is( $recall1->patron->categorycode, $category1, "Recall patron relationship correctly linked" );
82
is( $recall1->library->branchname, Koha::Libraries->find( $branch1 )->branchname, "Recall library relationship correctly linked" );
83
is( $recall1->checkout->itemnumber, $item1->itemnumber, "Recall checkout relationship correctly linked" );
84
is( $recall1->requested, 1, "Recall has been requested" );
85
86
$recall1->set_overdue;
87
is( $recall1->overdue, 1, "Recall is overdue" );
88
89
$recall1->set_cancelled;
90
is( $recall1->cancelled, 1, "Recall is cancelled" );
91
92
my $recall2 = Koha::Recall->new({
93
    borrowernumber => $patron1->borrowernumber,
94
    recalldate => dt_from_string,
95
    biblionumber => $biblio1->biblionumber,
96
    branchcode => $branch1,
97
    status => 'R',
98
    itemnumber => $item1->itemnumber,
99
    expirationdate => undef,
100
    item_level_recall => 1
101
})->store;
102
103
Koha::CirculationRules->set_rule({
104
    branchcode => undef,
105
    categorycode => undef,
106
    itemtype => undef,
107
    rule_name => 'recall_shelf_time',
108
    rule_value => undef,
109
});
110
111
t::lib::Mocks->mock_preference( 'RecallsMaxPickUpDelay', 7 );
112
my $expected_expirationdate = dt_from_string->add({ days => 7 });
113
my $expirationdate = $recall2->calc_expirationdate;
114
is( $expirationdate, $expected_expirationdate, "Expiration date calculated based on system preference as no circulation rules are set" );
115
116
Koha::CirculationRules->set_rule({
117
    branchcode => undef,
118
    categorycode => undef,
119
    itemtype => undef,
120
    rule_name => 'recall_shelf_time',
121
    rule_value => '3',
122
});
123
$expected_expirationdate = dt_from_string->add({ days => 3 });
124
$expirationdate = $recall2->calc_expirationdate;
125
is( $expirationdate, $expected_expirationdate, "Expiration date calculated based on circulation rules" );
126
127
$recall2->set_waiting({ expirationdate => $expirationdate });
128
is( $recall2->waiting, 1, "Recall is waiting" );
129
130
my $notice = C4::Message->find_last_message( $patron1->unblessed, 'PICKUP_RECALLED_ITEM', 'email' );
131
ok( defined $notice, "Patron was notified to pick up waiting recall" );
132
133
$recall2->set_expired({ interface => 'COMMANDLINE' });
134
is( $recall2->expired, 1, "Recall has expired" );
135
136
my $old_recalls_count_now = Koha::Recalls->search({ old => 1 })->count;
137
is( $old_recalls_count_now, $old_recalls_count + 2, "Recalls have been flagged as old when cancelled or expired" );
138
139
my $recall3 = Koha::Recall->new({
140
    borrowernumber => $patron1->borrowernumber,
141
    recalldate => dt_from_string,
142
    biblionumber => $biblio1->biblionumber,
143
    branchcode => $branch1,
144
    status => 'R',
145
    itemnumber => $item1->itemnumber,
146
    expirationdate => undef,
147
    item_level_recall => 1
148
})->store;
149
150
# test that recall gets T status
151
$recall3->start_transfer;
152
is( $recall3->in_transit, 1, "Recall is in transit" );
153
154
$recall3->revert_transfer;
155
is( $recall3->requested, 1, "Recall transfer has been cancelled and the status reverted" );
156
is( $recall3->itemnumber, $item1->itemnumber, "Item persists for item-level recall" );
157
158
# for testing purposes, pretend the item gets checked out
159
$recall3->set_finished;
160
is( $recall3->finished, 1, "Recall has been fulfilled" );
161
162
C4::Circulation::AddIssue( $patron2->unblessed, $item1->barcode );
163
my $recall4 = Koha::Recall->new({
164
    borrowernumber => $patron1->borrowernumber,
165
    recalldate => dt_from_string,
166
    biblionumber => $biblio1->biblionumber,
167
    branchcode => $branch1,
168
    status => 'R',
169
    itemnumber => undef,
170
    expirationdate => undef,
171
    item_level_recall => 0,
172
})->store;
173
174
ok( !defined $recall4->item, "No relevant item returned for a biblio-level recall" );
175
is( $recall4->checkout->itemnumber, $item1->itemnumber, "Return most relevant checkout for a biblio-level recall");
176
177
$recall4->set_waiting({ item => $item1, expirationdate => $expirationdate });
178
is( $recall4->itemnumber, $item1->itemnumber, "Item has been allocated to biblio-level recall" );
179
180
$recall4->revert_waiting;
181
ok( !defined $recall4->itemnumber, "Itemnumber has been removed from biblio-level recall when reverting waiting status" );
182
183
$recall4->start_transfer({ item => $item1 });
184
is( $recall4->itemnumber, $item1->itemnumber, "Itemnumber saved to recall when item is transferred" );
185
$recall4->revert_transfer;
186
ok( !defined $recall4->itemnumber, "Itemnumber has been removed from biblio-level recall when reverting transfer status" );
187
188
$schema->storage->txn_rollback();
(-)a/t/db_dependent/Koha/Recalls.t (-1 / +175 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 => 19;
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
my $item2 = $builder->build_sample_item();
47
my $biblio2 = $item1->biblio;
48
my $branch2 = $item1->holdingbranch;
49
my $itemtype2 = $item1->effective_itemtype;
50
51
my $category1 = $builder->build({ source => 'Category' })->{ categorycode };
52
my $patron1 = $builder->build_object({ class => 'Koha::Patrons', value => { categorycode => $category1, branchcode => $branch1 } });
53
my $patron2 = $builder->build_object({ class => 'Koha::Patrons', value => { categorycode => $category1, branchcode => $branch2 } });
54
my $patron3 = $builder->build_object({ class => 'Koha::Patrons', value => { categorycode => $category1, branchcode => $branch1 } });
55
t::lib::Mocks::mock_userenv({ patron => $patron1 });
56
57
Koha::CirculationRules->set_rules({
58
    branchcode => undef,
59
    categorycode => undef,
60
    itemtype => undef,
61
    rules => {
62
        'recall_due_date_interval' => undef,
63
        'recalls_allowed' => 10,
64
    }
65
});
66
67
C4::Circulation::AddIssue( $patron3->unblessed, $item1->barcode );
68
C4::Circulation::AddIssue( $patron3->unblessed, $item2->barcode );
69
70
my ( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({
71
    patron => undef,
72
    biblio => $biblio1,
73
    branchcode => $branch1,
74
    item => $item1,
75
    expirationdate => undef,
76
    interface => 'COMMANDLINE',
77
});
78
ok( !defined $recall, "Can't add a recall without specifying a patron" );
79
80
( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({
81
    patron => $patron1,
82
    biblio => undef,
83
    branchcode => $branch1,
84
    item => $item1,
85
    expirationdate => undef,
86
    interface => 'COMMANDLINE',
87
});
88
ok( !defined $recall, "Can't add a recall without specifying a biblio" );
89
90
( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({
91
    patron => $patron1,
92
    biblio => undef,
93
    branchcode => $branch1,
94
    item => $item1,
95
    expirationdate => undef,
96
    interface => 'COMMANDLINE',
97
});
98
ok( !defined $recall, "Can't add a recall without specifying a biblio" );
99
100
( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({
101
    patron => $patron2,
102
    biblio => $biblio1,
103
    branchcode => undef,
104
    item => $item2,
105
    expirationdate => undef,
106
    interface => 'COMMANDLINE',
107
});
108
is( $recall->branchcode, $branch2, "No pickup branch specified so patron branch used" );
109
is( $due_interval, 5, "Recall due date interval defaults to 5 if not specified" );
110
111
Koha::CirculationRules->set_rule({
112
    branchcode => undef,
113
    categorycode => undef,
114
    itemtype => undef,
115
    rule_name => 'recall_due_date_interval',
116
    rule_value => 3,
117
});
118
( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({
119
    patron => $patron1,
120
    biblio => $biblio1,
121
    branchcode => undef,
122
    item => $item1,
123
    expirationdate => undef,
124
    interface => 'COMMANDLINE',
125
});
126
is( $due_interval, 3, "Recall due date interval is based on circulation rules" );
127
128
( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({
129
    patron => $patron1,
130
    biblio => $biblio1,
131
    branchcode => $branch1,
132
    item => undef,
133
    expirationdate => undef,
134
    interface => 'COMMANDLINE',
135
});
136
is( $recall->item_level_recall, 0, "No item provided so recall not flagged as item-level" );
137
138
my $expected_due_date = dt_from_string->add( days => 3 );
139
is( dt_from_string( $recall->checkout->date_due ), $expected_due_date, "Checkout due date has correctly been extended by recall_due_date_interval days" );
140
is( $due_date, $expected_due_date, "Due date correctly returned" );
141
142
my $messages_count = Koha::Notice::Messages->search({ borrowernumber => $patron3->borrowernumber, letter_code => 'RETURN_RECALLED_ITEM' })->count;
143
is( $messages_count, 3, "RETURN_RECALLED_ITEM notice successfully sent to checkout borrower" );
144
145
my $message = Koha::Recalls->move_recall;
146
is( $message, 'no recall_id provided', "Can't move a recall without specifying which recall" );
147
148
$message = Koha::Recalls->move_recall({ recall_id => $recall->recall_id });
149
is( $message, 'no action provided', "No clear action to perform on recall" );
150
$message = Koha::Recalls->move_recall({ recall_id => $recall->recall_id, action => 'whatever' });
151
is( $message, 'no action provided', "Legal action not provided to perform on recall" );
152
153
$recall->set_waiting({ item => $item1 });
154
ok( $recall->waiting, "Recall is waiting" );
155
Koha::Recalls->move_recall({ recall_id => $recall->recall_id, action => 'revert' });
156
$recall = Koha::Recalls->find( $recall->recall_id );
157
ok( $recall->requested, "Recall reverted to requested with move_recall" );
158
159
Koha::Recalls->move_recall({ recall_id => $recall->recall_id, action => 'cancel' });
160
$recall = Koha::Recalls->find( $recall->recall_id );
161
ok( $recall->cancelled, "Recall cancelled with move_recall" );
162
163
( $recall, $due_interval, $due_date ) = Koha::Recalls->add_recall({
164
    patron => $patron1,
165
    biblio => $biblio1,
166
    branchcode => $branch1,
167
    item => $item2,
168
    expirationdate => undef,
169
    interface => 'COMMANDLINE',
170
});
171
$message = Koha::Recalls->move_recall({ recall_id => $recall->recall_id, itemnumber => $item2->itemnumber, borrowernumber => $patron1->borrowernumber });
172
$recall = Koha::Recalls->find( $recall->recall_id );
173
ok( $recall->finished, "Recall fulfilled with move_recall" );
174
175
$schema->storage->txn_rollback();

Return to bug 19532