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