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