Bug 23463: (RM follow-up) Add try block around Illrequest item creation
[koha.git] / Koha / Illrequest.pm
1 package Koha::Illrequest;
2
3 # Copyright PTFS Europe 2016,2018
4 #
5 # This file is part of Koha.
6 #
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.
11 #
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.
16 #
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>.
19
20 use Modern::Perl;
21
22 use Clone 'clone';
23 use File::Basename qw( basename );
24 use Encode qw( encode );
25 use Mail::Sendmail;
26 use Try::Tiny;
27 use DateTime;
28
29 use Koha::Database;
30 use Koha::Email;
31 use Koha::Exceptions::Ill;
32 use Koha::Illcomments;
33 use Koha::Illrequestattributes;
34 use Koha::AuthorisedValue;
35 use Koha::Illrequest::Logger;
36 use Koha::Patron;
37 use Koha::AuthorisedValues;
38 use Koha::Biblios;
39 use Koha::Items;
40 use Koha::ItemTypes;
41 use Koha::Libraries;
42 use C4::Circulation qw( CanBookBeIssued AddIssue  );
43
44 use base qw(Koha::Object);
45
46 =head1 NAME
47
48 Koha::Illrequest - Koha Illrequest Object class
49
50 =head1 (Re)Design
51
52 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
53 of related Illrequestattributes.
54
55 The former encapsulates the basic necessary information that any ILL requires
56 to be usable in Koha.  The latter is a set of additional properties used by
57 one of the backends.
58
59 The former subsumes the legacy "Status" object.  The latter remains
60 encapsulated in the "Record" object.
61
62 TODO:
63
64 - Anything invoking the ->status method; annotated with:
65   + # Old use of ->status !
66
67 =head1 API
68
69 =head2 Backend API Response Principles
70
71 All methods should return a hashref in the following format:
72
73 =over
74
75 =item * error
76
77 This should be set to 1 if an error was encountered.
78
79 =item * status
80
81 The status should be a string from the list of statuses detailed below.
82
83 =item * message
84
85 The message is a free text field that can be passed on to the end user.
86
87 =item * value
88
89 The value returned by the method.
90
91 =back
92
93 =head2 Interface Status Messages
94
95 =over
96
97 =item * branch_address_incomplete
98
99 An interface request has determined branch address details are incomplete.
100
101 =item * cancel_success
102
103 The interface's cancel_request method was successful in cancelling the
104 Illrequest using the API.
105
106 =item * cancel_fail
107
108 The interface's cancel_request method failed to cancel the Illrequest using
109 the API.
110
111 =item * unavailable
112
113 The interface's request method returned saying that the desired item is not
114 available for request.
115
116 =back
117
118 =head2 Class methods
119
120 =head3 statusalias
121
122     my $statusalias = $request->statusalias;
123
124 Returns a request's status alias, as a Koha::AuthorisedValue instance
125 or implicit undef. This is distinct from status_alias, which only returns
126 the value in the status_alias column, this method returns the entire
127 AuthorisedValue object
128
129 =cut
130
131 sub statusalias {
132     my ( $self ) = @_;
133     return unless $self->status_alias;
134     # We can't know which result is the right one if there are multiple
135     # ILLSTATUS authorised values with the same authorised_value column value
136     # so we just use the first
137     return Koha::AuthorisedValues->search({
138         branchcode => $self->branchcode,
139         category => 'ILLSTATUS',
140         authorised_value => $self->SUPER::status_alias
141     })->next;
142 }
143
144 =head3 illrequestattributes
145
146 =cut
147
148 sub illrequestattributes {
149     my ( $self ) = @_;
150     return Koha::Illrequestattributes->_new_from_dbic(
151         scalar $self->_result->illrequestattributes
152     );
153 }
154
155 =head3 illcomments
156
157 =cut
158
159 sub illcomments {
160     my ( $self ) = @_;
161     return Koha::Illcomments->_new_from_dbic(
162         scalar $self->_result->illcomments
163     );
164 }
165
166 =head3 logs
167
168 =cut
169
170 sub logs {
171     my ( $self ) = @_;
172     my $logger = Koha::Illrequest::Logger->new;
173     return $logger->get_request_logs($self);
174 }
175
176 =head3 patron
177
178 =cut
179
180 sub patron {
181     my ( $self ) = @_;
182     return Koha::Patron->_new_from_dbic(
183         scalar $self->_result->borrowernumber
184     );
185 }
186
187 =head3 status_alias
188
189     $Illrequest->status_alias(143);
190
191 Overloaded getter/setter for status_alias,
192 that only returns authorised values from the
193 correct category and records the fact that the status has changed
194
195 =cut
196
197 sub status_alias {
198     my ($self, $new_status_alias) = @_;
199
200     my $current_status_alias = $self->SUPER::status_alias;
201
202     if ($new_status_alias) {
203         # Keep a record of the previous status before we change it,
204         # we might need it
205         $self->{previous_status} = $current_status_alias ?
206             $current_status_alias :
207             scalar $self->status;
208         # This is hackery to enable us to undefine
209         # status_alias, since we need to have an overloaded
210         # status_alias method to get us around the problem described
211         # here:
212         # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
213         # We need a way of accepting implied undef, so we can nullify
214         # the status_alias column, when called from $self->status
215         my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
216         my $ret = $self->SUPER::status_alias($val);
217         my $val_to_log = $val ? $new_status_alias : scalar $self->status;
218         if ($ret) {
219             my $logger = Koha::Illrequest::Logger->new;
220             $logger->log_status_change({
221                 request => $self,
222                 value   => $val_to_log
223             });
224         } else {
225             delete $self->{previous_status};
226         }
227         return $ret;
228     }
229     # We can't know which result is the right one if there are multiple
230     # ILLSTATUS authorised values with the same authorised_value column value
231     # so we just use the first
232     my $alias = Koha::AuthorisedValues->search({
233         branchcode => $self->branchcode,
234         category => 'ILLSTATUS',
235         authorised_value => $self->SUPER::status_alias
236     })->next;
237     if ($alias) {
238         return $alias->authorised_value;
239     } else {
240         return;
241     }
242 }
243
244 =head3 status
245
246     $Illrequest->status('CANREQ');
247
248 Overloaded getter/setter for request status,
249 also nullifies status_alias and records the fact that the status has changed
250
251 =cut
252
253 sub status {
254     my ( $self, $new_status) = @_;
255
256     my $current_status = $self->SUPER::status;
257     my $current_status_alias = $self->SUPER::status_alias;
258
259     if ($new_status) {
260         # Keep a record of the previous status before we change it,
261         # we might need it
262         $self->{previous_status} = $current_status_alias ?
263             $current_status_alias :
264             $current_status;
265         my $ret = $self->SUPER::status($new_status)->store;
266         if ($current_status_alias) {
267             # This is hackery to enable us to undefine
268             # status_alias, since we need to have an overloaded
269             # status_alias method to get us around the problem described
270             # here:
271             # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
272             # We need a way of passing implied undef to nullify status_alias
273             # so we pass -1, which is special cased in the overloaded setter
274             $self->status_alias("-1");
275         } else {
276             my $logger = Koha::Illrequest::Logger->new;
277             $logger->log_status_change({
278                 request => $self,
279                 value   => $new_status
280             });
281         }
282         delete $self->{previous_status};
283         return $ret;
284     } else {
285         return $current_status;
286     }
287 }
288
289 =head3 load_backend
290
291 Require "Base.pm" from the relevant ILL backend.
292
293 =cut
294
295 sub load_backend {
296     my ( $self, $backend_id ) = @_;
297
298     my @raw = qw/Koha Illbackends/; # Base Path
299
300     my $backend_name = $backend_id || $self->backend;
301
302     unless ( defined $backend_name && $backend_name ne '' ) {
303         Koha::Exceptions::Ill::InvalidBackendId->throw(
304             "An invalid backend ID was requested ('')");
305     }
306
307     my $location = join "/", @raw, $backend_name, "Base.pm";    # File to load
308     my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
309     require $location;
310     $self->{_my_backend} = $backend_class->new({
311         config => $self->_config,
312         logger => Koha::Illrequest::Logger->new
313     });
314     return $self;
315 }
316
317
318 =head3 _backend
319
320     my $backend = $abstract->_backend($new_backend);
321     my $backend = $abstract->_backend;
322
323 Getter/Setter for our API object.
324
325 =cut
326
327 sub _backend {
328     my ( $self, $backend ) = @_;
329     $self->{_my_backend} = $backend if ( $backend );
330     # Dynamically load our backend object, as late as possible.
331     $self->load_backend unless ( $self->{_my_backend} );
332     return $self->{_my_backend};
333 }
334
335 =head3 _backend_capability
336
337     my $backend_capability_result = $self->_backend_capability($name, $args);
338
339 This is a helper method to invoke optional capabilities in the backend.  If
340 the capability named by $name is not supported, return 0, else invoke it,
341 passing $args along with the invocation, and return its return value.
342
343 NOTE: this module suffers from a confusion in termninology:
344
345 in _backend_capability, the notion of capability refers to an optional feature
346 that is implemented in core, but might not be supported by a given backend.
347
348 in capabilities & custom_capability, capability refers to entries in the
349 status_graph (after union between backend and core).
350
351 The easiest way to fix this would be to fix the terminology in
352 capabilities & custom_capability and their callers.
353
354 =cut
355
356 sub _backend_capability {
357     my ( $self, $name, $args ) = @_;
358     my $capability = 0;
359     # See if capability is defined in backend
360     try {
361         $capability = $self->_backend->capabilities($name);
362     } catch {
363         return 0;
364     };
365     # Try to invoke it
366     if ( $capability && ref($capability) eq 'CODE' ) {
367         return &{$capability}($args);
368     } else {
369         return 0;
370     }
371 }
372
373 =head3 _config
374
375     my $config = $abstract->_config($config);
376     my $config = $abstract->_config;
377
378 Getter/Setter for our config object.
379
380 =cut
381
382 sub _config {
383     my ( $self, $config ) = @_;
384     $self->{_my_config} = $config if ( $config );
385     # Load our config object, as late as possible.
386     unless ( $self->{_my_config} ) {
387         $self->{_my_config} = Koha::Illrequest::Config->new;
388     }
389     return $self->{_my_config};
390 }
391
392 =head3 metadata
393
394 =cut
395
396 sub metadata {
397     my ( $self ) = @_;
398     return $self->_backend->metadata($self);
399 }
400
401 =head3 _core_status_graph
402
403     my $core_status_graph = $illrequest->_core_status_graph;
404
405 Returns ILL module's default status graph.  A status graph defines the list of
406 available actions at any stage in the ILL workflow.  This is for instance used
407 by the perl script & template to generate the correct buttons to display to
408 the end user at any given point.
409
410 =cut
411
412 sub _core_status_graph {
413     my ( $self ) = @_;
414     return {
415         NEW => {
416             prev_actions => [ ],                           # Actions containing buttons
417                                                            # leading to this status
418             id             => 'NEW',                       # ID of this status
419             name           => 'New request',               # UI name of this status
420             ui_method_name => 'New request',               # UI name of method leading
421                                                            # to this status
422             method         => 'create',                    # method to this status
423             next_actions   => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
424                                                            # requests with this status
425             ui_method_icon => 'fa-plus',                   # UI Style class
426         },
427         REQ => {
428             prev_actions   => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
429             id             => 'REQ',
430             name           => 'Requested',
431             ui_method_name => 'Confirm request',
432             method         => 'confirm',
433             next_actions   => [ 'REQREV', 'COMP', 'CHK' ],
434             ui_method_icon => 'fa-check',
435         },
436         GENREQ => {
437             prev_actions   => [ 'NEW', 'REQREV' ],
438             id             => 'GENREQ',
439             name           => 'Requested from partners',
440             ui_method_name => 'Place request with partners',
441             method         => 'generic_confirm',
442             next_actions   => [ 'COMP', 'CHK' ],
443             ui_method_icon => 'fa-send-o',
444         },
445         REQREV => {
446             prev_actions   => [ 'REQ' ],
447             id             => 'REQREV',
448             name           => 'Request reverted',
449             ui_method_name => 'Revert Request',
450             method         => 'cancel',
451             next_actions   => [ 'REQ', 'GENREQ', 'KILL' ],
452             ui_method_icon => 'fa-times',
453         },
454         QUEUED => {
455             prev_actions   => [ ],
456             id             => 'QUEUED',
457             name           => 'Queued request',
458             ui_method_name => 0,
459             method         => 0,
460             next_actions   => [ 'REQ', 'KILL' ],
461             ui_method_icon => 0,
462         },
463         CANCREQ => {
464             prev_actions   => [ 'NEW' ],
465             id             => 'CANCREQ',
466             name           => 'Cancellation requested',
467             ui_method_name => 0,
468             method         => 0,
469             next_actions   => [ 'KILL', 'REQ' ],
470             ui_method_icon => 0,
471         },
472         COMP => {
473             prev_actions   => [ 'REQ' ],
474             id             => 'COMP',
475             name           => 'Completed',
476             ui_method_name => 'Mark completed',
477             method         => 'mark_completed',
478             next_actions   => [ 'CHK' ],
479             ui_method_icon => 'fa-check',
480         },
481         KILL => {
482             prev_actions   => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
483             id             => 'KILL',
484             name           => 0,
485             ui_method_name => 'Delete request',
486             method         => 'delete',
487             next_actions   => [ ],
488             ui_method_icon => 'fa-trash',
489         },
490         CHK => {
491             prev_actions   => [ 'REQ', 'GENREQ', 'COMP' ],
492             id             => 'CHK',
493             name           => 'Checked out',
494             ui_method_name => 'Check out',
495             needs_prefs    => [ 'CirculateILL' ],
496             needs_perms    => [ 'user_circulate_circulate_remaining_permissions' ],
497             # An array of functions that all must return true
498             needs_all      => [ sub { my $r = shift;  return $r->biblio; } ],
499             method         => 'check_out',
500             next_actions   => [ ],
501             ui_method_icon => 'fa-upload',
502         },
503         RET => {
504             prev_actions   => [ 'CHK' ],
505             id             => 'RET',
506             name           => 'Returned to library',
507             ui_method_name => 'Check in',
508             method         => 'check_in',
509             next_actions   => [ 'COMP' ],
510             ui_method_icon => 'fa-download',
511         }
512     };
513 }
514
515 =head3 _status_graph_union
516
517     my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
518
519 Return a new status_graph, the result of merging $origin & new_graph.  This is
520 operation is a union over the sets defied by the two graphs.
521
522 Each entry in $new_graph is added to $origin.  We do not provide a syntax for
523 'subtraction' of entries from $origin.
524
525 Whilst it is not intended that this works, you can override entries in $origin
526 with entries with the same key in $new_graph.  This can lead to problematic
527 behaviour when $new_graph adds an entry, which modifies a dependent entry in
528 $origin, only for the entry in $origin to be replaced later with a new entry
529 from $new_graph.
530
531 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
532 i.e. each of the graphs need to be correct at the outset of the operation.
533
534 =cut
535
536 sub _status_graph_union {
537     my ( $self, $core_status_graph, $backend_status_graph ) = @_;
538     # Create new status graph with:
539     # - all core_status_graph
540     # - for-each each backend_status_graph
541     #   + add to new status graph
542     #   + for each core prev_action:
543     #     * locate core_status
544     #     * update next_actions with additional next action.
545     #   + for each core next_action:
546     #     * locate core_status
547     #     * update prev_actions with additional prev action
548
549     my @core_status_ids = keys %{$core_status_graph};
550     my $status_graph = clone($core_status_graph);
551
552     foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
553         my $backend_status = $backend_status_graph->{$backend_status_key};
554         # Add to new status graph
555         $status_graph->{$backend_status_key} = $backend_status;
556         # Update all core methods' next_actions.
557         foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
558             if ( grep { $prev_action eq $_ } @core_status_ids ) {
559                 my @next_actions =
560                      @{$status_graph->{$prev_action}->{next_actions}};
561                 push @next_actions, $backend_status_key;
562                 $status_graph->{$prev_action}->{next_actions}
563                     = \@next_actions;
564             }
565         }
566         # Update all core methods' prev_actions
567         foreach my $next_action ( @{$backend_status->{next_actions}} ) {
568             if ( grep { $next_action eq $_ } @core_status_ids ) {
569                 my @prev_actions =
570                      @{$status_graph->{$next_action}->{prev_actions}};
571                 push @prev_actions, $backend_status_key;
572                 $status_graph->{$next_action}->{prev_actions}
573                     = \@prev_actions;
574             }
575         }
576     }
577
578     return $status_graph;
579 }
580
581 ### Core API methods
582
583 =head3 capabilities
584
585     my $capabilities = $illrequest->capabilities;
586
587 Return a hashref mapping methods to operation names supported by the queried
588 backend.
589
590 Example return value:
591
592     { create => "Create Request", confirm => "Progress Request" }
593
594 NOTE: this module suffers from a confusion in termninology:
595
596 in _backend_capability, the notion of capability refers to an optional feature
597 that is implemented in core, but might not be supported by a given backend.
598
599 in capabilities & custom_capability, capability refers to entries in the
600 status_graph (after union between backend and core).
601
602 The easiest way to fix this would be to fix the terminology in
603 capabilities & custom_capability and their callers.
604
605 =cut
606
607 sub capabilities {
608     my ( $self, $status ) = @_;
609     # Generate up to date status_graph
610     my $status_graph = $self->_status_graph_union(
611         $self->_core_status_graph,
612         $self->_backend->status_graph({
613             request => $self,
614             other   => {}
615         })
616     );
617     # Extract available actions from graph.
618     return $status_graph->{$status} if $status;
619     # Or return entire graph.
620     return $status_graph;
621 }
622
623 =head3 custom_capability
624
625 Return the result of invoking $CANDIDATE on this request's backend with
626 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
627
628 NOTE: this module suffers from a confusion in termninology:
629
630 in _backend_capability, the notion of capability refers to an optional feature
631 that is implemented in core, but might not be supported by a given backend.
632
633 in capabilities & custom_capability, capability refers to entries in the
634 status_graph (after union between backend and core).
635
636 The easiest way to fix this would be to fix the terminology in
637 capabilities & custom_capability and their callers.
638
639 =cut
640
641 sub custom_capability {
642     my ( $self, $candidate, $params ) = @_;
643     foreach my $capability ( values %{$self->capabilities} ) {
644         if ( $candidate eq $capability->{method} ) {
645             my $response =
646                 $self->_backend->$candidate({
647                     request    => $self,
648                     other      => $params,
649                 });
650             return $self->expandTemplate($response);
651         }
652     }
653     return 0;
654 }
655
656 =head3 available_backends
657
658 Return a list of available backends.
659
660 =cut
661
662 sub available_backends {
663     my ( $self, $reduced ) = @_;
664     my $backends = $self->_config->available_backends($reduced);
665     return $backends;
666 }
667
668 =head3 available_actions
669
670 Return a list of available actions.
671
672 =cut
673
674 sub available_actions {
675     my ( $self ) = @_;
676     my $current_action = $self->capabilities($self->status);
677     my @available_actions = map { $self->capabilities($_) }
678         @{$current_action->{next_actions}};
679     return \@available_actions;
680 }
681
682 =head3 mark_completed
683
684 Mark a request as completed (status = COMP).
685
686 =cut
687
688 sub mark_completed {
689     my ( $self ) = @_;
690     $self->status('COMP')->store;
691     $self->completed(DateTime->now)->store;
692     return {
693         error   => 0,
694         status  => '',
695         message => '',
696         method  => 'mark_completed',
697         stage   => 'commit',
698         next    => 'illview',
699     };
700 }
701
702 =head2 backend_migrate
703
704 Migrate a request from one backend to another.
705
706 =cut
707
708 sub backend_migrate {
709     my ( $self, $params ) = @_;
710
711     my $response = $self->_backend_capability('migrate',{
712             request    => $self,
713             other      => $params,
714         });
715     return $self->expandTemplate($response) if $response;
716     return $response;
717 }
718
719 =head2 backend_confirm
720
721 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
722
723 =over
724
725 =item * orderid
726
727 =item * accessurl, cost (if available).
728
729 =back
730
731 =cut
732
733 sub backend_confirm {
734     my ( $self, $params ) = @_;
735
736     my $response = $self->_backend->confirm({
737             request    => $self,
738             other      => $params,
739         });
740     return $self->expandTemplate($response);
741 }
742
743 =head3 backend_update_status
744
745 =cut
746
747 sub backend_update_status {
748     my ( $self, $params ) = @_;
749     return $self->expandTemplate($self->_backend->update_status($params));
750 }
751
752 =head3 backend_cancel
753
754     my $ILLResponse = $illRequest->backend_cancel;
755
756 The standard interface method allowing for request cancellation.
757
758 =cut
759
760 sub backend_cancel {
761     my ( $self, $params ) = @_;
762
763     my $result = $self->_backend->cancel({
764         request => $self,
765         other => $params
766     });
767
768     return $self->expandTemplate($result);
769 }
770
771 =head3 backend_renew
772
773     my $renew_response = $illRequest->backend_renew;
774
775 The standard interface method allowing for request renewal queries.
776
777 =cut
778
779 sub backend_renew {
780     my ( $self ) = @_;
781     return $self->expandTemplate(
782         $self->_backend->renew({
783             request    => $self,
784         })
785     );
786 }
787
788 =head3 backend_create
789
790     my $create_response = $abstractILL->backend_create($params);
791
792 Return an array of Record objects created by querying our backend with
793 a Search query.
794
795 In the context of the other ILL methods, this is a special method: we only
796 pass it $params, as it does not yet have any other data associated with it.
797
798 =cut
799
800 sub backend_create {
801     my ( $self, $params ) = @_;
802
803     # Establish whether we need to do a generic copyright clearance.
804     if ($params->{opac}) {
805         if ( ( !$params->{stage} || $params->{stage} eq 'init' )
806                 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
807             return {
808                 error   => 0,
809                 status  => '',
810                 message => '',
811                 method  => 'create',
812                 stage   => 'copyrightclearance',
813                 value   => {
814                     other   => $params,
815                     backend => $self->_backend->name
816                 }
817             };
818         } elsif (     defined $params->{stage}
819                 && $params->{stage} eq 'copyrightclearance' ) {
820             $params->{stage} = 'init';
821         }
822     }
823     # First perform API action, then...
824     my $args = {
825         request => $self,
826         other   => $params,
827     };
828     my $result = $self->_backend->create($args);
829
830     # ... simple case: we're not at 'commit' stage.
831     my $stage = $result->{stage};
832     return $self->expandTemplate($result)
833         unless ( 'commit' eq $stage );
834
835     # ... complex case: commit!
836
837     # Do we still have space for an ILL or should we queue?
838     my $permitted = $self->check_limits(
839         { patron => $self->patron }, { librarycode => $self->branchcode }
840     );
841
842     # Now augment our committed request.
843
844     $result->{permitted} = $permitted;             # Queue request?
845
846     # This involves...
847
848     # ...Updating status!
849     $self->status('QUEUED')->store unless ( $permitted );
850
851     ## Handle Unmediated ILLs
852
853     # For the unmediated workflow we only need to delegate to our backend. If
854     # that backend supports unmediateld_ill, it will do its thing and return a
855     # proper response.  If it doesn't then _backend_capability returns 0, so
856     # we keep the current result.
857     if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
858         my $unmediated_result = $self->_backend_capability(
859             'unmediated_ill',
860             $args
861         );
862         $result = $unmediated_result if $unmediated_result;
863     }
864
865     return $self->expandTemplate($result);
866 }
867
868 =head3 expandTemplate
869
870     my $params = $abstract->expandTemplate($params);
871
872 Return a version of $PARAMS augmented with our required template path.
873
874 =cut
875
876 sub expandTemplate {
877     my ( $self, $params ) = @_;
878     my $backend = $self->_backend->name;
879     # Generate path to file to load
880     my $backend_dir = $self->_config->backend_dir;
881     my $backend_tmpl = join "/", $backend_dir, $backend;
882     my $intra_tmpl =  join "/", $backend_tmpl, "intra-includes",
883         ( $params->{method}//q{} ) . ".inc";
884     my $opac_tmpl =  join "/", $backend_tmpl, "opac-includes",
885         ( $params->{method}//q{} ) . ".inc";
886     # Set files to load
887     $params->{template} = $intra_tmpl;
888     $params->{opac_template} = $opac_tmpl;
889     return $params;
890 }
891
892 #### Abstract Imports
893
894 =head3 getLimits
895
896     my $limit_rules = $abstract->getLimits( {
897         type  => 'brw_cat' | 'branch',
898         value => $value
899     } );
900
901 Return the ILL limit rules for the supplied combination of type / value.
902
903 As the config may have no rules for this particular type / value combination,
904 or for the default, we must define fall-back values here.
905
906 =cut
907
908 sub getLimits {
909     my ( $self, $params ) = @_;
910     my $limits = $self->_config->getLimitRules($params->{type});
911
912     if (     defined $params->{value}
913           && defined $limits->{$params->{value}} ) {
914             return $limits->{$params->{value}};
915     }
916     else {
917         return $limits->{default} || { count => -1, method => 'active' };
918     }
919 }
920
921 =head3 getPrefix
922
923     my $prefix = $abstract->getPrefix( {
924         branch  => $branch_code
925     } );
926
927 Return the ILL prefix as defined by our $params: either per borrower category,
928 per branch or the default.
929
930 =cut
931
932 sub getPrefix {
933     my ( $self, $params ) = @_;
934     my $brn_prefixes = $self->_config->getPrefixes();
935     return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
936 }
937
938 =head3 get_type
939
940     my $type = $abstract->get_type();
941
942 Return a string representing the material type of this request or undef
943
944 =cut
945
946 sub get_type {
947     my ($self) = @_;
948     my $attr = $self->illrequestattributes->find({ type => 'type'});
949     return if !$attr;
950     return $attr->value;
951 };
952
953 #### Illrequests Imports
954
955 =head3 check_limits
956
957     my $ok = $illRequests->check_limits( {
958         borrower   => $borrower,
959         branchcode => 'branchcode' | undef,
960     } );
961
962 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
963 see whether we are still able to place ILLs.
964
965 LimitRules are derived from koha-conf.xml:
966  + default limit counts, and counting method
967  + branch specific limit counts & counting method
968  + borrower category specific limit counts & counting method
969  + err on the side of caution: a counting fail will cause fail, even if
970    the other counts passes.
971
972 =cut
973
974 sub check_limits {
975     my ( $self, $params ) = @_;
976     my $patron     = $params->{patron};
977     my $branchcode = $params->{librarycode} || $patron->branchcode;
978
979     # Establish maximum number of allowed requests
980     my ( $branch_rules, $brw_rules ) = (
981         $self->getLimits( {
982             type => 'branch',
983             value => $branchcode
984         } ),
985         $self->getLimits( {
986             type => 'brw_cat',
987             value => $patron->categorycode,
988         } ),
989     );
990     my ( $branch_limit, $brw_limit )
991         = ( $branch_rules->{count}, $brw_rules->{count} );
992     # Establish currently existing requests
993     my ( $branch_count, $brw_count ) = (
994         $self->_limit_counter(
995             $branch_rules->{method}, { branchcode => $branchcode }
996         ),
997         $self->_limit_counter(
998             $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
999         ),
1000     );
1001
1002     # Compare and return
1003     # A limit of -1 means no limit exists.
1004     # We return blocked if either branch limit or brw limit is reached.
1005     if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
1006              || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
1007         return 0;
1008     } else {
1009         return 1;
1010     }
1011 }
1012
1013 sub _limit_counter {
1014     my ( $self, $method, $target ) = @_;
1015
1016     # Establish parameters of counts
1017     my $resultset;
1018     if ($method && $method eq 'annual') {
1019         $resultset = Koha::Illrequests->search({
1020             -and => [
1021                 %{$target},
1022                 \"YEAR(placed) = YEAR(NOW())"
1023             ]
1024         });
1025     } else {                    # assume 'active'
1026         # XXX: This status list is ugly. There should be a method in config
1027         # to return these.
1028         my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1029         $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1030     }
1031
1032     # Fetch counts
1033     return $resultset->count;
1034 }
1035
1036 =head3 requires_moderation
1037
1038     my $status = $illRequest->requires_moderation;
1039
1040 Return the name of the status if moderation by staff is required; or 0
1041 otherwise.
1042
1043 =cut
1044
1045 sub requires_moderation {
1046     my ( $self ) = @_;
1047     my $require_moderation = {
1048         'CANCREQ' => 'CANCREQ',
1049     };
1050     return $require_moderation->{$self->status};
1051 }
1052
1053 =head3 biblio
1054
1055     my $biblio = $request->biblio;
1056
1057 For a given request, return the biblio associated with it,
1058 or undef if none exists
1059
1060 =cut
1061
1062 sub biblio {
1063     my ( $self ) = @_;
1064
1065     return if !$self->biblio_id;
1066
1067     return Koha::Biblios->find({
1068         biblionumber => $self->biblio_id
1069     });
1070 }
1071
1072 =head3 check_out
1073
1074     my $stage_summary = $request->check_out;
1075
1076 Handle the check_out method. The first stage involves gathering the required
1077 data from the user via a form, the second stage creates an item and tries to
1078 issue it to the patron. If successful, it notifies the patron, then it
1079 returns a summary of how things went
1080
1081 =cut
1082
1083 sub check_out {
1084     my ( $self, $params ) = @_;
1085
1086     # Objects required by the template
1087     my $itemtypes = Koha::ItemTypes->search(
1088         {},
1089         { order_by => ['description'] }
1090     );
1091     my $libraries = Koha::Libraries->search(
1092         {},
1093         { order_by => ['branchcode'] }
1094     );
1095     my $biblio = $self->biblio;
1096
1097     # Find all statistical patrons
1098     my $statistical_patrons = Koha::Patrons->search(
1099         { 'category_type' => 'x' },
1100         { join => { 'categorycode' => 'borrowers' } }
1101     );
1102
1103     if (!$params->{stage} || $params->{stage} eq 'init') {
1104         # Present a form to gather the required data
1105         #
1106         # We may be viewing this page having previously tried to issue
1107         # the item (in which case, we may already have created an item)
1108         # so we pass the biblio for this request
1109         return {
1110             method  => 'check_out',
1111             stage   => 'form',
1112             value   => {
1113                 itemtypes   => $itemtypes,
1114                 libraries   => $libraries,
1115                 statistical => $statistical_patrons,
1116                 biblio      => $biblio
1117             }
1118         };
1119     } elsif ($params->{stage} eq 'form') {
1120         # Validate what we've got and return with an error if we fail
1121         my $errors = {};
1122         if (!$params->{item_type} || length $params->{item_type} == 0) {
1123             $errors->{item_type} = 1;
1124         }
1125         if ($params->{inhouse} && length $params->{inhouse} > 0) {
1126             my $patron_count = Koha::Patrons->search({
1127                 cardnumber => $params->{inhouse}
1128             })->count();
1129             if ($patron_count != 1) {
1130                 $errors->{inhouse} = 1;
1131             }
1132         }
1133
1134         # Check we don't have more than one item for this bib,
1135         # if we do, something very odd is going on
1136         # Having 1 is OK, it means we're likely trying to issue
1137         # following a previously failed attempt, the item exists
1138         # so we'll use it
1139         my @items = $biblio->items->as_list;
1140         my $item_count = scalar @items;
1141         if ($item_count > 1) {
1142             $errors->{itemcount} = 1;
1143         }
1144
1145         # Failed validation, go back to the form
1146         if (%{$errors}) {
1147             return {
1148                 method  => 'check_out',
1149                 stage   => 'form',
1150                 value   => {
1151                     params      => $params,
1152                     statistical => $statistical_patrons,
1153                     itemtypes   => $itemtypes,
1154                     libraries   => $libraries,
1155                     biblio      => $biblio,
1156                     errors      => $errors
1157                 }
1158             };
1159         }
1160
1161         # Passed validation
1162         #
1163         # Create an item if one doesn't already exist,
1164         # if one does, use that
1165         my $itemnumber;
1166         if ($item_count == 0) {
1167             my $item_hash = {
1168                 biblionumber  => $self->biblio_id,
1169                 homebranch    => $params->{branchcode},
1170                 holdingbranch => $params->{branchcode},
1171                 location      => $params->{branchcode},
1172                 itype         => $params->{item_type},
1173                 barcode       => 'ILL-' . $self->illrequest_id
1174             };
1175             try {
1176                 my $item = Koha::Item->new($item_hash)->store;
1177                 $itemnumber = $item->itemnumber;
1178             };
1179         } else {
1180             $itemnumber = $items[0]->itemnumber;
1181         }
1182         # Check we have an item before going forward
1183         if (!$itemnumber) {
1184             return {
1185                 method  => 'check_out',
1186                 stage   => 'form',
1187                 value   => {
1188                     params      => $params,
1189                     itemtypes   => $itemtypes,
1190                     libraries   => $libraries,
1191                     statistical => $statistical_patrons,
1192                     errors      => { item_creation => 1 }
1193                 }
1194             };
1195         }
1196
1197         # Do the check out
1198         #
1199         # Gather what we need
1200         my $target_item = Koha::Items->find( $itemnumber );
1201         # Determine who we're issuing to
1202         my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
1203             Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
1204             $self->patron;
1205
1206         my @issue_args = (
1207             $patron,
1208             scalar $target_item->barcode
1209         );
1210         if ($params->{duedate} && length $params->{duedate} > 0) {
1211             push @issue_args, $params->{duedate};
1212         }
1213         # Check if we can check out
1214         my ( $error, $confirm, $alerts, $messages ) =
1215             C4::Circulation::CanBookBeIssued(@issue_args);
1216
1217         # If we got anything back saying we can't check out,
1218         # return it to the template
1219         my $problems = {};
1220         if ( $error && %{$error} ) { $problems->{error} = $error };
1221         if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
1222         if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
1223         if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
1224
1225         if (%{$problems}) {
1226             return {
1227                 method  => 'check_out',
1228                 stage   => 'form',
1229                 value   => {
1230                     params           => $params,
1231                     itemtypes        => $itemtypes,
1232                     libraries        => $libraries,
1233                     statistical      => $statistical_patrons,
1234                     patron           => $patron,
1235                     biblio           => $biblio,
1236                     check_out_errors => $problems
1237                 }
1238             };
1239         }
1240
1241         # We can allegedly check out, so make it so
1242         # For some reason, AddIssue requires an unblessed Patron
1243         $issue_args[0] = $patron->unblessed;
1244         my $issue = C4::Circulation::AddIssue(@issue_args);
1245
1246         if ($issue) {
1247             # Update the request status
1248             $self->status('CHK')->store;
1249             return {
1250                 method  => 'check_out',
1251                 stage   => 'done_check_out',
1252                 value   => {
1253                     params    => $params,
1254                     patron    => $patron,
1255                     check_out => $issue
1256                 }
1257             };
1258         } else {
1259             return {
1260                 method  => 'check_out',
1261                 stage   => 'form',
1262                 value   => {
1263                     params    => $params,
1264                     itemtypes => $itemtypes,
1265                     libraries => $libraries,
1266                     errors    => { item_check_out => 1 }
1267                 }
1268             };
1269         }
1270     }
1271
1272 }
1273
1274 =head3 generic_confirm
1275
1276     my $stage_summary = $illRequest->generic_confirm;
1277
1278 Handle the generic_confirm extended method.  The first stage involves creating
1279 a template email for the end user to edit in the browser.  The second stage
1280 attempts to submit the email.
1281
1282 =cut
1283
1284 sub generic_confirm {
1285     my ( $self, $params ) = @_;
1286     my $branch = Koha::Libraries->find($params->{current_branchcode})
1287         || die "Invalid current branchcode. Are you logged in as the database user?";
1288     if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
1289         my $draft->{subject} = "ILL Request";
1290         $draft->{body} = <<EOF;
1291 Dear Sir/Madam,
1292
1293     We would like to request an interlibrary loan for a title matching the
1294 following description:
1295
1296 EOF
1297
1298         my $details = $self->metadata;
1299         while (my ($title, $value) = each %{$details}) {
1300             $draft->{body} .= "  - " . $title . ": " . $value . "\n"
1301                 if $value;
1302         }
1303         $draft->{body} .= <<EOF;
1304
1305 Please let us know if you are able to supply this to us.
1306
1307 Kind Regards
1308
1309 EOF
1310
1311         my @address = map { $branch->$_ }
1312             qw/ branchname branchaddress1 branchaddress2 branchaddress3
1313                 branchzip branchcity branchstate branchcountry branchphone
1314                 branchemail /;
1315         my $address = "";
1316         foreach my $line ( @address ) {
1317             $address .= $line . "\n" if $line;
1318         }
1319
1320         $draft->{body} .= $address;
1321
1322         my $partners = Koha::Patrons->search({
1323             categorycode => $self->_config->partner_code
1324         });
1325         return {
1326             error   => 0,
1327             status  => '',
1328             message => '',
1329             method  => 'generic_confirm',
1330             stage   => 'draft',
1331             value   => {
1332                 draft    => $draft,
1333                 partners => $partners,
1334             }
1335         };
1336
1337     } elsif ( 'draft' eq $params->{stage} ) {
1338         # Create the to header
1339         my $to = $params->{partners};
1340         if ( defined $to ) {
1341             $to =~ s/^\x00//;       # Strip leading NULLs
1342             $to =~ s/\x00/; /;      # Replace others with '; '
1343         }
1344         Koha::Exceptions::Ill::NoTargetEmail->throw(
1345             "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
1346           if ( !$to );
1347         # Create the from, replyto and sender headers
1348         my $from = $branch->branchemail;
1349         my $replyto = $branch->branchreplyto || $from;
1350         Koha::Exceptions::Ill::NoLibraryEmail->throw(
1351             "Your library has no usable email address. Please set it.")
1352           if ( !$from );
1353
1354         # Create the email
1355         my $message = Koha::Email->new;
1356         my %mail = $message->create_message_headers(
1357             {
1358                 to          => $to,
1359                 from        => $from,
1360                 replyto     => $replyto,
1361                 subject     => Encode::encode( "utf8", $params->{subject} ),
1362                 message     => Encode::encode( "utf8", $params->{body} ),
1363                 contenttype => 'text/plain',
1364             }
1365         );
1366         # Send it
1367         my $result = sendmail(%mail);
1368         if ( $result ) {
1369             $self->status("GENREQ")->store;
1370             $self->_backend_capability(
1371                 'set_requested_partners',
1372                 {
1373                     request => $self,
1374                     to => $to
1375                 }
1376             );
1377             return {
1378                 error   => 0,
1379                 status  => '',
1380                 message => '',
1381                 method  => 'generic_confirm',
1382                 stage   => 'commit',
1383                 next    => 'illview',
1384             };
1385         } else {
1386             return {
1387                 error   => 1,
1388                 status  => 'email_failed',
1389                 message => $Mail::Sendmail::error,
1390                 method  => 'generic_confirm',
1391                 stage   => 'draft',
1392             };
1393         }
1394     } else {
1395         die "Unknown stage, should not have happened."
1396     }
1397 }
1398
1399 =head3 id_prefix
1400
1401     my $prefix = $record->id_prefix;
1402
1403 Return the prefix appropriate for the current Illrequest as derived from the
1404 borrower and branch associated with this request's Status, and the config
1405 file.
1406
1407 =cut
1408
1409 sub id_prefix {
1410     my ( $self ) = @_;
1411     my $prefix = $self->getPrefix( {
1412         branch  => $self->branchcode,
1413     } );
1414     $prefix .= "-" if ( $prefix );
1415     return $prefix;
1416 }
1417
1418 =head3 _censor
1419
1420     my $params = $illRequest->_censor($params);
1421
1422 Return $params, modified to reflect our censorship requirements.
1423
1424 =cut
1425
1426 sub _censor {
1427     my ( $self, $params ) = @_;
1428     my $censorship = $self->_config->censorship;
1429     $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1430         if ( $params->{opac} );
1431     $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1432
1433     return $params;
1434 }
1435
1436 =head3 store
1437
1438     $Illrequest->store;
1439
1440 Overloaded I<store> method that, in addition to performing the 'store',
1441 possibly records the fact that something happened
1442
1443 =cut
1444
1445 sub store {
1446     my ( $self, $attrs ) = @_;
1447
1448     my $ret = $self->SUPER::store;
1449
1450     $attrs->{log_origin} = 'core';
1451
1452     if ($ret && defined $attrs) {
1453         my $logger = Koha::Illrequest::Logger->new;
1454         $logger->log_maybe({
1455             request => $self,
1456             attrs   => $attrs
1457         });
1458     }
1459
1460     return $ret;
1461 }
1462
1463 =head3 requested_partners
1464
1465     my $partners_string = $illRequest->requested_partners;
1466
1467 Return the string representing the email addresses of the partners to
1468 whom a request has been sent
1469
1470 =cut
1471
1472 sub requested_partners {
1473     my ( $self ) = @_;
1474     return $self->_backend_capability(
1475         'get_requested_partners',
1476         { request => $self }
1477     );
1478 }
1479
1480 =head3 TO_JSON
1481
1482     $json = $illrequest->TO_JSON
1483
1484 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1485 into the unblessed representation of the object.
1486
1487 TODO: This method does nothing and is not called anywhere. However, bug 74325
1488 touches it, so keeping this for now until both this and bug 74325 are merged,
1489 at which point we can sort it out and remove it completely
1490
1491 =cut
1492
1493 sub TO_JSON {
1494     my ( $self, $embed ) = @_;
1495
1496     my $object = $self->SUPER::TO_JSON();
1497
1498     return $object;
1499 }
1500
1501 =head2 Internal methods
1502
1503 =head3 _type
1504
1505 =cut
1506
1507 sub _type {
1508     return 'Illrequest';
1509 }
1510
1511 =head1 AUTHOR
1512
1513 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1514 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>
1515
1516 =cut
1517
1518 1;