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