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

(-)a/C4/Reserves.pm (-54 / +11 lines)
Lines 1003-1050 Cancels all reserves with an expiration date from before today. Link Here
1003
=cut
1003
=cut
1004
1004
1005
sub CancelExpiredReserves {
1005
sub CancelExpiredReserves {
1006
    my $today = dt_from_string();
1007
    my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
1006
1008
1007
    # Cancel reserves that have passed their expiration date.
1008
    my $dbh = C4::Context->dbh;
1009
    my $dbh = C4::Context->dbh;
1009
    my $sth = $dbh->prepare( "
1010
    my $sth = $dbh->prepare( "
1010
        SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
1011
        SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
1011
        AND expirationdate IS NOT NULL
1012
        AND expirationdate IS NOT NULL
1012
        AND found IS NULL
1013
    " );
1013
    " );
1014
    $sth->execute();
1014
    $sth->execute();
1015
1015
1016
    while ( my $res = $sth->fetchrow_hashref() ) {
1016
    while ( my $res = $sth->fetchrow_hashref() ) {
1017
        CancelReserve({ reserve_id => $res->{'reserve_id'} });
1017
        my $calendar = Koha::Calendar->new( branchcode => $res->{'branchcode'} );
1018
    }
1018
        my $cancel_params = { reserve_id => $res->{'reserve_id'} };
1019
1020
    # Cancel reserves that have been waiting too long
1021
    if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) {
1022
        my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
1023
        my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
1024
1025
        my $today = dt_from_string();
1026
1027
        my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
1028
        $sth = $dbh->prepare( $query );
1029
        $sth->execute( $max_pickup_delay );
1030
1031
        while ( my $res = $sth->fetchrow_hashref ) {
1032
            my $do_cancel = 1;
1033
            unless ( $cancel_on_holidays ) {
1034
                my $calendar = Koha::Calendar->new( branchcode => $res->{'branchcode'} );
1035
                my $is_holiday = $calendar->is_holiday( $today );
1036
1019
1037
                if ( $is_holiday ) {
1020
        next if !$cancel_on_holidays && $calendar->is_holiday( $today );
1038
                    $do_cancel = 0;
1039
                }
1040
            }
1041
1021
1042
            if ( $do_cancel ) {
1022
        if ( $res->{found} eq 'W' ) {
1043
                CancelReserve({ reserve_id => $res->{'reserve_id'}, charge_cancel_fee => 1 });
1023
            $cancel_params->{charge_cancel_fee} = 1;
1044
            }
1045
        }
1024
        }
1046
    }
1047
1025
1026
        CancelReserve($cancel_params);
1027
    }
1048
}
1028
}
1049
1029
1050
=head2 AutoUnsuspendReserves
1030
=head2 AutoUnsuspendReserves
Lines 1318-1350 sub ModReserveAffect { Link Here
1318
1298
1319
    return unless $hold;
1299
    return unless $hold;
1320
1300
1321
    $reserve_id = $hold->id();
1322
1323
    my $already_on_shelf = $hold->found && $hold->found eq 'W';
1301
    my $already_on_shelf = $hold->found && $hold->found eq 'W';
1324
1302
1325
    # If we affect a reserve that has to be transferred, don't set to Waiting
1303
    $hold->itemnumber($itemnumber);
1326
    my $query;
1304
    $hold->set_waiting($transferToDo);
1327
    if ($transferToDo) {
1328
        $hold->set(
1329
            {
1330
                priority   => 0,
1331
                itemnumber => $itemnumber,
1332
                found      => 'T',
1333
            }
1334
        );
1335
    }
1336
    else {
1337
        # affect the reserve to Waiting as well.
1338
        $hold->set(
1339
            {
1340
                priority    => 0,
1341
                itemnumber  => $itemnumber,
1342
                found       => 'W',
1343
                waitingdate => dt_from_string(),
1344
            }
1345
        );
1346
    }
1347
    $hold->store();
1348
1305
1349
    _koha_notify_reserve( $hold->reserve_id )
1306
    _koha_notify_reserve( $hold->reserve_id )
1350
      if ( !$transferToDo && !$already_on_shelf );
1307
      if ( !$transferToDo && !$already_on_shelf );
(-)a/Koha/Calendar.pm (+14 lines)
Lines 296-301 sub prev_open_day { Link Here
296
    return $base_date;
296
    return $base_date;
297
}
297
}
298
298
299
sub days_forward {
300
    my $self     = shift;
301
    my $start_dt = shift;
302
    my $num_days = shift;
303
304
    my $base_dt = $start_dt->clone();
305
306
    while ($num_days--) {
307
        $base_dt = $self->next_open_day($base_dt);
308
    }
309
310
    return $base_dt;
311
}
312
299
sub days_between {
313
sub days_between {
300
    my $self     = shift;
314
    my $self     = shift;
301
    my $start_dt = shift;
315
    my $start_dt = shift;
(-)a/Koha/Hold.pm (-1 / +41 lines)
Lines 25-31 use Data::Dumper qw(Dumper); Link Here
25
use C4::Context qw(preference);
25
use C4::Context qw(preference);
26
use C4::Log;
26
use C4::Log;
27
27
28
use Koha::DateUtils qw(dt_from_string);
28
use Koha::DateUtils qw(dt_from_string output_pref);
29
use Koha::Patrons;
29
use Koha::Patrons;
30
use Koha::Biblios;
30
use Koha::Biblios;
31
use Koha::Items;
31
use Koha::Items;
Lines 131-136 sub waiting_expires_on { Link Here
131
    return $dt;
131
    return $dt;
132
}
132
}
133
133
134
=head3 set_waiting
135
136
=cut
137
138
sub set_waiting {
139
    my ( $self, $transferToDo ) = @_;
140
141
    $self->priority(0);
142
143
    if ($transferToDo) {
144
        $self->found('T')->store();
145
        return $self;
146
    }
147
148
    my $today = dt_from_string();
149
    my $values = {
150
        found => 'W',
151
        waitingdate => $today->ymd,
152
    };
153
154
    if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) {
155
        my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
156
        my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
157
        my $calendar = Koha::Calendar->new( branchcode => $self->branchcode );
158
159
        my $expirationdate = $today->clone;
160
        $expirationdate->add(days => $max_pickup_delay);
161
162
        if ( C4::Context->preference("ExcludeHolidaysFromMaxPickUpDelay") ) {
163
            $expirationdate = $calendar->days_forward( dt_from_string($self->waitingdate), $max_pickup_delay );
164
        }
165
166
        $values->{expirationdate} = $expirationdate->ymd;
167
    }
168
169
    $self->set($values)->store();
170
171
    return $self;
172
}
173
134
=head3 is_found
174
=head3 is_found
135
175
136
Returns true if hold is a waiting or in transit
176
Returns true if hold is a waiting or in transit
(-)a/installer/data/mysql/atomicupdate/bug_12063-add_excludeholidaysfrommaxpickupdelay_syspref.sql (+1 lines)
Line 0 Link Here
1
INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES ('ExcludeHolidaysFromMaxPickUpDelay', '0', 'If ON, reserves max pickup delay takes into account the closed days.', NULL, 'Integer');
(-)a/installer/data/mysql/sysprefs.sql (+1 lines)
Lines 151-156 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
151
('ExpireReservesMaxPickUpDelay','0','','Enabling this allows holds to expire automatically if they have not been picked by within the time period specified in ReservesMaxPickUpDelay','YesNo'),
151
('ExpireReservesMaxPickUpDelay','0','','Enabling this allows holds to expire automatically if they have not been picked by within the time period specified in ReservesMaxPickUpDelay','YesNo'),
152
('ExpireReservesMaxPickUpDelayCharge','0',NULL,'If ExpireReservesMaxPickUpDelay is enabled, and this field has a non-zero value, than a borrower whose waiting hold has expired will be charged this amount.','free'),
152
('ExpireReservesMaxPickUpDelayCharge','0',NULL,'If ExpireReservesMaxPickUpDelay is enabled, and this field has a non-zero value, than a borrower whose waiting hold has expired will be charged this amount.','free'),
153
('ExpireReservesOnHolidays', '1', NULL, 'If false, reserves at a library will not be canceled on days the library is not open.', 'YesNo'),
153
('ExpireReservesOnHolidays', '1', NULL, 'If false, reserves at a library will not be canceled on days the library is not open.', 'YesNo'),
154
('ExcludeHolidaysFromMaxPickUpDelay', '0', NULL, 'If ON, reserves max pickup delay takes into accountthe closed days.', 'YesNo'),
154
('ExtendedPatronAttributes','0',NULL,'Use extended patron IDs and attributes','YesNo'),
155
('ExtendedPatronAttributes','0',NULL,'Use extended patron IDs and attributes','YesNo'),
155
('FacetLabelTruncationLength','20',NULL,'Specify the facet max length in OPAC','Integer'),
156
('FacetLabelTruncationLength','20',NULL,'Specify the facet max length in OPAC','Integer'),
156
('FacetMaxCount','20',NULL,'Specify the max facet count for each category','Integer'),
157
('FacetMaxCount','20',NULL,'Specify the max facet count for each category','Integer'),
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref (+6 lines)
Lines 604-609 Circulation: Link Here
604
                  no: "Don't allow"
604
                  no: "Don't allow"
605
            - expired holds to be canceled on days the library is closed.
605
            - expired holds to be canceled on days the library is closed.
606
        -
606
        -
607
            - pref: ExcludeHolidaysFromMaxPickUpDelay
608
              choices:
609
                  yes: Allow
610
                  no: "Don't allow"
611
            - Closed days to be taken into account in reserves max pickup delay.
612
        -
607
            - pref: decreaseLoanHighHolds
613
            - pref: decreaseLoanHighHolds
608
              choices:
614
              choices:
609
                  yes: Enable
615
                  yes: Enable
(-)a/t/db_dependent/Holds.t (-38 / +1 lines)
Lines 7-13 use t::lib::TestBuilder; Link Here
7
7
8
use C4::Context;
8
use C4::Context;
9
9
10
use Test::More tests => 61;
10
use Test::More tests => 58;
11
use MARC::Record;
11
use MARC::Record;
12
use C4::Biblio;
12
use C4::Biblio;
13
use C4::Items;
13
use C4::Items;
Lines 421-463 is(CanItemBeReserved($borrowernumbers[0], $itemnumber), Link Here
421
is(CanItemBeReserved($borrowernumbers[0], $itemnumber), 'OK',
421
is(CanItemBeReserved($borrowernumbers[0], $itemnumber), 'OK',
422
    "CanItemBeReserved should returns 'OK'");
422
    "CanItemBeReserved should returns 'OK'");
423
423
424
425
# Test CancelExpiredReserves
426
t::lib::Mocks::mock_preference('ExpireReservesMaxPickUpDelay', 1);
427
t::lib::Mocks::mock_preference('ReservesMaxPickUpDelay', 1);
428
429
my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime(time);
430
$year += 1900;
431
$mon += 1;
432
$reserves = $dbh->selectall_arrayref('SELECT * FROM reserves', { Slice => {} });
433
$reserve = $reserves->[0];
434
my $calendar = C4::Calendar->new(branchcode => $reserve->{branchcode});
435
$calendar->insert_single_holiday(
436
    day         => $mday,
437
    month       => $mon,
438
    year        => $year,
439
    title       => 'Test',
440
    description => 'Test',
441
);
442
$reserve_id = $reserve->{reserve_id};
443
$dbh->do("UPDATE reserves SET waitingdate = DATE_SUB( NOW(), INTERVAL 5 DAY ), found = 'W', priority = 0 WHERE reserve_id = ?", undef, $reserve_id );
444
t::lib::Mocks::mock_preference('ExpireReservesOnHolidays', 0);
445
CancelExpiredReserves();
446
my $count = $dbh->selectrow_array("SELECT COUNT(*) FROM reserves WHERE reserve_id = ?", undef, $reserve_id );
447
is( $count, 1, "Waiting reserve beyond max pickup delay *not* canceled on holiday" );
448
t::lib::Mocks::mock_preference('ExpireReservesOnHolidays', 1);
449
CancelExpiredReserves();
450
$count = $dbh->selectrow_array("SELECT COUNT(*) FROM reserves WHERE reserve_id = ?", undef, $reserve_id );
451
is( $count, 0, "Waiting reserve beyond max pickup delay canceled on holiday" );
452
453
# Test expirationdate
454
$reserve = $reserves->[1];
455
$reserve_id = $reserve->{reserve_id};
456
$dbh->do("UPDATE reserves SET expirationdate = DATE_SUB( NOW(), INTERVAL 1 DAY ) WHERE reserve_id = ?", undef, $reserve_id );
457
CancelExpiredReserves();
458
$count = $dbh->selectrow_array("SELECT COUNT(*) FROM reserves WHERE reserve_id = ?", undef, $reserve_id );
459
is( $count, 0, "Reserve with manual expiration date canceled correctly" );
460
461
# Bug 12632
424
# Bug 12632
462
t::lib::Mocks::mock_preference( 'item-level_itypes',     1 );
425
t::lib::Mocks::mock_preference( 'item-level_itypes',     1 );
463
t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
426
t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
(-)a/t/db_dependent/Holds/CancelReserves.t (+101 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
5
use C4::Reserves;
6
use Koha::DateUtils;
7
8
use t::lib::Mocks;
9
use t::lib::TestBuilder;
10
11
use Test::More tests => 5;
12
13
use_ok('C4::Reserves');
14
15
my $schema  = Koha::Database->new->schema;
16
$schema->storage->txn_begin;
17
18
my $dbh = C4::Context->dbh;
19
$dbh->{AutoCommit} = 0;
20
$dbh->{RaiseError} = 1;
21
22
my $builder = t::lib::TestBuilder->new();
23
24
t::lib::Mocks::mock_preference('ExpireReservesOnHolidays', 0);
25
26
my $today = dt_from_string();
27
my $reserve_reservedate = $today->clone;
28
$reserve_reservedate->subtract(days => 30);
29
30
my $reserve1_expirationdate = $today->clone;
31
$reserve1_expirationdate->add(days => 1);
32
33
# Reserve not expired
34
my $reserve1 = $builder->build({
35
    source => 'Reserve',
36
    value => {
37
        reservedate => $reserve_reservedate,
38
        expirationdate => $reserve1_expirationdate,
39
        cancellationdate => undef,
40
        priority => 0,
41
        found => 'W',
42
    },
43
});
44
45
CancelExpiredReserves();
46
my $r1 = Koha::Holds->find($reserve1->{reserve_id});
47
ok($r1, 'Reserve 1 should not be canceled.');
48
49
my $reserve2_expirationdate = $today->clone;
50
$reserve2_expirationdate->subtract(days => 1);
51
52
# Reserve expired
53
my $reserve2 = $builder->build({
54
    source => 'Reserve',
55
    value => {
56
        reservedate => $reserve_reservedate,
57
        expirationdate => $reserve2_expirationdate,
58
        cancellationdate => undef,
59
        priority => 0,
60
        found => 'W',
61
    },
62
});
63
64
CancelExpiredReserves();
65
my $r2 = Koha::Holds->find($reserve2->{reserve_id});
66
is($r2, undef,'Reserve 2 should be canceled.');
67
68
# Reserve expired on holiday
69
my $reserve3 = $builder->build({
70
    source => 'Reserve',
71
    value => {
72
        reservedate => $reserve_reservedate,
73
        expirationdate => $reserve2_expirationdate,
74
        branchcode => 'LIB1',
75
        cancellationdate => undef,
76
        priority => 0,
77
        found => 'W',
78
    },
79
});
80
81
Koha::Cache->get_instance()->flush_all();
82
my $holiday = $builder->build({
83
    source => 'SpecialHoliday',
84
    value => {
85
        branchcode => 'LIB1',
86
        day => $today->day,
87
        month => $today->month,
88
        year => $today->year,
89
        title => 'My holiday',
90
        isexception => 0
91
    },
92
});
93
94
CancelExpiredReserves();
95
my $r3 = Koha::Holds->find($reserve3->{reserve_id});
96
ok($r3,'Reserve 3 should not be canceled.');
97
98
t::lib::Mocks::mock_preference('ExpireReservesOnHolidays', 1);
99
CancelExpiredReserves();
100
$r3 = Koha::Holds->find($reserve3->{reserve_id});
101
is($r3, undef,'Reserve 3 should be canceled.');
(-)a/t/db_dependent/Holds/WaitingReserves.t (-1 / +208 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
5
use C4::Reserves;
6
use Koha::DateUtils;
7
8
use t::lib::Mocks;
9
use t::lib::TestBuilder;
10
11
use Test::More tests => 10;
12
13
use_ok('C4::Reserves');
14
15
my $schema  = Koha::Database->new->schema;
16
$schema->storage->txn_begin;
17
18
my $dbh = C4::Context->dbh;
19
$dbh->{AutoCommit} = 0;
20
$dbh->{RaiseError} = 1;
21
$dbh->do(q{DELETE FROM special_holidays});
22
23
my $builder = t::lib::TestBuilder->new();
24
25
# Category, branch and patrons
26
$builder->build({
27
    source => 'Category',
28
    value  => {
29
        categorycode => 'XYZ1',
30
    },
31
});
32
$builder->build({
33
    source => 'Branch',
34
    value  => {
35
        branchcode => 'LIB1',
36
    },
37
});
38
39
$builder->build({
40
    source => 'Branch',
41
    value  => {
42
        branchcode => 'LIB2',
43
    },
44
});
45
46
my $patron1 = $builder->build({
47
    source => 'Borrower',
48
    value  => {
49
        categorycode => 'XYZ1',
50
        branchcode => 'LIB1',
51
    },
52
});
53
54
my $patron2 = $builder->build({
55
    source => 'Borrower',
56
    value  => {
57
        categorycode => 'XYZ1',
58
        branchcode => 'LIB2',
59
    },
60
});
61
62
my $biblio = $builder->build({
63
    source => 'Biblio',
64
    value  => {
65
        title => 'Title 1',    },
66
});
67
68
my $biblio2 = $builder->build({
69
    source => 'Biblio',
70
    value  => {
71
        title => 'Title 2',    },
72
});
73
74
my $biblio3 = $builder->build({
75
    source => 'Biblio',
76
    value  => {
77
        title => 'Title 3',    },
78
});
79
80
my $item1 = $builder->build({
81
    source => 'Item',
82
    value  => {
83
        biblionumber => $biblio->{biblionumber},
84
    },
85
});
86
87
my $item2 = $builder->build({
88
    source => 'Item',
89
    value  => {
90
        biblionumber => $biblio2->{biblionumber},
91
    },
92
});
93
94
my $item3 = $builder->build({
95
    source => 'Item',
96
    value  => {
97
        biblionumber => $biblio3->{biblionumber},
98
    },
99
});
100
101
my $today = dt_from_string();
102
103
my $reserve1_reservedate = $today->clone;
104
$reserve1_reservedate->subtract(days => 20);
105
106
my $reserve1_expirationdate = $today->clone;
107
$reserve1_expirationdate->add(days => 6);
108
109
my $reserve1 = $builder->build({
110
    source => 'Reserve',
111
    value => {
112
        borrowernumber => $patron1->{borrowernumber},
113
        reservedate => $reserve1_reservedate->ymd,
114
        expirationdate => undef,
115
        biblionumber => $biblio->{biblionumber},
116
        branchcode => 'LIB1',
117
        priority => 1,
118
        found => '',
119
    },
120
});
121
122
t::lib::Mocks::mock_preference('ExpireReservesMaxPickUpDelay', 1);
123
t::lib::Mocks::mock_preference('ReservesMaxPickUpDelay', 6);
124
125
ModReserveAffect( $item1->{itemnumber}, $patron1->{borrowernumber}, 0);
126
my $r = Koha::Holds->find($reserve1->{reserve_id});
127
128
is($r->waitingdate, $today->ymd, 'Waiting date should be set to today' );
129
is($r->expirationdate, $reserve1_expirationdate->ymd, 'Expiration date should be set to today + 6' );
130
is($r->found, 'W', 'Reserve status is now "waiting"' );
131
is($r->priority, 0, 'Priority should be 0' );
132
is($r->itemnumber, $item1->{itemnumber}, 'Item number should be set correctly' );
133
134
my $reserve2 = $builder->build({
135
    source => 'Reserve',
136
    value => {
137
        borrowernumber => $patron2->{borrowernumber},
138
        reservedate => $reserve1_reservedate->ymd,
139
        expirationdate => undef,
140
        biblionumber => $biblio2->{biblionumber},
141
        branchcode => 'LIB1',
142
        priority => 1,
143
        found => '',
144
    },
145
});
146
147
ModReserveAffect( $item2->{itemnumber}, $patron2->{borrowernumber}, 1);
148
my $r2 = Koha::Holds->find($reserve2->{reserve_id});
149
150
is($r2->found, 'T', '2nd reserve - Reserve status is now "To transfer"' );
151
is($r2->priority, 0, '2nd reserve - Priority should be 0' );
152
is($r2->itemnumber, $item2->{itemnumber}, '2nd reserve - Item number should be set correctly' );
153
154
my $reserve3 = $builder->build({
155
    source => 'Reserve',
156
    value => {
157
        borrowernumber => $patron2->{borrowernumber},
158
        reservedate => $reserve1_reservedate->ymd,
159
        expirationdate => undef,
160
        biblionumber => $biblio3->{biblionumber},
161
        branchcode => 'LIB1',
162
        priority => 1,
163
        found => '',
164
    },
165
});
166
167
my $special_holiday1_dt = $today->clone;
168
$special_holiday1_dt->add(days => 2);
169
170
Koha::Cache->get_instance()->flush_all();
171
my $holiday = $builder->build({
172
    source => 'SpecialHoliday',
173
    value => {
174
        branchcode => 'LIB1',
175
        day => $special_holiday1_dt->day,
176
        month => $special_holiday1_dt->month,
177
        year => $special_holiday1_dt->year,
178
        title => 'My special holiday',
179
        isexception => 0
180
    },
181
});
182
183
my $special_holiday2_dt = $today->clone;
184
$special_holiday2_dt->add(days => 4);
185
186
my $holiday2 = $builder->build({
187
    source => 'SpecialHoliday',
188
    value => {
189
        branchcode => 'LIB1',
190
        day => $special_holiday2_dt->day,
191
        month => $special_holiday2_dt->month,
192
        year => $special_holiday2_dt->year,
193
        title => 'My special holiday 2',
194
        isexception => 0
195
    },
196
});
197
198
t::lib::Mocks::mock_preference('ExcludeHolidaysFromMaxPickUpDelay', 1);
199
ModReserveAffect( $item3->{itemnumber}, $patron2->{borrowernumber}, 0);
200
201
# Add 6 days of pickup delay + 1 day of holiday.
202
my $expected_expiration = $today->clone;
203
$expected_expiration->add(days => 8);
204
205
my $r3 = Koha::Holds->find($reserve3->{reserve_id});
206
is($r3->expirationdate, $expected_expiration->ymd, 'Expiration date should be set to today + 7' );
207
208
$dbh->rollback;

Return to bug 12063