1 package Koha::Illrequest;
3 # Copyright PTFS Europe 2016,2018
5 # This file is part of Koha.
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.
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.
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>.
22 use Clone qw( clone );
23 use Try::Tiny qw( catch try );
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;
35 use Koha::AuthorisedValues;
41 use C4::Circulation qw( CanBookBeIssued AddIssue );
43 use base qw(Koha::Object);
47 Koha::Illrequest - Koha Illrequest Object class
51 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
52 of related Illrequestattributes.
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
58 The former subsumes the legacy "Status" object. The latter remains
59 encapsulated in the "Record" object.
63 - Anything invoking the ->status method; annotated with:
64 + # Old use of ->status !
68 =head2 Backend API Response Principles
70 All methods should return a hashref in the following format:
76 This should be set to 1 if an error was encountered.
80 The status should be a string from the list of statuses detailed below.
84 The message is a free text field that can be passed on to the end user.
88 The value returned by the method.
92 =head2 Interface Status Messages
96 =item * branch_address_incomplete
98 An interface request has determined branch address details are incomplete.
100 =item * cancel_success
102 The interface's cancel_request method was successful in cancelling the
103 Illrequest using the API.
107 The interface's cancel_request method failed to cancel the Illrequest using
112 The interface's request method returned saying that the desired item is not
113 available for request.
119 =head3 init_processors
121 $request->init_processors()
123 Initialises an empty processors arrayref
127 sub init_processors {
130 $self->{processors} = [];
133 =head3 push_processor
135 $request->push_processors(sub { ...something... });
137 Pushes a passed processor function into our processors arrayref
142 my ( $self, $processor ) = @_;
143 push @{$self->{processors}}, $processor;
148 my $statusalias = $request->statusalias;
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
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(
165 category => 'ILLSTATUS',
166 authorised_value => $self->SUPER::status_alias
173 =head3 illrequestattributes
177 sub illrequestattributes {
179 return Koha::Illrequestattributes->_new_from_dbic(
180 scalar $self->_result->illrequestattributes
190 return Koha::Illcomments->_new_from_dbic(
191 scalar $self->_result->illcomments
201 my $logger = Koha::Illrequest::Logger->new;
202 return $logger->get_request_logs($self);
211 return Koha::Patron->_new_from_dbic(
212 scalar $self->_result->borrowernumber
218 $Illrequest->status_alias(143);
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
227 my ($self, $new_status_alias) = @_;
229 my $current_status_alias = $self->SUPER::status_alias;
231 if ($new_status_alias) {
232 # Keep a record of the previous status before we change 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
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;
248 my $logger = Koha::Illrequest::Logger->new;
249 $logger->log_status_change({
254 delete $self->{previous_status};
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(
263 category => 'ILLSTATUS',
264 authorised_value => $self->SUPER::status_alias
271 return $alias->authorised_value;
279 $Illrequest->status('CANREQ');
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
288 my ( $self, $new_status) = @_;
290 my $current_status = $self->SUPER::status;
291 my $current_status_alias = $self->SUPER::status_alias;
294 # Keep a record of the previous status before we change it,
296 $self->{previous_status} = $current_status_alias ?
297 $current_status_alias :
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
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");
310 my $logger = Koha::Illrequest::Logger->new;
311 $logger->log_status_change({
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');
323 return $current_status;
329 Require "Base.pm" from the relevant ILL backend.
334 my ( $self, $backend_id ) = @_;
336 my @raw = qw/Koha Illbackends/; # Base Path
338 my $backend_name = $backend_id || $self->backend;
340 unless ( defined $backend_name && $backend_name ne '' ) {
341 Koha::Exceptions::Ill::InvalidBackendId->throw(
342 "An invalid backend ID was requested ('')");
345 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
346 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
348 $self->{_my_backend} = $backend_class->new({
349 config => $self->_config,
350 logger => Koha::Illrequest::Logger->new
358 my $backend = $abstract->_backend($new_backend);
359 my $backend = $abstract->_backend;
361 Getter/Setter for our API object.
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};
373 =head3 _backend_capability
375 my $backend_capability_result = $self->_backend_capability($name, $args);
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.
381 NOTE: this module suffers from a confusion in termninology:
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.
386 in capabilities & custom_capability, capability refers to entries in the
387 status_graph (after union between backend and core).
389 The easiest way to fix this would be to fix the terminology in
390 capabilities & custom_capability and their callers.
394 sub _backend_capability {
395 my ( $self, $name, $args ) = @_;
397 # See if capability is defined in backend
399 $capability = $self->_backend->capabilities($name);
405 if ( $capability && ref($capability) eq 'CODE' ) {
406 return &{$capability}($args);
414 my $config = $abstract->_config($config);
415 my $config = $abstract->_config;
417 Getter/Setter for our config object.
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;
428 return $self->{_my_config};
437 return $self->_backend->metadata($self);
440 =head3 _core_status_graph
442 my $core_status_graph = $illrequest->_core_status_graph;
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.
451 sub _core_status_graph {
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
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
467 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
470 ui_method_name => 'Confirm request',
472 next_actions => [ 'REQREV', 'COMP', 'CHK' ],
473 ui_method_icon => 'fa-check',
476 prev_actions => [ 'NEW', 'REQREV' ],
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',
485 prev_actions => [ 'REQ' ],
487 name => 'Request reverted',
488 ui_method_name => 'Revert Request',
490 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
491 ui_method_icon => 'fa-times',
496 name => 'Queued request',
499 next_actions => [ 'REQ', 'KILL' ],
503 prev_actions => [ 'NEW' ],
505 name => 'Cancellation requested',
508 next_actions => [ 'KILL', 'REQ' ],
512 prev_actions => [ 'REQ' ],
515 ui_method_name => 'Mark completed',
516 method => 'mark_completed',
517 next_actions => [ 'CHK' ],
518 ui_method_icon => 'fa-check',
521 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
524 ui_method_name => 'Delete request',
527 ui_method_icon => 'fa-trash',
530 prev_actions => [ 'REQ', 'GENREQ', 'COMP' ],
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',
540 ui_method_icon => 'fa-upload',
543 prev_actions => [ 'CHK' ],
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',
554 =head3 _status_graph_union
556 my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
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.
561 Each entry in $new_graph is added to $origin. We do not provide a syntax for
562 'subtraction' of entries from $origin.
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
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.
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
588 my @core_status_ids = keys %{$core_status_graph};
589 my $status_graph = clone($core_status_graph);
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 ) {
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}
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 ) {
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}
619 return $status_graph;
626 my $capabilities = $illrequest->capabilities;
628 Return a hashref mapping methods to operation names supported by the queried
631 Example return value:
633 { create => "Create Request", confirm => "Progress Request" }
635 NOTE: this module suffers from a confusion in termninology:
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.
640 in capabilities & custom_capability, capability refers to entries in the
641 status_graph (after union between backend and core).
643 The easiest way to fix this would be to fix the terminology in
644 capabilities & custom_capability and their callers.
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({
658 # Extract available actions from graph.
659 return $status_graph->{$status} if $status;
660 # Or return entire graph.
661 return $status_graph;
664 =head3 custom_capability
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.
669 NOTE: this module suffers from a confusion in termninology:
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.
674 in capabilities & custom_capability, capability refers to entries in the
675 status_graph (after union between backend and core).
677 The easiest way to fix this would be to fix the terminology in
678 capabilities & custom_capability and their callers.
682 sub custom_capability {
683 my ( $self, $candidate, $params ) = @_;
684 foreach my $capability ( values %{$self->capabilities} ) {
685 if ( $candidate eq $capability->{method} ) {
687 $self->_backend->$candidate({
691 return $self->expandTemplate($response);
697 =head3 available_backends
699 Return a list of available backends.
703 sub available_backends {
704 my ( $self, $reduced ) = @_;
705 my $backends = $self->_config->available_backends($reduced);
709 =head3 available_actions
711 Return a list of available actions.
715 sub available_actions {
717 my $current_action = $self->capabilities($self->status);
718 my @available_actions = map { $self->capabilities($_) }
719 @{$current_action->{next_actions}};
720 return \@available_actions;
723 =head3 mark_completed
725 Mark a request as completed (status = COMP).
731 $self->status('COMP')->store;
732 $self->completed(dt_from_string())->store;
737 method => 'mark_completed',
743 =head2 backend_illview
745 View and manage an ILL request
749 sub backend_illview {
750 my ( $self, $params ) = @_;
752 my $response = $self->_backend_capability('illview',{
756 return $self->expandTemplate($response) if $response;
760 =head2 backend_migrate
762 Migrate a request from one backend to another.
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',{
774 return $self->expandTemplate($response) if $response;
778 =head2 backend_confirm
780 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
786 =item * accessurl, cost (if available).
792 sub backend_confirm {
793 my ( $self, $params ) = @_;
795 my $response = $self->_backend->confirm({
799 return $self->expandTemplate($response);
802 =head3 backend_update_status
806 sub backend_update_status {
807 my ( $self, $params ) = @_;
808 return $self->expandTemplate($self->_backend->update_status($params));
811 =head3 backend_cancel
813 my $ILLResponse = $illRequest->backend_cancel;
815 The standard interface method allowing for request cancellation.
820 my ( $self, $params ) = @_;
822 my $result = $self->_backend->cancel({
827 return $self->expandTemplate($result);
832 my $renew_response = $illRequest->backend_renew;
834 The standard interface method allowing for request renewal queries.
840 return $self->expandTemplate(
841 $self->_backend->renew({
847 =head3 backend_create
849 my $create_response = $abstractILL->backend_create($params);
851 Return an array of Record objects created by querying our backend with
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.
860 my ( $self, $params ) = @_;
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") ) {
871 stage => 'copyrightclearance',
874 backend => $self->_backend->name
877 } elsif ( defined $params->{stage}
878 && $params->{stage} eq 'copyrightclearance' ) {
879 $params->{stage} = 'init';
882 # First perform API action, then...
887 my $result = $self->_backend->create($args);
889 # ... simple case: we're not at 'commit' stage.
890 my $stage = $result->{stage};
891 return $self->expandTemplate($result)
892 unless ( 'commit' eq $stage );
894 # ... complex case: commit!
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 }
901 # Now augment our committed request.
903 $result->{permitted} = $permitted; # Queue request?
907 # ...Updating status!
908 $self->status('QUEUED')->store unless ( $permitted );
910 ## Handle Unmediated ILLs
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(
921 $result = $unmediated_result if $unmediated_result;
924 return $self->expandTemplate($result);
927 =head3 backend_get_update
929 my $update = backend_get_update($request);
931 Given a request, returns an update in a prescribed
932 format that can then be passed to update parsers
936 sub backend_get_update {
937 my ( $self, $options ) = @_;
939 my $response = $self->_backend_capability(
940 'get_supplier_update',
949 =head3 expandTemplate
951 my $params = $abstract->expandTemplate($params);
953 Return a version of $PARAMS augmented with our required template path.
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";
968 $params->{template} = $intra_tmpl;
969 $params->{opac_template} = $opac_tmpl;
973 #### Abstract Imports
977 my $limit_rules = $abstract->getLimits( {
978 type => 'brw_cat' | 'branch',
982 Return the ILL limit rules for the supplied combination of type / value.
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.
990 my ( $self, $params ) = @_;
991 my $limits = $self->_config->getLimitRules($params->{type});
993 if ( defined $params->{value}
994 && defined $limits->{$params->{value}} ) {
995 return $limits->{$params->{value}};
998 return $limits->{default} || { count => -1, method => 'active' };
1004 my $prefix = $abstract->getPrefix( {
1005 branch => $branch_code
1008 Return the ILL prefix as defined by our $params: either per borrower category,
1009 per branch or the default.
1014 my ( $self, $params ) = @_;
1015 my $brn_prefixes = $self->_config->getPrefixes();
1016 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
1021 my $type = $abstract->get_type();
1023 Return a string representing the material type of this request or undef
1029 my $attr = $self->illrequestattributes->find({ type => 'type'});
1031 return $attr->value;
1034 #### Illrequests Imports
1038 my $ok = $illRequests->check_limits( {
1039 borrower => $borrower,
1040 branchcode => 'branchcode' | undef,
1043 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
1044 see whether we are still able to place ILLs.
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.
1056 my ( $self, $params ) = @_;
1057 my $patron = $params->{patron};
1058 my $branchcode = $params->{librarycode} || $patron->branchcode;
1060 # Establish maximum number of allowed requests
1061 my ( $branch_rules, $brw_rules ) = (
1064 value => $branchcode
1068 value => $patron->categorycode,
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 }
1078 $self->_limit_counter(
1079 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
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 ) ) {
1094 sub _limit_counter {
1095 my ( $self, $method, $target ) = @_;
1097 # Establish parameters of counts
1099 if ($method && $method eq 'annual') {
1100 $resultset = Koha::Illrequests->search({
1103 \"YEAR(placed) = YEAR(NOW())"
1106 } else { # assume 'active'
1107 # XXX: This status list is ugly. There should be a method in config
1109 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1110 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1114 return $resultset->count;
1117 =head3 requires_moderation
1119 my $status = $illRequest->requires_moderation;
1121 Return the name of the status if moderation by staff is required; or 0
1126 sub requires_moderation {
1128 my $require_moderation = {
1129 'CANCREQ' => 'CANCREQ',
1131 return $require_moderation->{$self->status};
1136 my $biblio = $request->biblio;
1138 For a given request, return the biblio associated with it,
1139 or undef if none exists
1146 return if !$self->biblio_id;
1148 return Koha::Biblios->find({
1149 biblionumber => $self->biblio_id
1155 my $stage_summary = $request->check_out;
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
1165 my ( $self, $params ) = @_;
1167 # Objects required by the template
1168 my $itemtypes = Koha::ItemTypes->search(
1170 { order_by => ['description'] }
1172 my $libraries = Koha::Libraries->search(
1174 { order_by => ['branchcode'] }
1176 my $biblio = $self->biblio;
1178 # Find all statistical patrons
1179 my $statistical_patrons = Koha::Patrons->search(
1180 { 'category_type' => 'x' },
1181 { join => { 'categorycode' => 'borrowers' } }
1184 if (!$params->{stage} || $params->{stage} eq 'init') {
1185 # Present a form to gather the required data
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
1191 method => 'check_out',
1194 itemtypes => $itemtypes,
1195 libraries => $libraries,
1196 statistical => $statistical_patrons,
1200 } elsif ($params->{stage} eq 'form') {
1201 # Validate what we've got and return with an error if we fail
1203 if (!$params->{item_type} || length $params->{item_type} == 0) {
1204 $errors->{item_type} = 1;
1206 if ($params->{inhouse} && length $params->{inhouse} > 0) {
1207 my $patron_count = Koha::Patrons->search({
1208 cardnumber => $params->{inhouse}
1210 if ($patron_count != 1) {
1211 $errors->{inhouse} = 1;
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
1220 my @items = $biblio->items->as_list;
1221 my $item_count = scalar @items;
1222 if ($item_count > 1) {
1223 $errors->{itemcount} = 1;
1226 # Failed validation, go back to the form
1229 method => 'check_out',
1233 statistical => $statistical_patrons,
1234 itemtypes => $itemtypes,
1235 libraries => $libraries,
1244 # Create an item if one doesn't already exist,
1245 # if one does, use that
1247 if ($item_count == 0) {
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
1257 my $item = Koha::Item->new($item_hash)->store;
1258 $itemnumber = $item->itemnumber;
1261 $itemnumber = $items[0]->itemnumber;
1263 # Check we have an item before going forward
1266 method => 'check_out',
1270 itemtypes => $itemtypes,
1271 libraries => $libraries,
1272 statistical => $statistical_patrons,
1273 errors => { item_creation => 1 }
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} }) :
1289 scalar $target_item->barcode
1291 if ($params->{duedate} && length $params->{duedate} > 0) {
1292 push @issue_args, dt_from_string($params->{duedate});
1294 # Check if we can check out
1295 my ( $error, $confirm, $alerts, $messages ) =
1296 C4::Circulation::CanBookBeIssued(@issue_args);
1298 # If we got anything back saying we can't check out,
1299 # return it to the template
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 };
1308 method => 'check_out',
1312 itemtypes => $itemtypes,
1313 libraries => $libraries,
1314 statistical => $statistical_patrons,
1317 check_out_errors => $problems
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);
1328 # Update the request status
1329 $self->status('CHK')->store;
1331 method => 'check_out',
1332 stage => 'done_check_out',
1341 method => 'check_out',
1345 itemtypes => $itemtypes,
1346 libraries => $libraries,
1347 errors => { item_check_out => 1 }
1355 =head3 generic_confirm
1357 my $stage_summary = $illRequest->generic_confirm;
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.
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'
1376 my $partners = Koha::Patrons->search({
1377 categorycode => $self->_config->partner_code
1383 method => 'generic_confirm',
1387 subject => $letter->{title},
1388 body => $letter->{content}
1390 partners => $partners,
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
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.")
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);
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.")
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'
1423 $letter->{title} = $params->{subject};
1424 $letter->{content} = $params->{body};
1428 # Keep track of who received this notice
1430 # Iterate our array of recipient patron objects
1431 foreach my $patron(@to_patrons) {
1432 # Create the params we pass to the notice
1435 borrowernumber => $patron->borrowernumber,
1436 message_transport_type => 'email',
1437 to_address => $patron->email,
1438 from_address => $from,
1439 reply_address => $replyto
1441 my $result = C4::Letters::EnqueueLetter($params);
1443 push @queued, $patron->email;
1447 # If all notices were queued successfully,
1449 if (scalar @queued == scalar @to_patrons) {
1450 $self->status("GENREQ")->store;
1451 $self->_backend_capability(
1452 'set_requested_partners',
1455 to => join("; ", @queued)
1462 method => 'generic_confirm',
1471 status => 'email_failed',
1472 message => 'Email queueing failed',
1473 method => 'generic_confirm',
1477 die "Unknown stage, should not have happened."
1481 =head3 send_patron_notice
1483 my $result = $request->send_patron_notice($notice_code);
1485 Send a specified notice regarding this request to a patron
1489 sub send_patron_notice {
1490 my ( $self, $notice_code, $additional_text ) = @_;
1492 # We need a notice code
1493 if (!$notice_code) {
1495 error => 'notice_no_type'
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'
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}
1511 my @transports = keys %{ $borrower_preferences->{transports} };
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;
1519 # Send the notice to the patron via the chosen transport methods
1520 # and record the results
1523 for my $transport (@transports) {
1524 my $letter = $self->get_notice({
1525 notice_code => $notice_code,
1526 transport => $transport,
1527 additional_text => $additional_text
1530 my $result = C4::Letters::EnqueueLetter({
1532 borrowernumber => $self->borrowernumber,
1533 message_transport_type => $transport,
1534 from_address => $from_address,
1535 reply_address => $reply_address
1538 push @success, $transport;
1540 push @fail, $transport;
1543 push @fail, $transport;
1546 if (scalar @success > 0) {
1547 my $logger = Koha::Illrequest::Logger->new;
1548 $logger->log_patron_notice({
1550 notice_code => $notice_code
1555 success => \@success,
1561 =head3 send_staff_notice
1563 my $result = $request->send_staff_notice($notice_code);
1565 Send a specified notice regarding this request to staff
1569 sub send_staff_notice {
1570 my ( $self, $notice_code ) = @_;
1572 # We need a notice code
1573 if (!$notice_code) {
1575 error => 'notice_no_type'
1579 # Get the staff notices that have been assigned for sending in
1581 my $staff_to_send = C4::Context->preference('ILLSendStaffNotices') // q{};
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/) {
1586 error => 'notice_not_enabled'
1590 my $letter = $self->get_notice({
1591 notice_code => $notice_code,
1592 transport => 'email'
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;
1602 borrowernumber => $self->borrowernumber,
1603 message_transport_type => 'email',
1604 from_address => $from_address
1608 $params->{to_address} = $to_address;
1611 error => 'notice_no_create'
1616 C4::Letters::EnqueueLetter($params)
1617 or warn "can't enqueue letter $letter";
1619 success => 'notice_queued'
1623 error => 'notice_no_create'
1630 my $notice = $request->get_notice($params);
1632 Return a compiled notice hashref for the passed notice code
1638 my ( $self, $params ) = @_;
1640 my $title = $self->illrequestattributes->find(
1643 my $author = $self->illrequestattributes->find(
1644 { type => 'author' }
1646 my $metahash = $self->metadata;
1648 foreach my $key (sort { lc $a cmp lc $b } keys %{$metahash}) {
1649 my $value = $metahash->{$key};
1650 push @metaarray, "- $key: $value" if $value;
1652 my $metastring = join("\n", @metaarray);
1653 my $letter = C4::Letters::GetPreparedLetter(
1655 letter_code => $params->{notice_code},
1656 branchcode => $self->branchcode,
1657 message_transport_type => $params->{transport},
1658 lang => $self->patron->lang,
1660 illrequests => $self->illrequest_id,
1661 borrowers => $self->borrowernumber,
1662 biblio => $self->biblio_id,
1663 branches => $self->branchcode,
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}
1677 =head3 attach_processors
1679 Receive a Koha::Illrequest::SupplierUpdate and attach
1680 any processors we have for it
1684 sub attach_processors {
1685 my ( $self, $update ) = @_;
1687 foreach my $processor(@{$self->{processors}}) {
1689 $processor->{target_source_type} eq $update->{source_type} &&
1690 $processor->{target_source_name} eq $update->{source_name}
1692 $update->attach_processor($processor);
1697 =head3 append_to_note
1699 append_to_note("Some text");
1701 Append some text to the staff note
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;
1714 my $prefix = $record->id_prefix;
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
1724 my $prefix = $self->getPrefix( {
1725 branch => $self->branchcode,
1727 $prefix .= "-" if ( $prefix );
1733 my $params = $illRequest->_censor($params);
1735 Return $params, modified to reflect our censorship requirements.
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;
1753 Overloaded I<store> method that, in addition to performing the 'store',
1754 possibly records the fact that something happened
1759 my ( $self, $attrs ) = @_;
1761 my %updated_columns = $self->_result->get_dirty_columns;
1764 if( defined $updated_columns{'borrowernumber'} and
1765 Koha::Patrons->find( $updated_columns{'borrowernumber'} ) )
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,
1775 my $ret = $self->SUPER::store;
1777 if ( scalar @holds ) {
1778 # move holds to the changed borrowernumber
1779 foreach my $hold ( @holds ) {
1780 $hold->borrowernumber( $updated_columns{'borrowernumber'} )->store;
1784 $attrs->{log_origin} = 'core';
1786 if ($ret && defined $attrs) {
1787 my $logger = Koha::Illrequest::Logger->new;
1788 $logger->log_maybe({
1797 =head3 requested_partners
1799 my $partners_string = $illRequest->requested_partners;
1801 Return the string representing the email addresses of the partners to
1802 whom a request has been sent
1806 sub requested_partners {
1808 return $self->_backend_capability(
1809 'get_requested_partners',
1810 { request => $self }
1816 $json = $illrequest->TO_JSON
1818 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1819 into the unblessed representation of the object.
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
1828 my ( $self, $embed ) = @_;
1830 my $object = $self->SUPER::TO_JSON();
1835 =head2 Internal methods
1842 return 'Illrequest';
1847 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1848 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>