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 under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 3 of the License, or (at your option) any later
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
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' ],
66 hold_fulfillment_policy => {
67 scope => [ 'branchcode', 'itemtype' ],
70 scope => [ 'branchcode', 'itemtype' ],
74 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
77 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
79 cap_fine_to_replacement_price => {
80 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
83 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
85 chargeperiod_charge_at => {
86 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
89 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
92 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
95 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
98 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
100 hardduedatecompare => {
101 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
103 holds_per_record => {
104 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
107 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
110 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
113 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
115 maxonsiteissueqty => {
116 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
118 maxsuspensiondays => {
119 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
121 no_auto_renewal_after => {
122 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
124 no_auto_renewal_after_hard_limit => {
125 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
128 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
131 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
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' ],
151 # Not included (deprecated?):
162 =head3 get_effective_rule
166 sub get_effective_rule {
167 my ( $self, $params ) = @_;
169 $params->{categorycode} //= undef;
170 $params->{branchcode} //= undef;
171 $params->{itemtype} //= undef;
173 my $rule_name = $params->{rule_name};
174 my $categorycode = $params->{categorycode};
175 my $itemtype = $params->{itemtype};
176 my $branchcode = $params->{branchcode};
178 Koha::Exceptions::MissingParameter->throw(
179 "Required parameter 'rule_name' missing")
182 for my $v ( $branchcode, $categorycode, $itemtype ) {
183 $v = undef if $v and $v eq '*';
186 my $order_by = $params->{order_by}
187 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
190 $search_params->{rule_name} = $rule_name;
192 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
193 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
194 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
196 my $rule = $self->search(
199 order_by => $order_by,
207 =head3 get_effective_rule
211 sub get_effective_rules {
212 my ( $self, $params ) = @_;
214 my $rules = $params->{rules};
215 my $categorycode = $params->{categorycode};
216 my $itemtype = $params->{itemtype};
217 my $branchcode = $params->{branchcode};
220 foreach my $rule (@$rules) {
221 my $effective_rule = $self->get_effective_rule(
224 categorycode => $categorycode,
225 itemtype => $itemtype,
226 branchcode => $branchcode,
230 $r->{$rule} = $effective_rule->rule_value if $effective_rule;
241 my ( $self, $params ) = @_;
243 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
244 Koha::Exceptions::MissingParameter->throw(
245 "Required parameter '$mandatory_parameter' missing")
246 unless exists $params->{$mandatory_parameter};
249 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
250 Koha::Exceptions::MissingParameter->throw(
251 "set_rule given unknown rule '$params->{rule_name}'!")
252 unless defined $kind_info;
254 # Enforce scope; a rule should be set for its defined scope, no more, no less.
255 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
256 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
257 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
258 unless exists $params->{$scope_level};
260 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
261 if exists $params->{$scope_level};
265 my $branchcode = $params->{branchcode};
266 my $categorycode = $params->{categorycode};
267 my $itemtype = $params->{itemtype};
268 my $rule_name = $params->{rule_name};
269 my $rule_value = $params->{rule_value};
271 for my $v ( $branchcode, $categorycode, $itemtype ) {
272 $v = undef if $v and $v eq '*';
274 my $rule = $self->search(
276 rule_name => $rule_name,
277 branchcode => $branchcode,
278 categorycode => $categorycode,
279 itemtype => $itemtype,
284 if ( defined $rule_value ) {
285 $rule->rule_value($rule_value);
293 if ( defined $rule_value ) {
294 $rule = Koha::CirculationRule->new(
296 branchcode => $branchcode,
297 categorycode => $categorycode,
298 itemtype => $itemtype,
299 rule_name => $rule_name,
300 rule_value => $rule_value,
315 my ( $self, $params ) = @_;
318 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
319 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
320 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
321 my $rules = $params->{rules};
323 my $rule_objects = [];
324 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
325 my $rule_object = Koha::CirculationRules->set_rule(
328 rule_name => $rule_name,
329 rule_value => $rule_value,
332 push( @$rule_objects, $rule_object );
335 return $rule_objects;
340 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
347 while ( my $rule = $self->next ){
352 =head3 get_opacitemholds_policy
354 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
356 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
357 and the "Item level holds" (opacitemholds).
358 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
362 sub get_opacitemholds_policy {
363 my ( $class, $params ) = @_;
365 my $item = $params->{item};
366 my $patron = $params->{patron};
368 return unless $item or $patron;
370 my $rule = Koha::CirculationRules->get_effective_issuing_rule(
372 categorycode => $patron->categorycode,
373 itemtype => $item->effective_itemtype,
374 branchcode => $item->homebranch,
375 rule_name => 'opacitemholds',
379 return $rule ? $rule->rule_value : undef;
382 =head3 get_onshelfholds_policy
384 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
388 sub get_onshelfholds_policy {
389 my ( $class, $params ) = @_;
390 my $item = $params->{item};
391 my $itemtype = $item->effective_itemtype;
392 my $patron = $params->{patron};
393 my $rule = Koha::CirculationRules->get_effective_rule(
395 categorycode => ( $patron ? $patron->categorycode : undef ),
396 itemtype => $itemtype,
397 branchcode => $item->holdingbranch,
398 rule_name => 'onshelfholds',
401 return $rule ? $rule->rule_value : undef;
404 =head3 article_requestable_rules
406 Return rules that allow article requests, optionally filtered by
409 Use with care; see guess_article_requestable_itemtypes.
413 sub article_requestable_rules {
414 my ( $class, $params ) = @_;
415 my $category = $params->{categorycode};
417 return if !C4::Context->preference('ArticleRequests');
418 return $class->search({
419 $category ? ( categorycode => [ $category, '*' ] ) : (),
420 rule_name => 'article_requests',
421 rule_value => { '!=' => 'no' },
425 =head3 guess_article_requestable_itemtypes
427 Return item types in a hashref that are likely possible to be
428 'article requested'. Constructed by an intelligent guess in the
429 issuing rules (see article_requestable_rules).
431 Note: pref ArticleRequestsLinkControl overrides the algorithm.
433 Optional parameters: categorycode.
435 Note: the routine is used in opac-search to obtain a reasonable
436 estimate within performance borders (not looking at all items but
437 just using default itemtype). Also we are not looking at the
438 branchcode here, since home or holding branch of the item is
439 leading and branch may be unknown too (anonymous opac session).
443 sub guess_article_requestable_itemtypes {
444 my ( $class, $params ) = @_;
445 my $category = $params->{categorycode};
446 return {} if !C4::Context->preference('ArticleRequests');
447 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
449 my $cache = Koha::Caches->get_instance;
450 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
451 my $key = $category || '*';
452 return $last_article_requestable_guesses->{$key}
453 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
456 my $rules = $class->article_requestable_rules({
457 $category ? ( categorycode => $category ) : (),
459 return $res if !$rules;
460 foreach my $rule ( $rules->as_list ) {
461 $res->{ $rule->itemtype } = 1;
463 $last_article_requestable_guesses->{$key} = $res;
464 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
474 return 'CirculationRule';
482 return 'Koha::CirculationRule';