Bug 8732: (QA follow-up) Terminology - staff client to staff interface
[koha.git] / Koha / CirculationRules.pm
blob87062f541a16b0b2944b5e1022040b3694cc495e
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 refund => {
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' ],
66 hold_fulfillment_policy => {
67 scope => [ 'branchcode', 'itemtype' ],
69 returnbranch => {
70 scope => [ 'branchcode', 'itemtype' ],
73 article_requests => {
74 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
76 auto_renew => {
77 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
79 cap_fine_to_replacement_price => {
80 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
82 chargeperiod => {
83 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
85 chargeperiod_charge_at => {
86 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
88 fine => {
89 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
91 finedays => {
92 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
94 firstremind => {
95 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
97 hardduedate => {
98 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
100 hardduedatecompare => {
101 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
103 holds_per_day => {
104 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
106 holds_per_record => {
107 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
109 issuelength => {
110 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
112 daysmode => {
113 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
115 lengthunit => {
116 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
118 maxissueqty => {
119 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
121 maxonsiteissueqty => {
122 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
124 maxsuspensiondays => {
125 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
127 no_auto_renewal_after => {
128 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
130 no_auto_renewal_after_hard_limit => {
131 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
133 norenewalbefore => {
134 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
136 onshelfholds => {
137 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
139 opacitemholds => {
140 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
142 overduefinescap => {
143 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
145 renewalperiod => {
146 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
148 renewalsallowed => {
149 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
151 rentaldiscount => {
152 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
154 reservesallowed => {
155 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
157 suspension_chargeperiod => {
158 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
160 note => { # This is not really a rule. Maybe we will want to separate this later.
161 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
163 # Not included (deprecated?):
164 # * accountsent
165 # * reservecharge
166 # * restrictedtype
169 sub rule_kinds {
170 return $RULE_KINDS;
173 =head3 get_effective_rule
175 =cut
177 sub get_effective_rule {
178 my ( $self, $params ) = @_;
180 $params->{categorycode} //= undef;
181 $params->{branchcode} //= undef;
182 $params->{itemtype} //= undef;
184 my $rule_name = $params->{rule_name};
185 my $categorycode = $params->{categorycode};
186 my $itemtype = $params->{itemtype};
187 my $branchcode = $params->{branchcode};
189 Koha::Exceptions::MissingParameter->throw(
190 "Required parameter 'rule_name' missing")
191 unless $rule_name;
193 for my $v ( $branchcode, $categorycode, $itemtype ) {
194 $v = undef if $v and $v eq '*';
197 my $order_by = $params->{order_by}
198 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
200 my $search_params;
201 $search_params->{rule_name} = $rule_name;
203 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
204 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
205 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
207 my $rule = $self->search(
208 $search_params,
210 order_by => $order_by,
211 rows => 1,
213 )->single;
215 return $rule;
218 =head3 get_effective_rules
220 =cut
222 sub get_effective_rules {
223 my ( $self, $params ) = @_;
225 my $rules = $params->{rules};
226 my $categorycode = $params->{categorycode};
227 my $itemtype = $params->{itemtype};
228 my $branchcode = $params->{branchcode};
230 my $r;
231 foreach my $rule (@$rules) {
232 my $effective_rule = $self->get_effective_rule(
234 rule_name => $rule,
235 categorycode => $categorycode,
236 itemtype => $itemtype,
237 branchcode => $branchcode,
241 $r->{$rule} = $effective_rule->rule_value if $effective_rule;
244 return $r;
247 =head3 set_rule
249 =cut
251 sub set_rule {
252 my ( $self, $params ) = @_;
254 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
255 Koha::Exceptions::MissingParameter->throw(
256 "Required parameter '$mandatory_parameter' missing")
257 unless exists $params->{$mandatory_parameter};
260 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
261 Koha::Exceptions::MissingParameter->throw(
262 "set_rule given unknown rule '$params->{rule_name}'!")
263 unless defined $kind_info;
265 # Enforce scope; a rule should be set for its defined scope, no more, no less.
266 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
267 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
268 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
269 unless exists $params->{$scope_level};
270 } else {
271 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
272 if exists $params->{$scope_level};
276 my $branchcode = $params->{branchcode};
277 my $categorycode = $params->{categorycode};
278 my $itemtype = $params->{itemtype};
279 my $rule_name = $params->{rule_name};
280 my $rule_value = $params->{rule_value};
282 for my $v ( $branchcode, $categorycode, $itemtype ) {
283 $v = undef if $v and $v eq '*';
285 my $rule = $self->search(
287 rule_name => $rule_name,
288 branchcode => $branchcode,
289 categorycode => $categorycode,
290 itemtype => $itemtype,
292 )->next();
294 if ($rule) {
295 if ( defined $rule_value ) {
296 $rule->rule_value($rule_value);
297 $rule->update();
299 else {
300 $rule->delete();
303 else {
304 if ( defined $rule_value ) {
305 $rule = Koha::CirculationRule->new(
307 branchcode => $branchcode,
308 categorycode => $categorycode,
309 itemtype => $itemtype,
310 rule_name => $rule_name,
311 rule_value => $rule_value,
314 $rule->store();
318 return $rule;
321 =head3 set_rules
323 =cut
325 sub set_rules {
326 my ( $self, $params ) = @_;
328 my %set_params;
329 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
330 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
331 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
332 my $rules = $params->{rules};
334 my $rule_objects = [];
335 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
336 my $rule_object = Koha::CirculationRules->set_rule(
338 %set_params,
339 rule_name => $rule_name,
340 rule_value => $rule_value,
343 push( @$rule_objects, $rule_object );
346 return $rule_objects;
349 =head3 delete
351 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
353 =cut
355 sub delete {
356 my ( $self ) = @_;
358 while ( my $rule = $self->next ){
359 $rule->delete;
363 =head3 clone
365 Clone a set of circulation rules to another branch
367 =cut
369 sub clone {
370 my ( $self, $to_branch ) = @_;
372 while ( my $rule = $self->next ){
373 $rule->clone($to_branch);
377 =head3 get_opacitemholds_policy
379 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
381 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
382 and the "Item level holds" (opacitemholds).
383 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
385 =cut
387 sub get_opacitemholds_policy {
388 my ( $class, $params ) = @_;
390 my $item = $params->{item};
391 my $patron = $params->{patron};
393 return unless $item or $patron;
395 my $rule = Koha::CirculationRules->get_effective_rule(
397 categorycode => $patron->categorycode,
398 itemtype => $item->effective_itemtype,
399 branchcode => $item->homebranch,
400 rule_name => 'opacitemholds',
404 return $rule ? $rule->rule_value : undef;
407 =head3 get_onshelfholds_policy
409 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
411 =cut
413 sub get_onshelfholds_policy {
414 my ( $class, $params ) = @_;
415 my $item = $params->{item};
416 my $itemtype = $item->effective_itemtype;
417 my $patron = $params->{patron};
418 my $rule = Koha::CirculationRules->get_effective_rule(
420 categorycode => ( $patron ? $patron->categorycode : undef ),
421 itemtype => $itemtype,
422 branchcode => $item->holdingbranch,
423 rule_name => 'onshelfholds',
426 return $rule ? $rule->rule_value : 0;
429 =head3 get_lostreturn_policy
431 my $refund = Koha::CirculationRules->get_lostreturn_policy( { return_branch => $return_branch, item => $item } );
433 =cut
435 sub get_lostreturn_policy {
436 my ( $class, $params ) = @_;
438 my $item = $params->{item};
440 my $behaviour = C4::Context->preference( 'RefundLostOnReturnControl' ) // 'CheckinLibrary';
441 my $behaviour_mapping = {
442 CheckinLibrary => $params->{'return_branch'} // $item->homebranch,
443 ItemHomeBranch => $item->homebranch,
444 ItemHoldingBranch => $item->holdingbranch
447 my $branch = $behaviour_mapping->{ $behaviour };
449 my $rule = Koha::CirculationRules->get_effective_rule(
451 branchcode => $branch,
452 rule_name => 'refund',
456 return $rule ? $rule->rule_value : 1;
459 =head3 article_requestable_rules
461 Return rules that allow article requests, optionally filtered by
462 patron categorycode.
464 Use with care; see guess_article_requestable_itemtypes.
466 =cut
468 sub article_requestable_rules {
469 my ( $class, $params ) = @_;
470 my $category = $params->{categorycode};
472 return if !C4::Context->preference('ArticleRequests');
473 return $class->search({
474 $category ? ( categorycode => [ $category, undef ] ) : (),
475 rule_name => 'article_requests',
476 rule_value => { '!=' => 'no' },
480 =head3 guess_article_requestable_itemtypes
482 Return item types in a hashref that are likely possible to be
483 'article requested'. Constructed by an intelligent guess in the
484 issuing rules (see article_requestable_rules).
486 Note: pref ArticleRequestsLinkControl overrides the algorithm.
488 Optional parameters: categorycode.
490 Note: the routine is used in opac-search to obtain a reasonable
491 estimate within performance borders (not looking at all items but
492 just using default itemtype). Also we are not looking at the
493 branchcode here, since home or holding branch of the item is
494 leading and branch may be unknown too (anonymous opac session).
496 =cut
498 sub guess_article_requestable_itemtypes {
499 my ( $class, $params ) = @_;
500 my $category = $params->{categorycode};
501 return {} if !C4::Context->preference('ArticleRequests');
502 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
504 my $cache = Koha::Caches->get_instance;
505 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
506 my $key = $category || '*';
507 return $last_article_requestable_guesses->{$key}
508 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
510 my $res = {};
511 my $rules = $class->article_requestable_rules({
512 $category ? ( categorycode => $category ) : (),
514 return $res if !$rules;
515 foreach my $rule ( $rules->as_list ) {
516 $res->{ $rule->itemtype // '*' } = 1;
518 $last_article_requestable_guesses->{$key} = $res;
519 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
520 return $res;
523 =head3 get_daysmode_effective_value
525 Return the value for daysmode defined in the circulation rules.
526 If not defined (or empty string), the value of the system preference useDaysMode is returned
528 =cut
530 sub get_effective_daysmode {
531 my ( $class, $params ) = @_;
533 my $categorycode = $params->{categorycode};
534 my $itemtype = $params->{itemtype};
535 my $branchcode = $params->{branchcode};
537 my $daysmode_rule = $class->get_effective_rule(
539 categorycode => $categorycode,
540 itemtype => $itemtype,
541 branchcode => $branchcode,
542 rule_name => 'daysmode',
546 return ( defined($daysmode_rule)
547 and $daysmode_rule->rule_value ne '' )
548 ? $daysmode_rule->rule_value
549 : C4::Context->preference('useDaysMode');
554 =head3 type
556 =cut
558 sub _type {
559 return 'CirculationRule';
562 =head3 object_class
564 =cut
566 sub object_class {
567 return 'Koha::CirculationRule';