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