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.
23 use Koha::CirculationRule;
25 use base qw(Koha::Objects);
27 use constant GUESSED_ITEMTYPES_KEY => 'Koha_IssuingRules_last_guess';
31 Koha::CirculationRules - Koha CirculationRule Object set class
41 This structure describes the possible rules that may be set, and what scopes they can be set at.
43 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.
49 scope => [ 'branchcode' ],
52 patron_maxissueqty => {
53 scope => [ 'branchcode', 'categorycode' ],
55 patron_maxonsiteissueqty => {
56 scope => [ 'branchcode', 'categorycode' ],
59 scope => [ 'branchcode', 'categorycode' ],
63 scope => [ 'branchcode', 'itemtype' ],
65 hold_fulfillment_policy => {
66 scope => [ 'branchcode', 'itemtype' ],
69 scope => [ 'branchcode', 'itemtype' ],
73 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
76 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
78 cap_fine_to_replacement_price => {
79 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
82 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
84 chargeperiod_charge_at => {
85 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
88 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
91 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
94 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
97 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
99 hardduedatecompare => {
100 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
102 holds_per_record => {
103 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
106 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
109 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
112 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
114 maxonsiteissueqty => {
115 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
117 maxsuspensiondays => {
118 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
120 no_auto_renewal_after => {
121 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
123 no_auto_renewal_after_hard_limit => {
124 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
127 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
130 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
133 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
136 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
139 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
142 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
145 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
148 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
150 # Not included (deprecated?):
161 =head3 get_effective_rule
165 sub get_effective_rule {
166 my ( $self, $params ) = @_;
168 $params->{categorycode} //= undef;
169 $params->{branchcode} //= undef;
170 $params->{itemtype} //= undef;
172 my $rule_name = $params->{rule_name};
173 my $categorycode = $params->{categorycode};
174 my $itemtype = $params->{itemtype};
175 my $branchcode = $params->{branchcode};
177 Koha::Exceptions::MissingParameter->throw(
178 "Required parameter 'rule_name' missing")
181 for my $v ( $branchcode, $categorycode, $itemtype ) {
182 $v = undef if $v and $v eq '*';
185 my $order_by = $params->{order_by}
186 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
189 $search_params->{rule_name} = $rule_name;
191 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
192 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
193 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
195 my $rule = $self->search(
198 order_by => $order_by,
206 =head3 get_effective_rule
210 sub get_effective_rules {
211 my ( $self, $params ) = @_;
213 my $rules = $params->{rules};
214 my $categorycode = $params->{categorycode};
215 my $itemtype = $params->{itemtype};
216 my $branchcode = $params->{branchcode};
219 foreach my $rule (@$rules) {
220 my $effective_rule = $self->get_effective_rule(
223 categorycode => $categorycode,
224 itemtype => $itemtype,
225 branchcode => $branchcode,
229 $r->{$rule} = $effective_rule->rule_value if $effective_rule;
240 my ( $self, $params ) = @_;
242 for my $mandatory_parameter (qw( branchcode categorycode itemtype rule_name rule_value ) ){
243 Koha::Exceptions::MissingParameter->throw(
244 "Required parameter 'branchcode' missing")
245 unless exists $params->{$mandatory_parameter};
247 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
248 croak "set_rule given unknown rule '$params->{rule_name}'!"
249 unless defined $kind_info;
251 # Enforce scope; a rule should be set for its defined scope, no more, no less.
252 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
253 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
254 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
255 unless exists $params->{$scope_level};
257 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
258 if exists $params->{$scope_level};
262 my $branchcode = $params->{branchcode};
263 my $categorycode = $params->{categorycode};
264 my $itemtype = $params->{itemtype};
265 my $rule_name = $params->{rule_name};
266 my $rule_value = $params->{rule_value};
268 for my $v ( $branchcode, $categorycode, $itemtype ) {
269 $v = undef if $v and $v eq '*';
271 my $rule = $self->search(
273 rule_name => $rule_name,
274 branchcode => $branchcode,
275 categorycode => $categorycode,
276 itemtype => $itemtype,
281 if ( defined $rule_value ) {
282 $rule->rule_value($rule_value);
290 if ( defined $rule_value ) {
291 $rule = Koha::CirculationRule->new(
293 branchcode => $branchcode,
294 categorycode => $categorycode,
295 itemtype => $itemtype,
296 rule_name => $rule_name,
297 rule_value => $rule_value,
312 my ( $self, $params ) = @_;
315 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
316 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
317 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
318 my $rules = $params->{rules};
320 my $rule_objects = [];
321 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
322 my $rule_object = Koha::CirculationRules->set_rule(
325 rule_name => $rule_name,
326 rule_value => $rule_value,
329 push( @$rule_objects, $rule_object );
332 return $rule_objects;
337 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
344 while ( my $rule = $self->next ){
349 =head3 get_opacitemholds_policy
351 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
353 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
354 and the "Item level holds" (opacitemholds).
355 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
359 sub get_opacitemholds_policy {
360 my ( $class, $params ) = @_;
362 my $item = $params->{item};
363 my $patron = $params->{patron};
365 return unless $item or $patron;
367 my $rule = Koha::CirculationRules->get_effective_issuing_rule(
369 categorycode => $patron->categorycode,
370 itemtype => $item->effective_itemtype,
371 branchcode => $item->homebranch,
372 rule_name => 'opacitemholds',
376 return $rule ? $rule->rule_value : undef;
379 =head3 get_onshelfholds_policy
381 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
385 sub get_onshelfholds_policy {
386 my ( $class, $params ) = @_;
387 my $item = $params->{item};
388 my $itemtype = $item->effective_itemtype;
389 my $patron = $params->{patron};
390 my $rule = Koha::CirculationRules->get_effective_rule(
392 categorycode => ( $patron ? $patron->categorycode : undef ),
393 itemtype => $itemtype,
394 branchcode => $item->holdingbranch,
395 rule_name => 'onshelfholds',
398 return $rule ? $rule->rule_value : undef;
401 =head3 article_requestable_rules
403 Return rules that allow article requests, optionally filtered by
406 Use with care; see guess_article_requestable_itemtypes.
410 sub article_requestable_rules {
411 my ( $class, $params ) = @_;
412 my $category = $params->{categorycode};
414 return if !C4::Context->preference('ArticleRequests');
415 return $class->search({
416 $category ? ( categorycode => [ $category, '*' ] ) : (),
417 rule_name => 'article_requests',
418 rule_value => { '!=' => 'no' },
422 =head3 guess_article_requestable_itemtypes
424 Return item types in a hashref that are likely possible to be
425 'article requested'. Constructed by an intelligent guess in the
426 issuing rules (see article_requestable_rules).
428 Note: pref ArticleRequestsLinkControl overrides the algorithm.
430 Optional parameters: categorycode.
432 Note: the routine is used in opac-search to obtain a reasonable
433 estimate within performance borders (not looking at all items but
434 just using default itemtype). Also we are not looking at the
435 branchcode here, since home or holding branch of the item is
436 leading and branch may be unknown too (anonymous opac session).
440 sub guess_article_requestable_itemtypes {
441 my ( $class, $params ) = @_;
442 my $category = $params->{categorycode};
443 return {} if !C4::Context->preference('ArticleRequests');
444 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
446 my $cache = Koha::Caches->get_instance;
447 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
448 my $key = $category || '*';
449 return $last_article_requestable_guesses->{$key}
450 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
453 my $rules = $class->article_requestable_rules({
454 $category ? ( categorycode => $category ) : (),
456 return $res if !$rules;
457 foreach my $rule ( $rules->as_list ) {
458 $res->{ $rule->itemtype } = 1;
460 $last_article_requestable_guesses->{$key} = $res;
461 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
471 return 'CirculationRule';
479 return 'Koha::CirculationRule';