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

(-)a/C4/Reserves.pm (-54 / +11 lines)
Lines 940-987 Cancels all reserves with an expiration date from before today. Link Here
940
=cut
940
=cut
941
941
942
sub CancelExpiredReserves {
942
sub CancelExpiredReserves {
943
    my $today = dt_from_string();
944
    my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
943
945
944
    # Cancel reserves that have passed their expiration date.
945
    my $dbh = C4::Context->dbh;
946
    my $dbh = C4::Context->dbh;
946
    my $sth = $dbh->prepare( "
947
    my $sth = $dbh->prepare( "
947
        SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
948
        SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
948
        AND expirationdate IS NOT NULL
949
        AND expirationdate IS NOT NULL
949
        AND found IS NULL
950
    " );
950
    " );
951
    $sth->execute();
951
    $sth->execute();
952
952
953
    while ( my $res = $sth->fetchrow_hashref() ) {
953
    while ( my $res = $sth->fetchrow_hashref() ) {
954
        CancelReserve({ reserve_id => $res->{'reserve_id'} });
954
        my $calendar = Koha::Calendar->new( branchcode => $res->{'branchcode'} );
955
    }
955
        my $cancel_params = { reserve_id => $res->{'reserve_id'} };
956
957
    # Cancel reserves that have been waiting too long
958
    if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) {
959
        my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
960
        my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
961
962
        my $today = dt_from_string();
963
964
        my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
965
        $sth = $dbh->prepare( $query );
966
        $sth->execute( $max_pickup_delay );
967
968
        while ( my $res = $sth->fetchrow_hashref ) {
969
            my $do_cancel = 1;
970
            unless ( $cancel_on_holidays ) {
971
                my $calendar = Koha::Calendar->new( branchcode => $res->{'branchcode'} );
972
                my $is_holiday = $calendar->is_holiday( $today );
973
956
974
                if ( $is_holiday ) {
957
        next if !$cancel_on_holidays && $calendar->is_holiday( $today );
975
                    $do_cancel = 0;
976
                }
977
            }
978
958
979
            if ( $do_cancel ) {
959
        if ( $res->{found} eq 'W' ) {
980
                CancelReserve({ reserve_id => $res->{'reserve_id'}, charge_cancel_fee => 1 });
960
            $cancel_params->{charge_cancel_fee} = 1;
981
            }
982
        }
961
        }
983
    }
984
962
963
        CancelReserve($cancel_params);
964
    }
985
}
965
}
986
966
987
=head2 AutoUnsuspendReserves
967
=head2 AutoUnsuspendReserves
Lines 1260-1292 sub ModReserveAffect { Link Here
1260
1240
1261
    return unless $hold;
1241
    return unless $hold;
1262
1242
1263
    $reserve_id = $hold->id();
1264
1265
    my $already_on_shelf = $hold->found && $hold->found eq 'W';
1243
    my $already_on_shelf = $hold->found && $hold->found eq 'W';
1266
1244
1267
    # If we affect a reserve that has to be transferred, don't set to Waiting
1245
    $hold->itemnumber($itemnumber);
1268
    my $query;
1246
    $hold->set_waiting($transferToDo);
1269
    if ($transferToDo) {
1270
        $hold->set(
1271
            {
1272
                priority   => 0,
1273
                itemnumber => $itemnumber,
1274
                found      => 'T',
1275
            }
1276
        );
1277
    }
1278
    else {
1279
        # affect the reserve to Waiting as well.
1280
        $hold->set(
1281
            {
1282
                priority    => 0,
1283
                itemnumber  => $itemnumber,
1284
                found       => 'W',
1285
                waitingdate => dt_from_string(),
1286
            }
1287
        );
1288
    }
1289
    $hold->store();
1290
1247
1291
    _koha_notify_reserve( $hold->reserve_id )
1248
    _koha_notify_reserve( $hold->reserve_id )
1292
      if ( !$transferToDo && !$already_on_shelf );
1249
      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 154-159 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
154
('ExpireReservesMaxPickUpDelay','0','','Enabling this allows holds to expire automatically if they have not been picked by within the time period specified in ReservesMaxPickUpDelay','YesNo'),
154
('ExpireReservesMaxPickUpDelay','0','','Enabling this allows holds to expire automatically if they have not been picked by within the time period specified in ReservesMaxPickUpDelay','YesNo'),
155
('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'),
155
('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'),
156
('ExpireReservesOnHolidays', '1', NULL, 'If false, reserves at a library will not be canceled on days the library is not open.', 'YesNo'),
156
('ExpireReservesOnHolidays', '1', NULL, 'If false, reserves at a library will not be canceled on days the library is not open.', 'YesNo'),
157
('ExcludeHolidaysFromMaxPickUpDelay', '0', NULL, 'If ON, reserves max pickup delay takes into accountthe closed days.', 'YesNo'),
157
('ExportCircHistory', 0, NULL, "Display the export circulation options",  'YesNo' ),
158
('ExportCircHistory', 0, NULL, "Display the export circulation options",  'YesNo' ),
158
('ExportRemoveFields','',NULL,'List of fields for non export in circulation.pl (separated by a space)','Free'),
159
('ExportRemoveFields','',NULL,'List of fields for non export in circulation.pl (separated by a space)','Free'),
159
('ExtendedPatronAttributes','0',NULL,'Use extended patron IDs and attributes','YesNo'),
160
('ExtendedPatronAttributes','0',NULL,'Use extended patron IDs and attributes','YesNo'),
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref (+6 lines)
Lines 618-623 Circulation: Link Here
618
                  no: "Don't allow"
618
                  no: "Don't allow"
619
            - expired holds to be canceled on days the library is closed.
619
            - expired holds to be canceled on days the library is closed.
620
        -
620
        -
621
            - pref: ExcludeHolidaysFromMaxPickUpDelay
622
              choices:
623
                  yes: Allow
624
                  no: "Don't allow"
625
            - Closed days to be taken into account in reserves max pickup delay.
626
        -
621
            - pref: decreaseLoanHighHolds
627
            - pref: decreaseLoanHighHolds
622
              choices:
628
              choices:
623
                  yes: Enable
629
                  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 422-464 is(CanItemBeReserved($borrowernumbers[0], $itemnumber), Link Here
422
is(CanItemBeReserved($borrowernumbers[0], $itemnumber), 'OK',
422
is(CanItemBeReserved($borrowernumbers[0], $itemnumber), 'OK',
423
    "CanItemBeReserved should returns 'OK'");
423
    "CanItemBeReserved should returns 'OK'");
424
424
425
426
# Test CancelExpiredReserves
427
t::lib::Mocks::mock_preference('ExpireReservesMaxPickUpDelay', 1);
428
t::lib::Mocks::mock_preference('ReservesMaxPickUpDelay', 1);
429
430
my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime(time);
431
$year += 1900;
432
$mon += 1;
433
my $reserves = $dbh->selectall_arrayref('SELECT * FROM reserves', { Slice => {} });
434
$reserve = $reserves->[0];
435
my $calendar = C4::Calendar->new(branchcode => $reserve->{branchcode});
436
$calendar->insert_single_holiday(
437
    day         => $mday,
438
    month       => $mon,
439
    year        => $year,
440
    title       => 'Test',
441
    description => 'Test',
442
);
443
$reserve_id = $reserve->{reserve_id};
444
$dbh->do("UPDATE reserves SET waitingdate = DATE_SUB( NOW(), INTERVAL 5 DAY ), found = 'W', priority = 0 WHERE reserve_id = ?", undef, $reserve_id );
445
t::lib::Mocks::mock_preference('ExpireReservesOnHolidays', 0);
446
CancelExpiredReserves();
447
my $count = $dbh->selectrow_array("SELECT COUNT(*) FROM reserves WHERE reserve_id = ?", undef, $reserve_id );
448
is( $count, 1, "Waiting reserve beyond max pickup delay *not* canceled on holiday" );
449
t::lib::Mocks::mock_preference('ExpireReservesOnHolidays', 1);
450
CancelExpiredReserves();
451
$count = $dbh->selectrow_array("SELECT COUNT(*) FROM reserves WHERE reserve_id = ?", undef, $reserve_id );
452
is( $count, 0, "Waiting reserve beyond max pickup delay canceled on holiday" );
453
454
# Test expirationdate
455
$reserve = $reserves->[1];
456
$reserve_id = $reserve->{reserve_id};
457
$dbh->do("UPDATE reserves SET expirationdate = DATE_SUB( NOW(), INTERVAL 1 DAY ) WHERE reserve_id = ?", undef, $reserve_id );
458
CancelExpiredReserves();
459
$count = $dbh->selectrow_array("SELECT COUNT(*) FROM reserves WHERE reserve_id = ?", undef, $reserve_id );
460
is( $count, 0, "Reserve with manual expiration date canceled correctly" );
461
462
# Bug 12632
425
# Bug 12632
463
t::lib::Mocks::mock_preference( 'item-level_itypes',     1 );
426
t::lib::Mocks::mock_preference( 'item-level_itypes',     1 );
464
t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
427
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