Bug 23091: Add handling for new lostreturn rules
[koha.git] / Koha / CirculationRules.pm
bloba90f8846c4b8be768958d21b2adfd63227b771f4
1 package Koha::CirculationRules;
3 # Copyright ByWater Solutions 2017
5 # This file is part of Koha.
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.
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.
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>.
20 use Modern::Perl;
21 use Carp qw(croak);
23 use Koha::Exceptions;
24 use Koha::CirculationRule;
26 use base qw(Koha::Objects);
28 use constant GUESSED_ITEMTYPES_KEY => 'Koha_IssuingRules_last_guess';
30 =head1 NAME
32 Koha::CirculationRules - Koha CirculationRule Object set class
34 =head1 API
36 =head2 Class Methods
38 =cut
40 =head3 rule_kinds
42 This structure describes the possible rules that may be set, and what scopes they can be set at.
44 Any attempt to set a rule with a nonsensical scope (for instance, setting the C<patron_maxissueqty> for a branchcode and itemtype), is an error.
46 =cut
48 our $RULE_KINDS = {
49 lostreturn => {
50 scope => [ 'branchcode' ],
53 patron_maxissueqty => {
54 scope => [ 'branchcode', 'categorycode' ],
56 patron_maxonsiteissueqty => {
57 scope => [ 'branchcode', 'categorycode' ],
59 max_holds => {
60 scope => [ 'branchcode', 'categorycode' ],
63 holdallowed => {
64 scope => [ 'branchcode', 'itemtype' ],
65 can_be_blank => 0,
67 hold_fulfillment_policy => {
68 scope => [ 'branchcode', 'itemtype' ],
69 can_be_blank => 0,
71 returnbranch => {
72 scope => [ 'branchcode', 'itemtype' ],
73 can_be_blank => 0,
76 article_requests => {
77 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
79 auto_renew => {
80 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
82 cap_fine_to_replacement_price => {
83 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
85 chargeperiod => {
86 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
88 chargeperiod_charge_at => {
89 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
91 fine => {
92 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
94 finedays => {
95 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
97 firstremind => {
98 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
100 hardduedate => {
101 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
103 hardduedatecompare => {
104 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
106 holds_per_day => {
107 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
109 holds_per_record => {
110 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
112 issuelength => {
113 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
115 daysmode => {
116 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
118 lengthunit => {
119 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
121 maxissueqty => {
122 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
124 maxonsiteissueqty => {
125 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
127 maxsuspensiondays => {
128 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
130 no_auto_renewal_after => {
131 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
133 no_auto_renewal_after_hard_limit => {
134 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
136 norenewalbefore => {
137 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
139 onshelfholds => {
140 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
142 opacitemholds => {
143 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
145 overduefinescap => {
146 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
148 renewalperiod => {
149 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
151 renewalsallowed => {
152 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
154 rentaldiscount => {
155 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
157 reservesallowed => {
158 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
160 suspension_chargeperiod => {
161 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
163 note => { # This is not really a rule. Maybe we will want to separate this later.
164 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
166 # Not included (deprecated?):
167 # * accountsent
168 # * reservecharge
169 # * restrictedtype
172 sub rule_kinds {
173 return $RULE_KINDS;
176 =head3 get_effective_rule
178 =cut
180 sub get_effective_rule {
181 my ( $self, $params ) = @_;
183 $params->{categorycode} //= undef;
184 $params->{branchcode} //= undef;
185 $params->{itemtype} //= undef;
187 my $rule_name = $params->{rule_name};
188 my $categorycode = $params->{categorycode};
189 my $itemtype = $params->{itemtype};
190 my $branchcode = $params->{branchcode};
192 Koha::Exceptions::MissingParameter->throw(
193 "Required parameter 'rule_name' missing")
194 unless $rule_name;
196 for my $v ( $branchcode, $categorycode, $itemtype ) {
197 $v = undef if $v and $v eq '*';
200 my $order_by = $params->{order_by}
201 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
203 my $search_params;
204 $search_params->{rule_name} = $rule_name;
206 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
207 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
208 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
210 my $rule = $self->search(
211 $search_params,
213 order_by => $order_by,
214 rows => 1,
216 )->single;
218 return $rule;
221 =head3 get_effective_rules
223 =cut
225 sub get_effective_rules {
226 my ( $self, $params ) = @_;
228 my $rules = $params->{rules};
229 my $categorycode = $params->{categorycode};
230 my $itemtype = $params->{itemtype};
231 my $branchcode = $params->{branchcode};
233 my $r;
234 foreach my $rule (@$rules) {
235 my $effective_rule = $self->get_effective_rule(
237 rule_name => $rule,
238 categorycode => $categorycode,
239 itemtype => $itemtype,
240 branchcode => $branchcode,
244 $r->{$rule} = $effective_rule->rule_value if $effective_rule;
247 return $r;
250 =head3 set_rule
252 =cut
254 sub set_rule {
255 my ( $self, $params ) = @_;
257 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
258 Koha::Exceptions::MissingParameter->throw(
259 "Required parameter '$mandatory_parameter' missing")
260 unless exists $params->{$mandatory_parameter};
263 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
264 Koha::Exceptions::MissingParameter->throw(
265 "set_rule given unknown rule '$params->{rule_name}'!")
266 unless defined $kind_info;
268 # Enforce scope; a rule should be set for its defined scope, no more, no less.
269 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
270 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
271 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
272 unless exists $params->{$scope_level};
273 } else {
274 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
275 if exists $params->{$scope_level};
279 my $branchcode = $params->{branchcode};
280 my $categorycode = $params->{categorycode};
281 my $itemtype = $params->{itemtype};
282 my $rule_name = $params->{rule_name};
283 my $rule_value = $params->{rule_value};
284 my $can_be_blank = defined $kind_info->{can_be_blank} ? $kind_info->{can_be_blank} : 1;
285 $rule_value = undef if defined $rule_value && $rule_value eq "" && !$can_be_blank;
287 for my $v ( $branchcode, $categorycode, $itemtype ) {
288 $v = undef if $v and $v eq '*';
290 my $rule = $self->search(
292 rule_name => $rule_name,
293 branchcode => $branchcode,
294 categorycode => $categorycode,
295 itemtype => $itemtype,
297 )->next();
299 if ($rule) {
300 if ( defined $rule_value ) {
301 $rule->rule_value($rule_value);
302 $rule->update();
304 else {
305 $rule->delete();
308 else {
309 if ( defined $rule_value ) {
310 $rule = Koha::CirculationRule->new(
312 branchcode => $branchcode,
313 categorycode => $categorycode,
314 itemtype => $itemtype,
315 rule_name => $rule_name,
316 rule_value => $rule_value,
319 $rule->store();
323 return $rule;
326 =head3 set_rules
328 =cut
330 sub set_rules {
331 my ( $self, $params ) = @_;
333 my %set_params;
334 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
335 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
336 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
337 my $rules = $params->{rules};
339 my $rule_objects = [];
340 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
341 my $rule_object = Koha::CirculationRules->set_rule(
343 %set_params,
344 rule_name => $rule_name,
345 rule_value => $rule_value,
348 push( @$rule_objects, $rule_object );
351 return $rule_objects;
354 =head3 delete
356 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
358 =cut
360 sub delete {
361 my ( $self ) = @_;
363 while ( my $rule = $self->next ){
364 $rule->delete;
368 =head3 clone
370 Clone a set of circulation rules to another branch
372 =cut
374 sub clone {
375 my ( $self, $to_branch ) = @_;
377 while ( my $rule = $self->next ){
378 $rule->clone($to_branch);
382 =head3 get_opacitemholds_policy
384 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
386 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
387 and the "Item level holds" (opacitemholds).
388 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
390 =cut
392 sub get_opacitemholds_policy {
393 my ( $class, $params ) = @_;
395 my $item = $params->{item};
396 my $patron = $params->{patron};
398 return unless $item or $patron;
400 my $rule = Koha::CirculationRules->get_effective_rule(
402 categorycode => $patron->categorycode,
403 itemtype => $item->effective_itemtype,
404 branchcode => $item->homebranch,
405 rule_name => 'opacitemholds',
409 return $rule ? $rule->rule_value : undef;
412 =head3 get_onshelfholds_policy
414 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
416 =cut
418 sub get_onshelfholds_policy {
419 my ( $class, $params ) = @_;
420 my $item = $params->{item};
421 my $itemtype = $item->effective_itemtype;
422 my $patron = $params->{patron};
423 my $rule = Koha::CirculationRules->get_effective_rule(
425 categorycode => ( $patron ? $patron->categorycode : undef ),
426 itemtype => $itemtype,
427 branchcode => $item->holdingbranch,
428 rule_name => 'onshelfholds',
431 return $rule ? $rule->rule_value : 0;
434 =head3 get_lostreturn_policy
436 my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
438 Return values are:
440 =over 2
442 =item 0 - Do not refund
443 =item refund - Refund the lost item charge
444 =item restore - Refund the lost item charge and restore the original overdue fine
445 =item charge - Refund the lost item charge and charge a new overdue fine
447 =back
449 =cut
451 sub get_lostreturn_policy {
452 my ( $class, $params ) = @_;
454 my $item = $params->{item};
456 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
457 my $behaviour_mapping = {
458 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
459 ItemHomeBranch => $item->homebranch,
460 ItemHoldingBranch => $item->holdingbranch
463 my $branch = $behaviour_mapping->{ $behaviour };
465 my $rule = Koha::CirculationRules->get_effective_rule(
467 branchcode => $branch,
468 rule_name => 'lostreturn',
472 return $rule ? $rule->rule_value : 'refund';
475 =head3 article_requestable_rules
477 Return rules that allow article requests, optionally filtered by
478 patron categorycode.
480 Use with care; see guess_article_requestable_itemtypes.
482 =cut
484 sub article_requestable_rules {
485 my ( $class, $params ) = @_;
486 my $category = $params->{categorycode};
488 return if !C4::Context->preference('ArticleRequests');
489 return $class->search({
490 $category ? ( categorycode => [ $category, undef ] ) : (),
491 rule_name => 'article_requests',
492 rule_value => { '!=' => 'no' },
496 =head3 guess_article_requestable_itemtypes
498 Return item types in a hashref that are likely possible to be
499 'article requested'. Constructed by an intelligent guess in the
500 issuing rules (see article_requestable_rules).
502 Note: pref ArticleRequestsLinkControl overrides the algorithm.
504 Optional parameters: categorycode.
506 Note: the routine is used in opac-search to obtain a reasonable
507 estimate within performance borders (not looking at all items but
508 just using default itemtype). Also we are not looking at the
509 branchcode here, since home or holding branch of the item is
510 leading and branch may be unknown too (anonymous opac session).
512 =cut
514 sub guess_article_requestable_itemtypes {
515 my ( $class, $params ) = @_;
516 my $category = $params->{categorycode};
517 return {} if !C4::Context->preference('ArticleRequests');
518 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
520 my $cache = Koha::Caches->get_instance;
521 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
522 my $key = $category || '*';
523 return $last_article_requestable_guesses->{$key}
524 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
526 my $res = {};
527 my $rules = $class->article_requestable_rules({
528 $category ? ( categorycode => $category ) : (),
530 return $res if !$rules;
531 foreach my $rule ( $rules->as_list ) {
532 $res->{ $rule->itemtype // '*' } = 1;
534 $last_article_requestable_guesses->{$key} = $res;
535 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
536 return $res;
539 =head3 get_daysmode_effective_value
541 Return the value for daysmode defined in the circulation rules.
542 If not defined (or empty string), the value of the system preference useDaysMode is returned
544 =cut
546 sub get_effective_daysmode {
547 my ( $class, $params ) = @_;
549 my $categorycode = $params->{categorycode};
550 my $itemtype = $params->{itemtype};
551 my $branchcode = $params->{branchcode};
553 my $daysmode_rule = $class->get_effective_rule(
555 categorycode => $categorycode,
556 itemtype => $itemtype,
557 branchcode => $branchcode,
558 rule_name => 'daysmode',
562 return ( defined($daysmode_rule)
563 and $daysmode_rule->rule_value ne '' )
564 ? $daysmode_rule->rule_value
565 : C4::Context->preference('useDaysMode');
570 =head3 type
572 =cut
574 sub _type {
575 return 'CirculationRule';
578 =head3 object_class
580 =cut
582 sub object_class {
583 return 'Koha::CirculationRule';