Bug 23019: (follow-up) Fix test permissions and API definition bugs
[koha.git] / Koha / CirculationRules.pm
blobe0d895325b9797426a802e25036c9b35cda62dbe
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 decreaseloanholds => {
167 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
169 # Not included (deprecated?):
170 # * accountsent
171 # * reservecharge
172 # * restrictedtype
175 sub rule_kinds {
176 return $RULE_KINDS;
179 =head3 get_effective_rule
181 =cut
183 sub get_effective_rule {
184 my ( $self, $params ) = @_;
186 $params->{categorycode} //= undef;
187 $params->{branchcode} //= undef;
188 $params->{itemtype} //= undef;
190 my $rule_name = $params->{rule_name};
191 my $categorycode = $params->{categorycode};
192 my $itemtype = $params->{itemtype};
193 my $branchcode = $params->{branchcode};
195 Koha::Exceptions::MissingParameter->throw(
196 "Required parameter 'rule_name' missing")
197 unless $rule_name;
199 for my $v ( $branchcode, $categorycode, $itemtype ) {
200 $v = undef if $v and $v eq '*';
203 my $order_by = $params->{order_by}
204 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
206 my $search_params;
207 $search_params->{rule_name} = $rule_name;
209 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
210 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
211 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
213 my $rule = $self->search(
214 $search_params,
216 order_by => $order_by,
217 rows => 1,
219 )->single;
221 return $rule;
224 =head3 get_effective_rules
226 =cut
228 sub get_effective_rules {
229 my ( $self, $params ) = @_;
231 my $rules = $params->{rules};
232 my $categorycode = $params->{categorycode};
233 my $itemtype = $params->{itemtype};
234 my $branchcode = $params->{branchcode};
236 my $r;
237 foreach my $rule (@$rules) {
238 my $effective_rule = $self->get_effective_rule(
240 rule_name => $rule,
241 categorycode => $categorycode,
242 itemtype => $itemtype,
243 branchcode => $branchcode,
247 $r->{$rule} = $effective_rule->rule_value if $effective_rule;
250 return $r;
253 =head3 set_rule
255 =cut
257 sub set_rule {
258 my ( $self, $params ) = @_;
260 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
261 Koha::Exceptions::MissingParameter->throw(
262 "Required parameter '$mandatory_parameter' missing")
263 unless exists $params->{$mandatory_parameter};
266 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
267 Koha::Exceptions::MissingParameter->throw(
268 "set_rule given unknown rule '$params->{rule_name}'!")
269 unless defined $kind_info;
271 # Enforce scope; a rule should be set for its defined scope, no more, no less.
272 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
273 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
274 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
275 unless exists $params->{$scope_level};
276 } else {
277 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
278 if exists $params->{$scope_level};
282 my $branchcode = $params->{branchcode};
283 my $categorycode = $params->{categorycode};
284 my $itemtype = $params->{itemtype};
285 my $rule_name = $params->{rule_name};
286 my $rule_value = $params->{rule_value};
287 my $can_be_blank = defined $kind_info->{can_be_blank} ? $kind_info->{can_be_blank} : 1;
288 $rule_value = undef if defined $rule_value && $rule_value eq "" && !$can_be_blank;
290 for my $v ( $branchcode, $categorycode, $itemtype ) {
291 $v = undef if $v and $v eq '*';
293 my $rule = $self->search(
295 rule_name => $rule_name,
296 branchcode => $branchcode,
297 categorycode => $categorycode,
298 itemtype => $itemtype,
300 )->next();
302 if ($rule) {
303 if ( defined $rule_value ) {
304 $rule->rule_value($rule_value);
305 $rule->update();
307 else {
308 $rule->delete();
311 else {
312 if ( defined $rule_value ) {
313 $rule = Koha::CirculationRule->new(
315 branchcode => $branchcode,
316 categorycode => $categorycode,
317 itemtype => $itemtype,
318 rule_name => $rule_name,
319 rule_value => $rule_value,
322 $rule->store();
326 return $rule;
329 =head3 set_rules
331 =cut
333 sub set_rules {
334 my ( $self, $params ) = @_;
336 my %set_params;
337 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
338 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
339 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
340 my $rules = $params->{rules};
342 my $rule_objects = [];
343 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
344 my $rule_object = Koha::CirculationRules->set_rule(
346 %set_params,
347 rule_name => $rule_name,
348 rule_value => $rule_value,
351 push( @$rule_objects, $rule_object );
354 return $rule_objects;
357 =head3 delete
359 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
361 =cut
363 sub delete {
364 my ( $self ) = @_;
366 while ( my $rule = $self->next ){
367 $rule->delete;
371 =head3 clone
373 Clone a set of circulation rules to another branch
375 =cut
377 sub clone {
378 my ( $self, $to_branch ) = @_;
380 while ( my $rule = $self->next ){
381 $rule->clone($to_branch);
385 =head3 get_opacitemholds_policy
387 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
389 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
390 and the "Item level holds" (opacitemholds).
391 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
393 =cut
395 sub get_opacitemholds_policy {
396 my ( $class, $params ) = @_;
398 my $item = $params->{item};
399 my $patron = $params->{patron};
401 return unless $item or $patron;
403 my $rule = Koha::CirculationRules->get_effective_rule(
405 categorycode => $patron->categorycode,
406 itemtype => $item->effective_itemtype,
407 branchcode => $item->homebranch,
408 rule_name => 'opacitemholds',
412 return $rule ? $rule->rule_value : undef;
415 =head3 get_onshelfholds_policy
417 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
419 =cut
421 sub get_onshelfholds_policy {
422 my ( $class, $params ) = @_;
423 my $item = $params->{item};
424 my $itemtype = $item->effective_itemtype;
425 my $patron = $params->{patron};
426 my $rule = Koha::CirculationRules->get_effective_rule(
428 categorycode => ( $patron ? $patron->categorycode : undef ),
429 itemtype => $itemtype,
430 branchcode => $item->holdingbranch,
431 rule_name => 'onshelfholds',
434 return $rule ? $rule->rule_value : 0;
437 =head3 get_lostreturn_policy
439 my $lostrefund_policy = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
441 Return values are:
443 =over 2
445 =item '0' - Do not refund
447 =item 'refund' - Refund the lost item charge
449 =item 'restore' - Refund the lost item charge and restore the original overdue fine
451 =item 'charge' - Refund the lost item charge and charge a new overdue fine
453 =back
455 =cut
457 sub get_lostreturn_policy {
458 my ( $class, $params ) = @_;
460 my $item = $params->{item};
462 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
463 my $behaviour_mapping = {
464 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
465 ItemHomeBranch => $item->homebranch,
466 ItemHoldingBranch => $item->holdingbranch
469 my $branch = $behaviour_mapping->{ $behaviour };
471 my $rule = Koha::CirculationRules->get_effective_rule(
473 branchcode => $branch,
474 rule_name => 'lostreturn',
478 return $rule ? $rule->rule_value : 'refund';
481 =head3 article_requestable_rules
483 Return rules that allow article requests, optionally filtered by
484 patron categorycode.
486 Use with care; see guess_article_requestable_itemtypes.
488 =cut
490 sub article_requestable_rules {
491 my ( $class, $params ) = @_;
492 my $category = $params->{categorycode};
494 return if !C4::Context->preference('ArticleRequests');
495 return $class->search({
496 $category ? ( categorycode => [ $category, undef ] ) : (),
497 rule_name => 'article_requests',
498 rule_value => { '!=' => 'no' },
502 =head3 guess_article_requestable_itemtypes
504 Return item types in a hashref that are likely possible to be
505 'article requested'. Constructed by an intelligent guess in the
506 issuing rules (see article_requestable_rules).
508 Note: pref ArticleRequestsLinkControl overrides the algorithm.
510 Optional parameters: categorycode.
512 Note: the routine is used in opac-search to obtain a reasonable
513 estimate within performance borders (not looking at all items but
514 just using default itemtype). Also we are not looking at the
515 branchcode here, since home or holding branch of the item is
516 leading and branch may be unknown too (anonymous opac session).
518 =cut
520 sub guess_article_requestable_itemtypes {
521 my ( $class, $params ) = @_;
522 my $category = $params->{categorycode};
523 return {} if !C4::Context->preference('ArticleRequests');
524 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
526 my $cache = Koha::Caches->get_instance;
527 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
528 my $key = $category || '*';
529 return $last_article_requestable_guesses->{$key}
530 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
532 my $res = {};
533 my $rules = $class->article_requestable_rules({
534 $category ? ( categorycode => $category ) : (),
536 return $res if !$rules;
537 foreach my $rule ( $rules->as_list ) {
538 $res->{ $rule->itemtype // '*' } = 1;
540 $last_article_requestable_guesses->{$key} = $res;
541 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
542 return $res;
545 =head3 get_daysmode_effective_value
547 Return the value for daysmode defined in the circulation rules.
548 If not defined (or empty string), the value of the system preference useDaysMode is returned
550 =cut
552 sub get_effective_daysmode {
553 my ( $class, $params ) = @_;
555 my $categorycode = $params->{categorycode};
556 my $itemtype = $params->{itemtype};
557 my $branchcode = $params->{branchcode};
559 my $daysmode_rule = $class->get_effective_rule(
561 categorycode => $categorycode,
562 itemtype => $itemtype,
563 branchcode => $branchcode,
564 rule_name => 'daysmode',
568 return ( defined($daysmode_rule)
569 and $daysmode_rule->rule_value ne '' )
570 ? $daysmode_rule->rule_value
571 : C4::Context->preference('useDaysMode');
576 =head3 type
578 =cut
580 sub _type {
581 return 'CirculationRule';
584 =head3 object_class
586 =cut
588 sub object_class {
589 return 'Koha::CirculationRule';