Bug 25184: (RM follow-up) Make DB update idempotent
[koha.git] / Koha / CirculationRules.pm
blob5042e6f55d30ba9e6d2ff9474e1537e948102c2c
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 lengthunit => {
113 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
115 maxissueqty => {
116 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
118 maxonsiteissueqty => {
119 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
121 maxsuspensiondays => {
122 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
124 no_auto_renewal_after => {
125 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
127 no_auto_renewal_after_hard_limit => {
128 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
130 norenewalbefore => {
131 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
133 onshelfholds => {
134 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
136 opacitemholds => {
137 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
139 overduefinescap => {
140 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
142 renewalperiod => {
143 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
145 renewalsallowed => {
146 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
148 rentaldiscount => {
149 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
151 reservesallowed => {
152 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
154 suspension_chargeperiod => {
155 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
157 note => { # This is not really a rule. Maybe we will want to separate this later.
158 scope => [ 'branchcode', 'categorycode', 'itemtype' ],
160 # Not included (deprecated?):
161 # * accountsent
162 # * reservecharge
163 # * restrictedtype
166 sub rule_kinds {
167 return $RULE_KINDS;
170 =head3 get_effective_rule
172 =cut
174 sub get_effective_rule {
175 my ( $self, $params ) = @_;
177 $params->{categorycode} //= undef;
178 $params->{branchcode} //= undef;
179 $params->{itemtype} //= undef;
181 my $rule_name = $params->{rule_name};
182 my $categorycode = $params->{categorycode};
183 my $itemtype = $params->{itemtype};
184 my $branchcode = $params->{branchcode};
186 Koha::Exceptions::MissingParameter->throw(
187 "Required parameter 'rule_name' missing")
188 unless $rule_name;
190 for my $v ( $branchcode, $categorycode, $itemtype ) {
191 $v = undef if $v and $v eq '*';
194 my $order_by = $params->{order_by}
195 // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
197 my $search_params;
198 $search_params->{rule_name} = $rule_name;
200 $search_params->{categorycode} = defined $categorycode ? [ $categorycode, undef ] : undef;
201 $search_params->{itemtype} = defined $itemtype ? [ $itemtype, undef ] : undef;
202 $search_params->{branchcode} = defined $branchcode ? [ $branchcode, undef ] : undef;
204 my $rule = $self->search(
205 $search_params,
207 order_by => $order_by,
208 rows => 1,
210 )->single;
212 return $rule;
215 =head3 get_effective_rules
217 =cut
219 sub get_effective_rules {
220 my ( $self, $params ) = @_;
222 my $rules = $params->{rules};
223 my $categorycode = $params->{categorycode};
224 my $itemtype = $params->{itemtype};
225 my $branchcode = $params->{branchcode};
227 my $r;
228 foreach my $rule (@$rules) {
229 my $effective_rule = $self->get_effective_rule(
231 rule_name => $rule,
232 categorycode => $categorycode,
233 itemtype => $itemtype,
234 branchcode => $branchcode,
238 $r->{$rule} = $effective_rule->rule_value if $effective_rule;
241 return $r;
244 =head3 set_rule
246 =cut
248 sub set_rule {
249 my ( $self, $params ) = @_;
251 for my $mandatory_parameter (qw( rule_name rule_value ) ) {
252 Koha::Exceptions::MissingParameter->throw(
253 "Required parameter '$mandatory_parameter' missing")
254 unless exists $params->{$mandatory_parameter};
257 my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
258 Koha::Exceptions::MissingParameter->throw(
259 "set_rule given unknown rule '$params->{rule_name}'!")
260 unless defined $kind_info;
262 # Enforce scope; a rule should be set for its defined scope, no more, no less.
263 foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
264 if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
265 croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
266 unless exists $params->{$scope_level};
267 } else {
268 croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
269 if exists $params->{$scope_level};
273 my $branchcode = $params->{branchcode};
274 my $categorycode = $params->{categorycode};
275 my $itemtype = $params->{itemtype};
276 my $rule_name = $params->{rule_name};
277 my $rule_value = $params->{rule_value};
279 for my $v ( $branchcode, $categorycode, $itemtype ) {
280 $v = undef if $v and $v eq '*';
282 my $rule = $self->search(
284 rule_name => $rule_name,
285 branchcode => $branchcode,
286 categorycode => $categorycode,
287 itemtype => $itemtype,
289 )->next();
291 if ($rule) {
292 if ( defined $rule_value ) {
293 $rule->rule_value($rule_value);
294 $rule->update();
296 else {
297 $rule->delete();
300 else {
301 if ( defined $rule_value ) {
302 $rule = Koha::CirculationRule->new(
304 branchcode => $branchcode,
305 categorycode => $categorycode,
306 itemtype => $itemtype,
307 rule_name => $rule_name,
308 rule_value => $rule_value,
311 $rule->store();
315 return $rule;
318 =head3 set_rules
320 =cut
322 sub set_rules {
323 my ( $self, $params ) = @_;
325 my %set_params;
326 $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
327 $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
328 $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
329 my $rules = $params->{rules};
331 my $rule_objects = [];
332 while ( my ( $rule_name, $rule_value ) = each %$rules ) {
333 my $rule_object = Koha::CirculationRules->set_rule(
335 %set_params,
336 rule_name => $rule_name,
337 rule_value => $rule_value,
340 push( @$rule_objects, $rule_object );
343 return $rule_objects;
346 =head3 delete
348 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
350 =cut
352 sub delete {
353 my ( $self ) = @_;
355 while ( my $rule = $self->next ){
356 $rule->delete;
360 =head3 clone
362 Clone a set of circulation rules to another branch
364 =cut
366 sub clone {
367 my ( $self, $to_branch ) = @_;
369 while ( my $rule = $self->next ){
370 $rule->clone($to_branch);
374 =head3 get_opacitemholds_policy
376 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
378 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
379 and the "Item level holds" (opacitemholds).
380 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
382 =cut
384 sub get_opacitemholds_policy {
385 my ( $class, $params ) = @_;
387 my $item = $params->{item};
388 my $patron = $params->{patron};
390 return unless $item or $patron;
392 my $rule = Koha::CirculationRules->get_effective_rule(
394 categorycode => $patron->categorycode,
395 itemtype => $item->effective_itemtype,
396 branchcode => $item->homebranch,
397 rule_name => 'opacitemholds',
401 return $rule ? $rule->rule_value : undef;
404 =head3 get_onshelfholds_policy
406 my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
408 =cut
410 sub get_onshelfholds_policy {
411 my ( $class, $params ) = @_;
412 my $item = $params->{item};
413 my $itemtype = $item->effective_itemtype;
414 my $patron = $params->{patron};
415 my $rule = Koha::CirculationRules->get_effective_rule(
417 categorycode => ( $patron ? $patron->categorycode : undef ),
418 itemtype => $itemtype,
419 branchcode => $item->holdingbranch,
420 rule_name => 'onshelfholds',
423 return $rule ? $rule->rule_value : 0;
426 =head3 article_requestable_rules
428 Return rules that allow article requests, optionally filtered by
429 patron categorycode.
431 Use with care; see guess_article_requestable_itemtypes.
433 =cut
435 sub article_requestable_rules {
436 my ( $class, $params ) = @_;
437 my $category = $params->{categorycode};
439 return if !C4::Context->preference('ArticleRequests');
440 return $class->search({
441 $category ? ( categorycode => [ $category, undef ] ) : (),
442 rule_name => 'article_requests',
443 rule_value => { '!=' => 'no' },
447 =head3 guess_article_requestable_itemtypes
449 Return item types in a hashref that are likely possible to be
450 'article requested'. Constructed by an intelligent guess in the
451 issuing rules (see article_requestable_rules).
453 Note: pref ArticleRequestsLinkControl overrides the algorithm.
455 Optional parameters: categorycode.
457 Note: the routine is used in opac-search to obtain a reasonable
458 estimate within performance borders (not looking at all items but
459 just using default itemtype). Also we are not looking at the
460 branchcode here, since home or holding branch of the item is
461 leading and branch may be unknown too (anonymous opac session).
463 =cut
465 sub guess_article_requestable_itemtypes {
466 my ( $class, $params ) = @_;
467 my $category = $params->{categorycode};
468 return {} if !C4::Context->preference('ArticleRequests');
469 return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
471 my $cache = Koha::Caches->get_instance;
472 my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
473 my $key = $category || '*';
474 return $last_article_requestable_guesses->{$key}
475 if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
477 my $res = {};
478 my $rules = $class->article_requestable_rules({
479 $category ? ( categorycode => $category ) : (),
481 return $res if !$rules;
482 foreach my $rule ( $rules->as_list ) {
483 $res->{ $rule->itemtype // '*' } = 1;
485 $last_article_requestable_guesses->{$key} = $res;
486 $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
487 return $res;
491 =head3 type
493 =cut
495 sub _type {
496 return 'CirculationRule';
499 =head3 object_class
501 =cut
503 sub object_class {
504 return 'Koha::CirculationRule';