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