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