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