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>.
24 use Koha
::CirculationRule
;
26 use base
qw(Koha::Objects);
28 use constant GUESSED_ITEMTYPES_KEY
=> 'Koha_IssuingRules_last_guess';
32 Koha::CirculationRules - Koha CirculationRule Object set class
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.
50 scope
=> [ 'branchcode' ],
53 patron_maxissueqty
=> {
54 scope
=> [ 'branchcode', 'categorycode' ],
56 patron_maxonsiteissueqty
=> {
57 scope
=> [ 'branchcode', 'categorycode' ],
60 scope
=> [ 'branchcode', 'categorycode' ],
64 scope
=> [ 'branchcode', 'itemtype' ],
67 hold_fulfillment_policy
=> {
68 scope
=> [ 'branchcode', 'itemtype' ],
72 scope
=> [ 'branchcode', 'itemtype' ],
77 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
80 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
82 cap_fine_to_replacement_price
=> {
83 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
86 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
88 chargeperiod_charge_at
=> {
89 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
92 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
95 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
98 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
101 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
103 hardduedatecompare
=> {
104 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
107 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
109 holds_per_record
=> {
110 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
113 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
116 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
119 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
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' ],
137 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
140 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
143 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
146 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
149 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
152 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
155 scope
=> [ 'branchcode', 'categorycode', 'itemtype' ],
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?):
176 =head3 get_effective_rule
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")
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' ] };
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(
213 order_by
=> $order_by,
221 =head3 get_effective_rules
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
};
234 foreach my $rule (@
$rules) {
235 my $effective_rule = $self->get_effective_rule(
238 categorycode
=> $categorycode,
239 itemtype
=> $itemtype,
240 branchcode
=> $branchcode,
244 $r->{$rule} = $effective_rule->rule_value if $effective_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};
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 $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,
300 if ( defined $rule_value ) {
301 $rule->rule_value($rule_value);
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,
331 my ( $self, $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(
344 rule_name
=> $rule_name,
345 rule_value
=> $rule_value,
348 push( @
$rule_objects, $rule_object );
351 return $rule_objects;
356 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
363 while ( my $rule = $self->next ){
370 Clone a set of circulation rules to another branch
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
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 });
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 $refund = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
440 sub get_lostreturn_policy
{
441 my ( $class, $params ) = @_;
443 my $item = $params->{item
};
445 my $behaviour = C4
::Context
->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
446 my $behaviour_mapping = {
447 CheckinLibrary
=> $params->{'return_branch'} // $item->homebranch,
448 ItemHomeBranch
=> $item->homebranch,
449 ItemHoldingBranch
=> $item->holdingbranch
452 my $branch = $behaviour_mapping->{ $behaviour };
454 my $rule = Koha
::CirculationRules
->get_effective_rule(
456 branchcode
=> $branch,
457 rule_name
=> 'refund',
461 return $rule ?
$rule->rule_value : 1;
464 =head3 article_requestable_rules
466 Return rules that allow article requests, optionally filtered by
469 Use with care; see guess_article_requestable_itemtypes.
473 sub article_requestable_rules
{
474 my ( $class, $params ) = @_;
475 my $category = $params->{categorycode
};
477 return if !C4
::Context
->preference('ArticleRequests');
478 return $class->search({
479 $category ?
( categorycode
=> [ $category, undef ] ) : (),
480 rule_name
=> 'article_requests',
481 rule_value
=> { '!=' => 'no' },
485 =head3 guess_article_requestable_itemtypes
487 Return item types in a hashref that are likely possible to be
488 'article requested'. Constructed by an intelligent guess in the
489 issuing rules (see article_requestable_rules).
491 Note: pref ArticleRequestsLinkControl overrides the algorithm.
493 Optional parameters: categorycode.
495 Note: the routine is used in opac-search to obtain a reasonable
496 estimate within performance borders (not looking at all items but
497 just using default itemtype). Also we are not looking at the
498 branchcode here, since home or holding branch of the item is
499 leading and branch may be unknown too (anonymous opac session).
503 sub guess_article_requestable_itemtypes
{
504 my ( $class, $params ) = @_;
505 my $category = $params->{categorycode
};
506 return {} if !C4
::Context
->preference('ArticleRequests');
507 return { '*' => 1 } if C4
::Context
->preference('ArticleRequestsLinkControl') eq 'always';
509 my $cache = Koha
::Caches
->get_instance;
510 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY
);
511 my $key = $category || '*';
512 return $last_article_requestable_guesses->{$key}
513 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
516 my $rules = $class->article_requestable_rules({
517 $category ?
( categorycode
=> $category ) : (),
519 return $res if !$rules;
520 foreach my $rule ( $rules->as_list ) {
521 $res->{ $rule->itemtype // '*' } = 1;
523 $last_article_requestable_guesses->{$key} = $res;
524 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY
, $last_article_requestable_guesses);
528 =head3 get_daysmode_effective_value
530 Return the value for daysmode defined in the circulation rules.
531 If not defined (or empty string), the value of the system preference useDaysMode is returned
535 sub get_effective_daysmode
{
536 my ( $class, $params ) = @_;
538 my $categorycode = $params->{categorycode
};
539 my $itemtype = $params->{itemtype
};
540 my $branchcode = $params->{branchcode
};
542 my $daysmode_rule = $class->get_effective_rule(
544 categorycode
=> $categorycode,
545 itemtype
=> $itemtype,
546 branchcode
=> $branchcode,
547 rule_name
=> 'daysmode',
551 return ( defined($daysmode_rule)
552 and $daysmode_rule->rule_value ne '' )
553 ?
$daysmode_rule->rule_value
554 : C4
::Context
->preference('useDaysMode');
564 return 'CirculationRule';
572 return 'Koha::CirculationRule';