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