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' ],
154 unseen_renewals_allowed => {
155 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
158 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
162 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
164 suspension_chargeperiod => {
165 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
167 note => { # This is not really a rule. Maybe we will want to separate this later.
168 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
170 decreaseloanholds => {
171 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
173 # Not included (deprecated?):
183 =head3 get_effective_rule
187 sub get_effective_rule {
188 my ( $self, $params ) = @_;
190 $params->{categorycode} //= undef;
191 $params->{branchcode} //= undef;
192 $params->{itemtype} //= undef;
194 my $rule_name = $params->{rule_name};
195 my $categorycode = $params->{categorycode};
196 my $itemtype = $params->{itemtype};
197 my $branchcode = $params->{branchcode};
199 Koha::Exceptions::MissingParameter->throw(
200 "Required parameter 'rule_name' missing")
203 for my $v ( $branchcode, $categorycode, $itemtype ) {
204 $v = undef if $v and $v eq '*';
207 my $order_by = $params->{order_by}
208 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
211 $search_params->{rule_name} = $rule_name;
213 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
214 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
215 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
217 my $rule = $self->search(
220 order_by => $order_by,
228 =head3 get_effective_rules
232 sub get_effective_rules {
233 my ( $self, $params ) = @_;
235 my $rules = $params->{rules};
236 my $categorycode = $params->{categorycode};
237 my $itemtype = $params->{itemtype};
238 my $branchcode = $params->{branchcode};
241 foreach my $rule (@$rules) {
242 my $effective_rule = $self->get_effective_rule(
245 categorycode => $categorycode,
246 itemtype => $itemtype,
247 branchcode => $branchcode,
251 $r->{$rule} = $effective_rule->rule_value if $effective_rule;
262 my ( $self, $params ) = @_;
264 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
265 Koha::Exceptions::MissingParameter->throw(
266 "Required parameter '$mandatory_parameter' missing")
267 unless exists $params->{$mandatory_parameter};
270 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
271 Koha::Exceptions::MissingParameter->throw(
272 "set_rule given unknown rule '$params->{rule_name}'!")
273 unless defined $kind_info;
275 # Enforce scope; a rule should be set for its defined scope, no more, no less.
276 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
277 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
278 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
279 unless exists $params->{$scope_level};
281 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
282 if exists $params->{$scope_level};
286 my $branchcode = $params->{branchcode};
287 my $categorycode = $params->{categorycode};
288 my $itemtype = $params->{itemtype};
289 my $rule_name = $params->{rule_name};
290 my $rule_value = $params->{rule_value};
291 my $can_be_blank = defined $kind_info->{can_be_blank} ? $kind_info->{can_be_blank} : 1;
292 $rule_value = undef if defined $rule_value && $rule_value eq "" && !$can_be_blank;
294 for my $v ( $branchcode, $categorycode, $itemtype ) {
295 $v = undef if $v and $v eq '*';
297 my $rule = $self->search(
299 rule_name => $rule_name,
300 branchcode => $branchcode,
301 categorycode => $categorycode,
302 itemtype => $itemtype,
307 if ( defined $rule_value ) {
308 $rule->rule_value($rule_value);
316 if ( defined $rule_value ) {
317 $rule = Koha::CirculationRule->new(
319 branchcode => $branchcode,
320 categorycode => $categorycode,
321 itemtype => $itemtype,
322 rule_name => $rule_name,
323 rule_value => $rule_value,
338 my ( $self, $params ) = @_;
341 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
342 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
343 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
344 my $rules = $params->{rules};
346 my $rule_objects = [];
347 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
348 my $rule_object = Koha::CirculationRules->set_rule(
351 rule_name => $rule_name,
352 rule_value => $rule_value,
355 push( @$rule_objects, $rule_object );
358 return $rule_objects;
363 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
370 while ( my $rule = $self->next ){
377 Clone a set of circulation rules to another branch
382 my ( $self, $to_branch ) = @_;
384 while ( my $rule = $self->next ){
385 $rule->clone($to_branch);
389 =head3 get_opacitemholds_policy
391 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
393 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
394 and the "Item level holds" (opacitemholds).
395 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
399 sub get_opacitemholds_policy {
400 my ( $class, $params ) = @_;
402 my $item = $params->{item};
403 my $patron = $params->{patron};
405 return unless $item or $patron;
407 my $rule = Koha::CirculationRules->get_effective_rule(
409 categorycode => $patron->categorycode,
410 itemtype => $item->effective_itemtype,
411 branchcode => $item->homebranch,
412 rule_name => 'opacitemholds',
416 return $rule ? $rule->rule_value : undef;
419 =head3 get_onshelfholds_policy
421 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
425 sub get_onshelfholds_policy {
426 my ( $class, $params ) = @_;
427 my $item = $params->{item};
428 my $itemtype = $item->effective_itemtype;
429 my $patron = $params->{patron};
430 my $rule = Koha::CirculationRules->get_effective_rule(
432 categorycode => ( $patron ? $patron->categorycode : undef ),
433 itemtype => $itemtype,
434 branchcode => $item->holdingbranch,
435 rule_name => 'onshelfholds',
438 return $rule ? $rule->rule_value : 0;
441 =head3 get_lostreturn_policy
443 my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
449 =item '0' - Do not refund
451 =item 'refund' - Refund the lost item charge
453 =item 'restore' - Refund the lost item charge and restore the original overdue fine
455 =item 'charge' - Refund the lost item charge and charge a new overdue fine
461 sub get_lostreturn_policy {
462 my ( $class, $params ) = @_;
464 my $item = $params->{item};
466 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
467 my $behaviour_mapping = {
468 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
469 ItemHomeBranch => $item->homebranch,
470 ItemHoldingBranch => $item->holdingbranch
473 my $branch = $behaviour_mapping->{ $behaviour };
475 my $rule = Koha::CirculationRules->get_effective_rule(
477 branchcode => $branch,
478 rule_name => 'lostreturn',
482 return $rule ? $rule->rule_value : 'refund';
485 =head3 article_requestable_rules
487 Return rules that allow article requests, optionally filtered by
490 Use with care; see guess_article_requestable_itemtypes.
494 sub article_requestable_rules {
495 my ( $class, $params ) = @_;
496 my $category = $params->{categorycode};
498 return if !C4::Context->preference('ArticleRequests');
499 return $class->search({
500 $category ? ( categorycode => [ $category, undef ] ) : (),
501 rule_name => 'article_requests',
502 rule_value => { '!=' => 'no' },
506 =head3 guess_article_requestable_itemtypes
508 Return item types in a hashref that are likely possible to be
509 'article requested'. Constructed by an intelligent guess in the
510 issuing rules (see article_requestable_rules).
512 Note: pref ArticleRequestsLinkControl overrides the algorithm.
514 Optional parameters: categorycode.
516 Note: the routine is used in opac-search to obtain a reasonable
517 estimate within performance borders (not looking at all items but
518 just using default itemtype). Also we are not looking at the
519 branchcode here, since home or holding branch of the item is
520 leading and branch may be unknown too (anonymous opac session).
524 sub guess_article_requestable_itemtypes {
525 my ( $class, $params ) = @_;
526 my $category = $params->{categorycode};
527 return {} if !C4::Context->preference('ArticleRequests');
528 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
530 my $cache = Koha::Caches->get_instance;
531 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
532 my $key = $category || '*';
533 return $last_article_requestable_guesses->{$key}
534 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
537 my $rules = $class->article_requestable_rules({
538 $category ? ( categorycode => $category ) : (),
540 return $res if !$rules;
541 foreach my $rule ( $rules->as_list ) {
542 $res->{ $rule->itemtype // '*' } = 1;
544 $last_article_requestable_guesses->{$key} = $res;
545 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
549 =head3 get_daysmode_effective_value
551 Return the value for daysmode defined in the circulation rules.
552 If not defined (or empty string), the value of the system preference useDaysMode is returned
556 sub get_effective_daysmode {
557 my ( $class, $params ) = @_;
559 my $categorycode = $params->{categorycode};
560 my $itemtype = $params->{itemtype};
561 my $branchcode = $params->{branchcode};
563 my $daysmode_rule = $class->get_effective_rule(
565 categorycode => $categorycode,
566 itemtype => $itemtype,
567 branchcode => $branchcode,
568 rule_name => 'daysmode',
572 return ( defined($daysmode_rule)
573 and $daysmode_rule->rule_value ne '' )
574 ? $daysmode_rule->rule_value
575 : C4::Context->preference('useDaysMode');
585 return 'CirculationRule';
593 return 'Koha::CirculationRule';