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

(-)a/C4/Circulation.pm (+18 lines)
Lines 4143-4148 sub CalcDateDue { Link Here
4143
                # due time doesn't conflict with library open hours, don't need to check
4143
                # due time doesn't conflict with library open hours, don't need to check
4144
                $datedue->add( hours => $loanlength->{$length_key} );
4144
                $datedue->add( hours => $loanlength->{$length_key} );
4145
            }
4145
            }
4146
        } elsif ( $loanlength->{lengthunit} eq 'minutes' ) {
4147
            $datedue->add( minutes => $loanlength->{$length_key} );
4146
        } else {    # days
4148
        } else {    # days
4147
            $datedue->add( days => $loanlength->{$length_key} );
4149
            $datedue->add( days => $loanlength->{$length_key} );
4148
            $datedue->set_hour(23);
4150
            $datedue->set_hour(23);
Lines 4177-4182 sub CalcDateDue { Link Here
4177
                # due time doesn't conflict with library open hours, don't need to check
4179
                # due time doesn't conflict with library open hours, don't need to check
4178
                $dur = DateTime::Duration->new( hours => $loanlength->{$length_key} );
4180
                $dur = DateTime::Duration->new( hours => $loanlength->{$length_key} );
4179
            }
4181
            }
4182
        } elsif ( $loanlength->{lengthunit} eq 'minutes' ) {
4183
            $dur = DateTime::Duration->new( minutes => $loanlength->{$length_key} );
4180
        } else {    # days
4184
        } else {    # days
4181
            $dur = DateTime::Duration->new( days => $loanlength->{$length_key} );
4185
            $dur = DateTime::Duration->new( days => $loanlength->{$length_key} );
4182
        }
4186
        }
Lines 4226-4231 sub CalcDateDue { Link Here
4226
            }
4230
            }
4227
        }
4231
        }
4228
    }
4232
    }
4233
4234
    #check if it's minutes or hourly loan and set due date to close hour, if the current due date is passed closing hours.
4235
    if ( $loanlength->{lengthunit} eq 'hours' || $loanlength->{lengthunit} eq 'minutes' ) {
4236
        my $calendar = Koha::DiscreteCalendar->new( { branchcode => $branch, days_mode => $daysmode } );
4237
        my $dateInfo = $calendar->get_date_info($datedue);
4238
        my $close    = dt_from_string( $dateInfo->{date} . " " . $dateInfo->{close_hour}, 'iso', C4::Context->tz() );
4239
4240
        my $close_datetime = $datedue->clone()->set( hour => $close->hour(), minute => $close->minute() );
4241
4242
        if ( DateTime->compare( $datedue, $close_datetime ) > 0 ) {
4243
            $datedue = $close_datetime->clone();
4244
        }
4245
    }
4246
4229
    return $datedue;
4247
    return $datedue;
4230
}
4248
}
4231
4249
(-)a/C4/Overdues.pm (+11 lines)
Lines 360-365 sub get_chargeable_units { Link Here
360
            return 1;
360
            return 1;
361
        }
361
        }
362
        return $charge_duration;
362
        return $charge_duration;
