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

(-)a/C4/Reserves.pm (-54 / +11 lines)
Lines 899-946 Cancels all reserves with an expiration date from before today. Link Here
899
=cut
899
=cut
900
900
901
sub CancelExpiredReserves {
901
sub CancelExpiredReserves {
902
    my $today = dt_from_string();
903
    my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
902
904
903
    # Cancel reserves that have passed their expiration date.
904
    my $dbh = C4::Context->dbh;
905
    my $dbh = C4::Context->dbh;
905
    my $sth = $dbh->prepare( "
906
    my $sth = $dbh->prepare( "
906
        SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
907
        SELECT * FROM reserves WHERE DATE(expirationdate) < DATE( CURDATE() )
907
        AND expirationdate IS NOT NULL
908
        AND expirationdate IS NOT NULL
908
        AND found IS NULL
909
    " );
909
    " );
910
    $sth->execute();
910
    $sth->execute();
911
911
912
    while ( my $res = $sth->fetchrow_hashref() ) {
912
    while ( my $res = $sth->fetchrow_hashref() ) {
913
        CancelReserve({ reserve_id => $res->{'reserve_id'} });
913
        my $calendar = Koha::Calendar->new( branchcode => $res->{'branchcode'} );
914
    }
914
        my $cancel_params = { reserve_id => $res->{'reserve_id'} };
915
916
    # Cancel reserves that have been waiting too long
917
    if ( C4::Context->preference("ExpireReservesMaxPickUpDelay") ) {
918
        my $max_pickup_delay = C4::Context->preference("ReservesMaxPickUpDelay");
919
        my $cancel_on_holidays = C4::Context->preference('ExpireReservesOnHolidays');
920
921
        my $today = dt_from_string();
922
923
        my $query = "SELECT * FROM reserves WHERE TO_DAYS( NOW() ) - TO_DAYS( waitingdate ) > ? AND found = 'W' AND priority = 0";
924
        $sth = $dbh->prepare( $query );
925
        $sth->execute( $max_pickup_delay );
926
927
        while ( my $res = $sth->fetchrow_hashref ) {
928
            my $do_cancel = 1;
929
            unless ( $cancel_on_holidays ) {
930
                my $calendar = Koha::Calendar->new( branchcode => $res->{'branchcode'} );
931
                my $is_holiday = $calendar->is_holiday( $today );
932
915
933
                if ( $is_holiday ) {
916
        next if !$cancel_on_holidays && $calendar->is_holiday( $today );
934
                    $do_cancel = 0;
935
                }
936
            }
937
917
938
            if ( $do_cancel ) {
918
        if ( $res->{found} eq 'W' ) {
939
                CancelReserve({ reserve_id => $res->{'reserve_id'}, charge_cancel_fee => 1 });
919
            $cancel_params->{charge_cancel_fee} = 1;
940
            }
941
        }
920
        }
942
    }
943
921
922
        CancelReserve($cancel_params);
923
    }
944
}
924
}
945
925
946
=head2 AutoUnsuspendReserves
926
=head2 AutoUnsuspendReserves
Lines 1219-1251 sub ModReserveAffect { Link Here
1219
1199
1220
    return unless $hold;
1200
    return unless $hold;
1221
1201
1222
    $reserve_id = $hold->id();
1223
1224
    my $already_on_shelf = $hold->found && $hold->found eq 'W';
1202
    my $already_on_shelf = $hold->found && $hold->found eq 'W';
1225
1203
1226
    # If we affect a reserve that has to be transferred, don't set to Waiting
1204
    $hold->itemnumber($itemnumber);
1227
    my $query;
1205
    $hold->set_waiting($transferToDo);
1228
    if ($transferToDo) {
1229
        $hold->set(
1230
            {
1231
                priority   => 0,
1232
                itemnumber => $itemnumber,
1233
                found      => 'T',
1234
            }
1235
        );
1236
    }
1237
    else {
1238
        # affect the reserve to Waiting as well.
1239
        $hold->set(
1240
            {
1241
                priority    => 0,
1242
                itemnumber  => $itemnumber,
1243
                found       => 'W',
1244
                waitingdate => dt_from_string(),
1245
            }
1246
        );
1247
    }
1248
    $hold->store();
1249
1206
1250
    _koha_notify_reserve( $hold->reserve_id )
1207
    _koha_notify_reserve( $hold->reserve_id )
1251
      if ( !$transferToDo && !$already_on_shelf );
1208
      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 155-160 INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` Link Here
155
('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
('ExpireReservesMaxPickUpDelay','0','','Enabling this allows holds to expire automatically if they have not been picked by within the time period specified in ReservesMaxPickUpDelay','YesNo'),
156
('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
('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'),
157
('ExpireReservesOnHolidays', '1', NULL, 'If false, reserves at a library will not be canceled on days the library is not open.', 'YesNo'),
157
('ExpireReservesOnHolidays', '1', NULL, 'If false, reserves at a library will not be canceled on days the library is not open.', 'YesNo'),
158
('ExcludeHolidaysFromMaxPickUpDelay', '0', NULL, 'If ON, reserves max pickup delay takes into accountthe closed days.', 'YesNo'),
158
('ExportCircHistory', 0, NULL, "Display the export circulation options",  'YesNo' ),
159
('ExportCircHistory', 0, NULL, "Display the export circulation options",  'YesNo' ),
159
('ExportRemoveFields','',NULL,'List of fields for non export in circulation.pl (separated by a space)','Free'),
160
('ExportRemoveFields','',NULL,'List of fields for non export in circulation.pl (separated by a space)','Free'),
160
('ExtendedPatronAttributes','0',NULL,'Use extended patron IDs and attributes','YesNo'),
161
('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 624-629 Circulation: Link Here
624
                  no: "Don't allow"
624
                  no: "Don't allow"
625
            - expired holds to be canceled on days the library is closed.
625
            - expired holds to be canceled on days the library is closed.
626
        -
626
        -
627
            - pref: ExcludeHolidaysFromMaxPickUpDelay
628
              choices:
629
                  yes: Allow
630
                  no: "Don't allow"
631
            - Closed days to be taken into account in reserves max pickup delay.
632
        -
627
            - pref: decreaseLoanHighHolds
633
            - pref: decreaseLoanHighHolds
628
              choices:
634
              choices:
629
                  yes: Enable
635
                  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 432-474 is(CanItemBeReserved($borrowernumbers[0], $itemnumber), Link Here
432
is(CanItemBeReserved($borrowernumbers[0], $itemnumber), 'OK',
432
is(CanItemBeReserved($borrowernumbers[0], $itemnumber), 'OK',
433
    "CanItemBeReserved should returns 'OK'");
433
    "CanItemBeReserved should returns 'OK'");
434
434
435
436
# Test CancelExpiredReserves
437
t::lib::Mocks::mock_preference('ExpireReservesMaxPickUpDelay', 1);
438
t::lib::Mocks::mock_preference('ReservesMaxPickUpDelay', 1);
439
440
my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime(time);
441
$year += 1900;
442
$mon += 1;
443
my $reserves = $dbh->selectall_arrayref('SELECT * FROM reserves', { Slice => {} });
444
$reserve = $reserves->[0];
445
my $calendar = C4::Calendar->new(branchcode => $reserve->{branchcode});
446
$calendar->insert_single_holiday(
447
    day         => $mday,
448
    month       => $mon,
449
    year        => $year,
450
    title       => 'Test',
451
    description => 'Test',
452
);
453
$reserve_id = $reserve->{reserve_id};
454
$dbh->do("UPDATE reserves SET waitingdate = DATE_SUB( NOW(), INTERVAL 5 DAY ), found = 'W', priority = 0 WHERE reserve_id = ?", undef, $reserve_id );
455
t::lib::Mocks::mock_preference('ExpireReservesOnHolidays', 0);
456
CancelExpiredReserves();
457
my $count = $dbh->selectrow_array("SELECT COUNT(*) FROM reserves WHERE reserve_id = ?", undef, $reserve_id );
458
is( $count, 1, "Waiting reserve beyond max pickup delay *not* canceled on holiday" );
459
t::lib::Mocks::mock_preference('ExpireReservesOnHolidays', 1);
460
CancelExpiredReserves();
461
$count = $dbh->selectrow_array("SELECT COUNT(*) FROM reserves WHERE reserve_id = ?", undef, $reserve_id );
462
is( $count, 0, "Waiting reserve beyond max pickup delay canceled on holiday" );
463
464
# Test expirationdate
465
$reserve = $reserves->[1];
466
$reserve_id = $reserve->{reserve_id};
467
$dbh->do("UPDATE reserves SET expirationdate = DATE_SUB( NOW(), INTERVAL 1 DAY ) WHERE reserve_id = ?", undef, $reserve_id );
468
CancelExpiredReserves();
469
$count = $dbh->selectrow_array("SELECT COUNT(*) FROM reserves WHERE reserve_id = ?", undef, $reserve_id );
470
is( $count, 0, "Reserve with manual expiration date canceled correctly" );
471
472
# Bug 12632
435
# Bug 12632
473
t::lib::Mocks::mock_preference( 'item-level_itypes',     1 );
436
t::lib::Mocks::mock_preference( 'item-level_itypes',     1 );
474
t::lib::Mocks::mock_preference( 'ReservesControlBranch', 'PatronLibrary' );
437
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