Bug 18936: chargename removed by bug 21753
[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     #   * reservecharge
154     #   * restrictedtype
155 };
156
157 sub rule_kinds {
158     return $RULE_KINDS;
159 }
160
161 =head3 get_effective_rule
162
163 =cut
164
165 sub get_effective_rule {
166     my ( $self, $params ) = @_;
167
168     $params->{categorycode} //= undef;
169     $params->{branchcode}   //= undef;
170     $params->{itemtype}     //= undef;
171
172     my $rule_name    = $params->{rule_name};
173     my $categorycode = $params->{categorycode};
174     my $itemtype     = $params->{itemtype};
175     my $branchcode   = $params->{branchcode};
176
177     Koha::Exceptions::MissingParameter->throw(
178         "Required parameter 'rule_name' missing")
179       unless $rule_name;
180
181     for my $v ( $branchcode, $categorycode, $itemtype ) {
182         $v = undef if $v and $v eq '*';
183     }
184
185     my $order_by = $params->{order_by}
186       // { -desc => [ 'branchcode', 'categorycode', 'itemtype' ] };
187
188     my $search_params;
189     $search_params->{rule_name} = $rule_name;
190
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;
194
195     my $rule = $self->search(
196         $search_params,
197         {
198             order_by => $order_by,
199             rows => 1,
200         }
201     )->single;
202
203     return $rule;
204 }
205
206 =head3 get_effective_rule
207
208 =cut
209
210 sub get_effective_rules {
211     my ( $self, $params ) = @_;
212
213     my $rules        = $params->{rules};
214     my $categorycode = $params->{categorycode};
215     my $itemtype     = $params->{itemtype};
216     my $branchcode   = $params->{branchcode};
217
218     my $r;
219     foreach my $rule (@$rules) {
220         my $effective_rule = $self->get_effective_rule(
221             {
222                 rule_name    => $rule,
223                 categorycode => $categorycode,
224                 itemtype     => $itemtype,
225                 branchcode   => $branchcode,
226             }
227         );
228
229         $r->{$rule} = $effective_rule->rule_value if $effective_rule;
230     }
231
232     return $r;
233 }
234
235 =head3 set_rule
236
237 =cut
238
239 sub set_rule {
240     my ( $self, $params ) = @_;
241
242     for my $mandatory_parameter (qw( rule_name rule_value ) ) {
243         Koha::Exceptions::MissingParameter->throw(
244             "Required parameter '$mandatory_parameter' missing")
245           unless exists $params->{$mandatory_parameter};
246     }
247
248     my $kind_info = $RULE_KINDS->{ $params->{rule_name} };
249     Koha::Exceptions::MissingParameter->throw(
250         "set_rule given unknown rule '$params->{rule_name}'!")
251         unless defined $kind_info;
252
253     # Enforce scope; a rule should be set for its defined scope, no more, no less.
254     foreach my $scope_level ( qw( branchcode categorycode itemtype ) ) {
255         if ( grep /$scope_level/, @{ $kind_info->{scope} } ) {
256             croak "set_rule needs '$scope_level' to set '$params->{rule_name}'!"
257                 unless exists $params->{$scope_level};
258         } else {
259             croak "set_rule cannot set '$params->{rule_name}' for a '$scope_level'!"
260                 if exists $params->{$scope_level};
261         }
262     }
263
264     my $branchcode   = $params->{branchcode};
265     my $categorycode = $params->{categorycode};
266     my $itemtype     = $params->{itemtype};
267     my $rule_name    = $params->{rule_name};
268     my $rule_value   = $params->{rule_value};
269
270     for my $v ( $branchcode, $categorycode, $itemtype ) {
271         $v = undef if $v and $v eq '*';
272     }
273     my $rule = $self->search(
274         {
275             rule_name    => $rule_name,
276             branchcode   => $branchcode,
277             categorycode => $categorycode,
278             itemtype     => $itemtype,
279         }
280     )->next();
281
282     if ($rule) {
283         if ( defined $rule_value ) {
284             $rule->rule_value($rule_value);
285             $rule->update();
286         }
287         else {
288             $rule->delete();
289         }
290     }
291     else {
292         if ( defined $rule_value ) {
293             $rule = Koha::CirculationRule->new(
294                 {
295                     branchcode   => $branchcode,
296                     categorycode => $categorycode,
297                     itemtype     => $itemtype,
298                     rule_name    => $rule_name,
299                     rule_value   => $rule_value,
300                 }
301             );
302             $rule->store();
303         }
304     }
305
306     return $rule;
307 }
308
309 =head3 set_rules
310
311 =cut
312
313 sub set_rules {
314     my ( $self, $params ) = @_;
315
316     my %set_params;
317     $set_params{branchcode} = $params->{branchcode} if exists $params->{branchcode};
318     $set_params{categorycode} = $params->{categorycode} if exists $params->{categorycode};
319     $set_params{itemtype} = $params->{itemtype} if exists $params->{itemtype};
320     my $rules        = $params->{rules};
321
322     my $rule_objects = [];
323     while ( my ( $rule_name, $rule_value ) = each %$rules ) {
324         my $rule_object = Koha::CirculationRules->set_rule(
325             {
326                 %set_params,
327                 rule_name    => $rule_name,
328                 rule_value   => $rule_value,
329             }
330         );
331         push( @$rule_objects, $rule_object );
332     }
333
334     return $rule_objects;
335 }
336
337 =head3 delete
338
339 Delete a set of circulation rules, needed for cleaning up when deleting issuingrules
340
341 =cut
342
343 sub delete {
344     my ( $self ) = @_;
345
346     while ( my $rule = $self->next ){
347         $rule->delete;
348     }
349 }
350
351 =head3 get_opacitemholds_policy
352
353 my $can_place_a_hold_at_item_level = Koha::CirculationRules->get_opacitemholds_policy( { patron => $patron, item => $item } );
354
355 Return 'Y' or 'F' if the patron can place a hold on this item according to the issuing rules
356 and the "Item level holds" (opacitemholds).
357 Can be 'N' - Don't allow, 'Y' - Allow, and 'F' - Force
358
359 =cut
360
361 sub get_opacitemholds_policy {
362     my ( $class, $params ) = @_;
363
364     my $item   = $params->{item};
365     my $patron = $params->{patron};
366
367     return unless $item or $patron;
368
369     my $rule = Koha::CirculationRules->get_effective_issuing_rule(
370         {
371             categorycode => $patron->categorycode,
372             itemtype     => $item->effective_itemtype,
373             branchcode   => $item->homebranch,
374             rule_name    => 'opacitemholds',
375         }
376     );
377
378     return $rule ? $rule->rule_value : undef;
379 }
380
381 =head3 get_onshelfholds_policy
382
383     my $on_shelf_holds = Koha::CirculationRules->get_onshelfholds_policy({ item => $item, patron => $patron });
384
385 =cut
386
387 sub get_onshelfholds_policy {
388     my ( $class, $params ) = @_;
389     my $item = $params->{item};
390     my $itemtype = $item->effective_itemtype;
391     my $patron = $params->{patron};
392     my $rule = Koha::CirculationRules->get_effective_rule(
393         {
394             categorycode => ( $patron ? $patron->categorycode : undef ),
395             itemtype     => $itemtype,
396             branchcode   => $item->holdingbranch,
397             rule_name    => 'onshelfholds',
398         }
399     );
400     return $rule ? $rule->rule_value : undef;
401 }
402
403 =head3 article_requestable_rules
404
405     Return rules that allow article requests, optionally filtered by
406     patron categorycode.
407
408     Use with care; see guess_article_requestable_itemtypes.
409
410 =cut
411
412 sub article_requestable_rules {
413     my ( $class, $params ) = @_;
414     my $category = $params->{categorycode};
415
416     return if !C4::Context->preference('ArticleRequests');
417     return $class->search({
418         $category ? ( categorycode => [ $category, '*' ] ) : (),
419         rule_name => 'article_requests',
420         rule_value => { '!=' => 'no' },
421     });
422 }
423
424 =head3 guess_article_requestable_itemtypes
425
426     Return item types in a hashref that are likely possible to be
427     'article requested'. Constructed by an intelligent guess in the
428     issuing rules (see article_requestable_rules).
429
430     Note: pref ArticleRequestsLinkControl overrides the algorithm.
431
432     Optional parameters: categorycode.
433
434     Note: the routine is used in opac-search to obtain a reasonable
435     estimate within performance borders (not looking at all items but
436     just using default itemtype). Also we are not looking at the
437     branchcode here, since home or holding branch of the item is
438     leading and branch may be unknown too (anonymous opac session).
439
440 =cut
441
442 sub guess_article_requestable_itemtypes {
443     my ( $class, $params ) = @_;
444     my $category = $params->{categorycode};
445     return {} if !C4::Context->preference('ArticleRequests');
446     return { '*' => 1 } if C4::Context->preference('ArticleRequestsLinkControl') eq 'always';
447
448     my $cache = Koha::Caches->get_instance;
449     my $last_article_requestable_guesses = $cache->get_from_cache(GUESSED_ITEMTYPES_KEY);
450     my $key = $category || '*';
451     return $last_article_requestable_guesses->{$key}
452         if $last_article_requestable_guesses && exists $last_article_requestable_guesses->{$key};
453
454     my $res = {};
455     my $rules = $class->article_requestable_rules({
456         $category ? ( categorycode => $category ) : (),
457     });
458     return $res if !$rules;
459     foreach my $rule ( $rules->as_list ) {
460         $res->{ $rule->itemtype } = 1;
461     }
462     $last_article_requestable_guesses->{$key} = $res;
463     $cache->set_in_cache(GUESSED_ITEMTYPES_KEY, $last_article_requestable_guesses);
464     return $res;
465 }
466
467
468 =head3 type
469
470 =cut
471
472 sub _type {
473     return 'CirculationRule';
474 }
475
476 =head3 object_class
477
478 =cut
479
480 sub object_class {
481     return 'Koha::CirculationRule';
482 }
483
484 1;