363
    } elsif ( $unit eq 'minutes' ) {
364
        if ( C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed' ) {
365
            my $calendar = Koha::DiscreteCalendar->new( { branchcode => $branchcode } );
366
            $charge_duration = $calendar->open_minutes_between( $date_due, $date_returned );
367
        } else {
368
            $charge_duration = $date_returned->delta_ms($date_due);
369
        }
370
        if ( $charge_duration == 0 && $charge_duration * 60 > 0 ) {
371
            return 1;
372
        }
373
        return $charge_duration;
363
    } else {    # days
374
    } else {    # days
364
        if ( C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed' ) {
375
        if ( C4::Context->preference('finesCalendar') eq 'noFinesWhenClosed' ) {
365
            my $calendar = Koha::DiscreteCalendar->new({ branchcode => $branchcode });
376
            my $calendar = Koha::DiscreteCalendar->new({ branchcode => $branchcode });
(-)a/Koha/DiscreteCalendar.pm (+100 lines)
Lines 1255-1260 sub open_hours_between { Link Here
1255
    return ($working_hours_between->next()->get_column('hours_between') - $not_used_hours->next()->get_column('not_used_hours'));
1255
    return ($working_hours_between->next()->get_column('hours_between') - $not_used_hours->next()->get_column('not_used_hours'));
1256
}
1256
}
1257
1257
1258
=head2 open_minutes_between
1259
1260
  $minutes = $calendar->open_minutes_between($start_date, $end_date)
1261
1262
Returns the number of minutes between C<$start_date> and C<$end_date>, taking into
1263
account the opening hours of the library.
1264
1265
=cut
1266
1267
sub open_minutes_between {
1268
    my ( $self, $start_date, $end_date ) = @_;
1269
    my $branchcode = $self->{branchcode};
1270
    my $schema     = Koha::Database->new->schema;
1271
    my $dtf        = DateTime::Format::Strptime->new( pattern => "%F %T" );
1272
    $start_date = $dtf->format_datetime($start_date);
1273
    $end_date   = $dtf->format_datetime($end_date);
1274
1275
    my $working_minutes_between = $schema->resultset('DiscreteCalendar')->search(
1276
        {
1277
            branchcode => $branchcode,
1278
            is_opened  => 1,
1279
        },
1280
        {
1281
            select => \['sum(time_to_sec(timediff(close_hour, open_hour)) / 60)'],
1282
            as     => [qw /minutes_between/],
1283
            where  => \[ 'date BETWEEN DATE(?) AND DATE(?)', $start_date, $end_date ]
1284
        }
1285
    );
1286
1287
    my $loan_day = $schema->resultset('DiscreteCalendar')->search(
1288
        {
1289
            branchcode => $branchcode,
1290
        },
1291
        {
1292
            order_by => \[ 'ABS(DATEDIFF(date, ?))', $start_date ],
1293
            rows     => 1,
1294
        }
1295
    );
1296
1297
    my $return_day = $schema->resultset('DiscreteCalendar')->search(
1298
        {
1299
            branchcode => $branchcode,
1300
        },
1301
        {
1302
            order_by => \[ 'ABS(DATEDIFF(date, ?))', $end_date ],
1303
            rows     => 1,
1304
        }
1305
    );
1306
1307
    #Capture the time portion of the date
1308
    $start_date =~ /\s(.*)/;
1309
    my $loan_date_time = $1;
1310
    $end_date =~ /\s(.*)/;
1311
    my $return_date_time = $1;
1312
1313
    my $not_used_minutes = $schema->resultset('DiscreteCalendar')->search(
1314
        {
1315
            branchcode => $branchcode,
1316
            is_opened  => 1,
1317
        },
1318
        {
1319
            select => \[
1320
                '(time_to_sec(timediff(?, ?)) + time_to_sec(timediff(?, ?)) ) / 60', $return_day->next()->close_hour(),
1321
                $return_date_time, $loan_date_time, $loan_day->next()->open_hour()
1322
            ],
1323
            as => [qw /not_used_minutes/],
1324
        }
1325
    );
1326
1327
    return ( $working_minutes_between->next()->get_column('minutes_between') || 0 ) -
1328
        ( $not_used_minutes->next()->get_column('not_used_minutes') || 0 );
1329
}
1330
1258
=head2 addDuration
1331
=head2 addDuration
1259
1332
1260
  my $dt = $calendar->addDuration($date, $dur, $unit)
1333
  my $dt = $calendar->addDuration($date, $dur, $unit)
Lines 1281-1286 sub addDuration { Link Here
1281
        my $return_by_hour = 10;
1354
        my $return_by_hour = 10;
1282
1355
1283
        $dt = $self->addHours($startdate, $add_duration, $return_by_hour);
1356
        $dt = $self->addHours($startdate, $add_duration, $return_by_hour);
1357
    } elsif ( $unit eq 'minutes' ) {
1358
        $dt = $self->addMinutes( $startdate, $add_duration );
1284
    } else {
1359
    } else {
1285
        # days
1360
        # days
1286
        $dt = $self->addDays($startdate, $add_duration);
1361
        $dt = $self->addDays($startdate, $add_duration);
Lines 1323-1328 sub addHours { Link Here
1323
    return $base_date;
1398
    return $base_date;
1324
}
1399
}
1325
1400
1401
=head2 addMinutes
1402
1403
  $end = $calendar->addMinutes($date, $dur)
1404
1405
Add C<$dur> minutes to C<$start> date.
1406
1407
=cut
1408
1409
sub addMinutes {
1410
    my ( $self, $startdate, $minutes_duration ) = @_;
1411
    my $base_date = $startdate->clone();
1412
1413
    $base_date->add_duration($minutes_duration);
1414
1415
    my $dateInfo = $self->get_date_info($base_date);
1416
1417
    my $close_datetime = dt_from_string( $dateInfo->{date} . " " . $dateInfo->{close_hour}, 'iso' );
1418
1419
    if ( DateTime->compare( $base_date, $close_datetime ) > 0 ) {
1420
        return $close_datetime;
1421
    }
1422
1423
    return $base_date;
1424
}
1425
1326
=head2 addDays
1426
=head2 addDays
1327
1427
1328
  $date = $calendar->addDays($start, $duration)
1428
  $date = $calendar->addDays($start, $duration)
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/smart-rules.tt (+3 lines)
Lines 298-303 Link Here
298
                                            <span>Days</span>
298
                                            <span>Days</span>
299
                                        [% ELSIF ( lengthunit == 'hours') %]
299
                                        [% ELSIF ( lengthunit == 'hours') %]
300
                                            <span>Hours</span>
300
                                            <span>Hours</span>
301
                                        [% ELSIF ( lengthunit == 'minutes' ) %]
302
                                            <span>Minutes</span>
301
                                        [% ELSE %]
303
                                        [% ELSE %]
302
                                            <span>Undefined</span>
304
                                            <span>Undefined</span>
303
                                        [% END %]
305
                                        [% END %]
Lines 502-507 Link Here
502
                            <select name="lengthunit" id="lengthunit">
504
                            <select name="lengthunit" id="lengthunit">
503
                                <option value="days" selected="selected">Days</option>
505
                                <option value="days" selected="selected">Days</option>
504
                                <option value="hours">Hours</option>
506
                                <option value="hours">Hours</option>
507
                                <option value="minutes">Minutes</option>
505
                            </select>
508
                            </select>
506
                        </td>
509
                        </td>
507
                        <td>
510
                        <td>
(-)a/t/db_dependent/MinuteLoans.t (-1 / +284 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/env perl
2
3
use strict;
4
use warnings;
5
6
use DateTime;
7
use DateTime::Duration;
8
use Test::More tests => 11;
9
use Test::MockModule;
10
use Test::NoWarnings;
11
12
use C4::Circulation;
13
use C4::Overdues;
14
use Koha::DateUtils qw( dt_from_string );
15
use Koha::DiscreteCalendar;
16
17
use t::lib::Mocks;
18
use t::lib::TestBuilder;
19
20
my $dbh    = C4::Context->dbh();
21
my $today  = DateTime->today;
22
my $schema = Koha::Database->new->schema;
23
$schema->storage->txn_begin;
24
25
my $builder = t::lib::TestBuilder->new();
26
my $branch  = $builder->build( { source => 'Branch' } )->{branchcode};
27
28
my $calendar = Koha::DiscreteCalendar->new( { branchcode => $branch } );
29
30
#######################################
31
# Add minutes and open minutes between
32
#######################################
33
34
my $start_date = dt_from_string->set( hour => 14, minute => 15, second => 0 );
35
my $end_date   = dt_from_string->set( hour => 14, minute => 20, second => 0 );
36
37
is( $calendar->open_minutes_between( $start_date, $end_date ), 5, "Item returned 5 minutes late" );
38
39
#Adding 10 minutes
40
my $ten_mins_duration = DateTime::Duration->new( minutes => 10 );
41
is(
42
    $calendar->addDuration( $start_date, $ten_mins_duration, 'minutes' ),
43
    dt_from_string->set( hour => 14, minute => 25, second => 0 ),
44
    'Added 10 minutes to loan'
45
);
46
47
#Adding 10 minutes, passed closing hour
48
$start_date = dt_from_string->set( hour => 16, minute => 51, second => 0 );
49
is(
50
    $calendar->addDuration( $start_date, $ten_mins_duration, 'minutes' ),
51
    dt_from_string->set( hour => 17, minute => 0, second => 0 ),
52
    'Added 10 minutes, due date passed closing hours, set due date to same day at close hour'
53
);
54
55
#Item returned next open day after a holiday.
56
my $open_minutes_today              = DateTime->today->add( hours => 11, minutes => 10 );
57
my $open_minutes_tomorrow           = DateTime->today->add( days  => 1 );
58
my $open_minutes_day_after_tomorrow = DateTime->today->add( days  => 2, hours => 11 );
59
60
$calendar->edit_holiday(
61
    {
62
        title        => "Single holiday Today",
63
        holiday_type => $Koha::DiscreteCalendar::HOLIDAYS->{EXCEPTION},
64
        start_date   => $open_minutes_tomorrow,
65
        end_date     => $open_minutes_tomorrow
66
    }
67
);
68
69
is(
70
    $calendar->open_minutes_between( $open_minutes_today, $open_minutes_day_after_tomorrow ),
71
    530,
72
    "Correct open minutes, with a holiday in between"
73
);
74
75
######################
76
# DueDate calculation
77
######################
78
79
#Set syspref ReturnBeforeExpiry = 1 and useDaysMode = 'Days'
80
t::lib::Mocks::mock_preference( 'ReturnBeforeExpiry', 1 );
81
t::lib::Mocks::mock_preference( 'useDaysMode',        'Days' );
82
83
my $issuelength   = 25;
84
my $renewalperiod = 5;
85
my $lengthunit    = 'minutes';
86
my $dateexpiry    = DateTime->today->add( years => 1 );
87
88
my $mock_loan_length = [
89
    [ 'issuelength', 'renewalperiod', 'lengthunit' ],
90
    [ $issuelength,  $renewalperiod,  $lengthunit ]
91
];
92
93
my $categorycode = $builder->build( { source => 'Category' } )->{categorycode};
94
my $patron       = $builder->build_object(
95
    {
96
        class => 'Koha::Patrons',
97
        value => {
98
            categorycode => $categorycode,
99
            branchcode   => $branch,
100
            dateexpiry   => $dateexpiry,
101
        },
102
    }
103
);
104
my $itemtype = $builder->build_object( { class => 'Koha::ItemTypes' } )->itemtype;
105
$dbh->do(
106
    "INSERT INTO circulation_rules (branchcode, categorycode, itemtype, rule_name, rule_value) VALUES('$branch', '$categorycode', '$itemtype', 'issuelength', '$issuelength')"
107
);
108
$dbh->do(
109
    "INSERT INTO circulation_rules (branchcode, categorycode, itemtype, rule_name, rule_value) VALUES('$branch', '$categorycode', '$itemtype', 'lengthunit', '$lengthunit')"
110
);
111
112
my $date = C4::Circulation::CalcDateDue( $today, $itemtype, $branch, $patron );
113
is( $date, DateTime->today->add( minutes => $issuelength ), "Due date calculated correctly" );
114
115
#passed closing hour
116
$issuelength = 1300;
117
Koha::CirculationRules->set_rules(
118
    {
119
        categorycode => $categorycode,
120
        branchcode   => $branch,
121
        itemtype     => $itemtype,
122
        rules        => {
123
            issuelength => $issuelength,
124
        }
125
    }
126
);
127
128
$date = C4::Circulation::CalcDateDue( $today, $itemtype, $branch, $patron );
129
is( $date, DateTime->today->add( hours => 17 ), "Due date passed close hour, item due at close hour" );
130
131
#############################
132
# Chargeable minutes between
133
############################
134
135
$issuelength = 25;
136
Koha::CirculationRules->set_rules(
137
    {
138
        categorycode => $categorycode,
139
        branchcode   => $branch,
140
        itemtype     => $itemtype,
141
        rules        => {
142
            issuelength => $issuelength,
143
        }
144
    }
145
);
146
147
my $date_due      = DateTime->today;
148
my $date_returned = DateTime->today->add( minutes => 40 );
149
150
my $chargeable_units = C4::Overdues::get_chargeable_units( 'minutes', $date_due, $date_returned, $branch );
151
is( $chargeable_units, 40, "40 minutes of use" );
152
153
######################
154
# Fines calculation
155
#####################
156
157
my $biblio = $builder->build_sample_biblio();
158
159
my $item = $builder->build(
160
    {
161
        source => 'Item',
162
        value  => {
163
            biblionumber     => $biblio->biblionumber,
164
            homebranch       => $branch,
165
            holdingbranch    => $branch,
166
            replacementprice => '5.00',
167
        },
168
    }
169
);
170
my $rule = $builder->schema->resultset('CirculationRule')->search(
171
    {
172
        branchcode   => undef,
173
        categorycode => undef,
174
        itemtype     => undef,
175
    }
176
);
177
$rule->delete_all if $rule;
178
179
# FinesIncludeGracePeriod included
180
t::lib::Mocks::mock_preference( 'FinesIncludeGracePeriod', 1 );
181
$builder->build(
182
    {
183
        source => 'CirculationRule',
184
        value  => {
185
            branchcode   => undef,
186
            categorycode => undef,
187
            itemtype     => undef,
188
            rule_name    => 'fine',
189
            rule_value   => '1',
190
        },
191
    }
192
);
193
$builder->build(
194
    {
195
        source => 'CirculationRule',
196
        value  => {
197
            branchcode   => undef,
198
            categorycode => undef,
199
            itemtype     => undef,
200
            rule_name    => 'lengthunit',
201
            rule_value   => 'minutes',
202
        },
203
    }
204
);
205
$builder->build(
206
    {
207
        source => 'CirculationRule',
208
        value  => {
209
            branchcode   => undef,
210
            categorycode => undef,
211
            itemtype     => undef,
212
            rule_name    => 'finedays',
213
            rule_value   => 0,
214
        },
215
    }
216
);
217
$builder->build(
218
    {
219
        source => 'CirculationRule',
220
        value  => {
221
            branchcode   => undef,
222
            categorycode => undef,
223
            itemtype     => undef,
224
            rule_name    => 'firstremind',
225
            rule_value   => 5,
226
        },
227
    }
228
);
229
$builder->build(
230
    {
231
        source => 'CirculationRule',
232
        value  => {
233
            branchcode   => undef,
234
            categorycode => undef,
235
            itemtype     => undef,
236
            rule_name    => 'chargeperiod',
237
            rule_value   => 1,
238
        },
239
    }
240
);
241
$builder->build(
242
    {
243
        source => 'CirculationRule',
244
        value  => {
245
            branchcode   => undef,
246
            categorycode => undef,
247
            itemtype     => undef,
248
            rule_name    => 'overduefinescap',
249
            rule_value   => 9000,
250
        },
251
    }
252
);
253
$builder->build(
254
    {
255
        source => 'CirculationRule',
256
        value  => {
257
            branchcode   => undef,
258
            categorycode => undef,
259
            itemtype     => undef,
260
            rule_name    => 'cap_fine_to_replacement_price',
261
            rule_value   => 0,
262
        },
263
    }
264
);
265
266
my $start_dt = dt_from_string->set( hour => 15, minute => 0, second => 0 );
267
my $end_dt   = dt_from_string->set( hour => 15, minute => 4, second => 0 );
268
269
my @amount = C4::Overdues::CalcFine( $item, $patron->{categorycode}, $branch, $start_dt, $end_dt );
270
is( $amount[0], 0, "No fine when under the fine grace period" );
271
272
$end_dt = dt_from_string->set( hour => 15, minute => 6, second => 0 );
273
@amount = C4::Overdues::CalcFine( $item, $patron->{categorycode}, $branch, $start_dt, $end_dt );
274
is( $amount[0], 6, "6\$ fine for 6 minutes delay, fine grace period included" );
275
276
# FinesIncludeGracePeriod not included
277
t::lib::Mocks::mock_preference( 'FinesIncludeGracePeriod', 0 );
278
279
@amount = C4::Overdues::CalcFine( $item, $patron->{categorycode}, $branch, $start_dt, $end_dt );
280
is( $amount[0], 1, "1\$ fine for 6 minutes delay, fine grace period not included" );
281
282
$schema->storage->txn_rollback;
283
284
1;

Return to bug 17983