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