Bug 18936: Fix several issues
[koha.git] / Koha / CirculationRules.pm
1 package Koha::CirculationRules;
2
3 # Copyright ByWater Solutions 2017
4 #
5 # This file is part of Koha.
6 #
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
10 # version.
11 #
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.
15 #
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.
19
20 use Modern::Perl;
21 use Carp qw(croak);
22
23 use Koha::Exceptions;
24 use Koha::CirculationRule;
25
26 use base qw(Koha::Objects);
27
28 use constant GUESSED_ITEMTYPES_KEY => 'Koha_IssuingRules_last_guess';
29
30 =head1 NAME
31
32 Koha::CirculationRules - Koha CirculationRule Object set class
33
34 =head1 API
35
36 =head2 Class Methods
37
38 =cut
39
40 =head3 rule_kinds
41
42 This structure describes the possible rules that may be set, and what scopes they can be set at.
43
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.
45
46 =cut
47
48 our $RULE_KINDS = {
49     refund => {
50         scope => [ 'branchcode' ],
51     },
52
53     patron_maxissueqty => {
54         scope => [ 'branchcode', 'categorycode' ],
55     },
56     patron_maxonsiteissueqty => {
57         scope => [ 'branchcode', 'categorycode' ],
58     },
59     max_holds => {
60         scope => [ 'branchcode', 'categorycode' ],
61     },
62
63     holdallowed => {
64         scope => [ 'branchcode', 'itemtype' ],
65     },
66     hold_fulfillment_policy => {
67         scope => [ 'branchcode', 'itemtype' ],
68     },
69     returnbranch => {
70         scope => [ 'branchcode', 'itemtype' ],
71     },
72
73     article_requests => {
74         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
75     },
76     auto_renew => {
77         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
78     },
79     cap_fine_to_replacement_price => {
80         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
81     },
82     chargeperiod => {
83         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
84     },
85     chargeperiod_charge_at => {
86         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
87     },
88     fine => {
89         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
90     },
91     finedays => {
92         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
93     },
94     firstremind => {
95         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
96     },
97     hardduedate => {
98         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
99     },
100     hardduedatecompare => {
101         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
102     },
103     holds_per_record => {
104         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
105     },
106     issuelength => {
107         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
108     },
109     lengthunit => {
110         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
111     },
112     maxissueqty => {
113         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
114     },
115     maxonsiteissueqty => {
116         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
117     },
118     maxsuspensiondays => {
119         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
120     },
121     no_auto_renewal_after => {
122         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
123     },
124     no_auto_renewal_after_hard_limit => {
125         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
126     },
127     norenewalbefore => {
128         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
129     },
130     onshelfholds => {
131         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
132     },
133     opacitemholds => {
134         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
135     },
136     overduefinescap => {
137         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
138     },
139     renewalperiod => {
140         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
141     },
142     renewalsallowed => {
143         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
144     },
145     rentaldiscount => {
146         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
147     },
148     reservesallowed => {
149         scope => [ 'branchcode', 'categorycode', 'itemtype' ],
150     },
151     # Not included (deprecated?):
152     #   * accountsent
153     #   * chargename
154     #   * reservecharge
155     #   * restrictedtype
156 };
157
158 sub rule_kinds {
159     return $RULE_KINDS;
160 }
161
162 =head3 get_effective_rule
163
164 =cut
165
166 sub get_effective_rule {
167     my ( $self, $params ) = @_;
168
169     $params->{categorycode} //= undef;
170     $params->{branchcode}   //= undef;
171     $params->{itemtype}     //= undef;
172
173     my $rule_name    = $params->{rule_name};
174     my $categorycode = $params->{categorycode};
175     my $itemtype     = $params->{itemtype};
176     my $branchcode   = $params->{branchcode};
177
178     Koha::Exceptions::MissingParameter->throw(
179         "Required parameter 'rule_name' missing")
180       unless $rule_name;
181
182     for my $v ( $branchcode, $categorycode, $itemtype ) {
183         $v = undef if $v and $v eq '*';
184     }
185
186     my $order_by = $params->{order_by}
187       // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
188
189     my $search_params;
190     $search_params->{rule_name} = $rule_name;
191
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;
195
196     my $rule = $self->search(
197         $search_params,
198         {
199             order_by => $order_by,
200             rows => 1,
201         }
202     )->single;
203
204     return $rule;
205 }
206
207 =head3 get_effective_rule
208
209 =cut
210
211 sub get_effective_rules {
212     my ( $self, $params ) = @_;
213
214     my $rules        = $params->{rules};
215     my $categorycode = $params->{categorycode};
216     my $itemtype     = $params->{itemtype};
217     my $branchcode   = $params->{branchcode};
218
219     my $r;
220     foreach my $rule (@$rules) {
221         my $effective_rule = $self->get_effective_rule(
222             {
223                 rule_name    => $rule,
224                 categorycode => $categorycode,
225                 itemtype     => $itemtype,
226                 branchcode   => $branchcode,
227             }
228         );
229
230         $r->{$rule} = $effective_rule->rule_value if $effective_rule;
231     }
232
233     return $r;
234 }
235
236 =head3 set_rule
237
238 =cut
239
240 sub set_rule {
241     my ( $self, $params ) = @_;
242
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};
247     }
248
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;
253
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};
259         } else {
260             croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
261                 if exists $params->{$scope_level};
262         }
263     }
264
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};
270
271     for my $v ( $branchcode, $categorycode, $itemtype ) {
272         $v = undef if $v and $v eq '*';
273     }
274     my $rule = $self->search(
275         {
276             rule_name    => $rule_name,
277             branchcode   => $branchcode,
278             categorycode => $categorycode,
279             itemtype     => $itemtype,
280         }
281     )->next();
282
283     if ($rule) {
284         if ( defined $rule_value ) {
285             $rule->rule_value($rule_value);
286             $rule->update();
287         }
288         else {
289             $rule->delete();
290         }
291     }
292     else {
293         if ( defined $rule_value ) {
294             $rule = Koha::CirculationRule->new(
295                 {
296                     branchcode   => $branchcode,
297                     categorycode => $categorycode,
298                     itemtype     => $itemtype,
299                     rule_name    => $rule_name,
300                     rule_value   => $rule_value,
301                 }
302             );
303             $rule->store();
304         }
305     }
306
307     return $rule;
308 }
309
310 =head3 set_rules
311
312 =cut
313
314 sub set_rules {
315     my ( $self, $params ) = @_;
316
317     my %set_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};
322
323     my $rule_objects = [];
324     while ( my ( $rule_name, $rule_value ) = each %$rules ) {
325         my $rule_object = Koha::CirculationRules->set_rule(
326             {
327                 %set_params,
328                 rule_name    => $rule_name,
329                 rule_value   => $rule_value,
330             }
331         );
332         push( @$rule_objects, $rule_object );
333     }
334
335     return $rule_objects;
336 }
337
338 =head3 delete
339
340 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
341
342 =cut
343
344 sub delete {
345     my ( $self ) = @_;
346
347     while ( my $rule = $self->next ){
348         $rule->delete;
349     }
350 }
351
352 =head3 get_opacitemholds_policy
353
354 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
355
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
359
360 =cut
361
362 sub get_opacitemholds_policy {
363     my ( $class, $params ) = @_;
364
365     my $item   = $params->{item};
366     my $patron = $params->{patron};
367
368     return unless $item or $patron;
369
370     my $rule = Koha::CirculationRules->get_effective_issuing_rule(
371         {
372             categorycode => $patron->categorycode,
373             itemtype     => $item->effective_itemtype,
374             branchcode   => $item->homebranch,
375             rule_name    => 'opacitemholds',
376         }
377     );
378
379     return $rule ? $rule->rule_value : undef;
380 }
381
382 =head3 get_onshelfholds_policy
383
384     my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
385
386 =cut
387
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(
394         {
395             categorycode => ( $patron ? $patron->categorycode : undef ),
396             itemtype     => $itemtype,
397             branchcode   => $item->holdingbranch,
398             rule_name    => 'onshelfholds',
399         }
400     );
401     return $rule ? $rule->rule_value : undef;
402 }
403
404 =head3 article_requestable_rules
405
406     Return rules that allow article requests, optionally filtered by
407     patron categorycode.
408
409     Use with care; see guess_article_requestable_itemtypes.
410
411 =cut
412
413 sub article_requestable_rules {
414     my ( $class, $params ) = @_;
415     my $category = $params->{categorycode};
416
417     return if !C4::Context->preference('ArticleRequests');
418     return $class->search({
419         $category ? ( categorycode => [ $category, '*' ] ) : (),
420         rule_name => 'article_requests',
421         rule_value => { '!=' => 'no' },
422     });
423 }
424
425 =head3 guess_article_requestable_itemtypes
426
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).
430
431     Note: pref ArticleRequestsLinkControl overrides the algorithm.
432
433     Optional parameters: categorycode.
434
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).
440
441 =cut
442
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';
448
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};
454
455     my $res = {};
456     my $rules = $class->article_requestable_rules({
457         $category ? ( categorycode => $category ) : (),
458     });
459     return $res if !$rules;
460     foreach my $rule ( $rules->as_list ) {
461         $res->{ $rule->itemtype } = 1;
462     }
463     $last_article_requestable_guesses->{$key} = $res;
464     $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
465     return $res;
466 }
467
468
469 =head3 type
470
471 =cut
472
473 sub _type {
474     return 'CirculationRule';
475 }
476
477 =head3 object_class
478
479 =cut
480
481 sub object_class {
482     return 'Koha::CirculationRule';
483 }
484
485 1;