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