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 );
27 use Mojo::Util qw(deprecated);
29 use Koha::Cache::Memory::Lite;
31 use Koha::DateUtils qw( dt_from_string );
32 use Koha::Exceptions::Ill;
33 use Koha::ILL::Comments;
34 use Koha::Illrequestattributes;
35 use Koha::AuthorisedValue;
36 use Koha::Illrequest::Logger;
38 use Koha::ILL::Batches;
39 use Koha::AuthorisedValues;
44 use Koha::AdditionalContents;
46 use C4::Circulation qw( CanBookBeIssued AddIssue );
48 use base qw(Koha::Object);
52 Koha::Illrequest - Koha Illrequest Object class
56 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
57 of related Illrequestattributes.
59 The former encapsulates the basic necessary information that any ILL requires
60 to be usable in Koha. The latter is a set of additional properties used by
63 The former subsumes the legacy "Status" object. The latter remains
64 encapsulated in the "Record" object.
68 - Anything invoking the ->status method; annotated with:
69 + # Old use of ->status !
73 =head2 Backend API Response Principles
75 All methods should return a hashref in the following format:
81 This should be set to 1 if an error was encountered.
85 The status should be a string from the list of statuses detailed below.
89 The message is a free text field that can be passed on to the end user.
93 The value returned by the method.
97 =head2 Interface Status Messages
101 =item * branch_address_incomplete
103 An interface request has determined branch address details are incomplete.
105 =item * cancel_success
107 The interface's cancel_request method was successful in cancelling the
108 Illrequest using the API.
112 The interface's cancel_request method failed to cancel the Illrequest using
117 The interface's request method returned saying that the desired item is not
118 available for request.
124 =head3 init_processors
126 $request->init_processors()
128 Initialises an empty processors arrayref
132 sub init_processors {
135 $self->{processors} = [];
138 =head3 push_processor
140 $request->push_processors(sub { ...something... });
142 Pushes a passed processor function into our processors arrayref
147 my ( $self, $processor ) = @_;
148 push @{$self->{processors}}, $processor;
153 my $ill_batch = $request->ill_batch;
155 Returns the I<Koha::ILL::Batch> associated with the request
162 my $ill_batch = $self->_result->ill_batch;
163 return unless $ill_batch;
164 return Koha::ILL::Batch->_new_from_dbic($ill_batch);
169 my $statusalias = $request->statusalias;
171 Returns a request's status alias, as a Koha::AuthorisedValue instance
172 or implicit undef. This is distinct from status_alias, which only returns
173 the value in the status_alias column, this method returns the entire
174 AuthorisedValue object
180 return unless $self->status_alias;
181 # We can't know which result is the right one if there are multiple
182 # ILL_STATUS_ALIAS authorised values with the same authorised_value column value
183 # so we just use the first
184 return Koha::AuthorisedValues->search(
186 category => 'ILL_STATUS_ALIAS',
187 authorised_value => $self->SUPER::status_alias
194 =head3 illrequestattributes
198 sub illrequestattributes {
199 deprecated 'illrequestattributes is DEPRECATED in favor of extended_attributes';
201 return Koha::Illrequestattributes->_new_from_dbic(
202 scalar $self->_result->illrequestattributes
212 return Koha::ILL::Comments->_new_from_dbic(
213 scalar $self->_result->illcomments
219 my $ill_comments = $req->comments;
221 Returns a I<Koha::ILL::Comments> resultset for the linked comments.
227 return Koha::ILL::Comments->_new_from_dbic(
228 scalar $self->_result->comments
238 my $logger = Koha::Illrequest::Logger->new;
239 return $logger->get_request_logs($self);
244 my $patron = $request->patron;
246 For a given request, return the linked I<Koha::Patron> object
247 associated with it, or undef if none exists
254 my $patron_rs = $self->_result->patron;
255 return unless $patron_rs;
256 return Koha::Patron->_new_from_dbic($patron_rs);
261 my $library = $request->library;
263 Returns the linked I<Koha::Library> object.
270 return Koha::Library->_new_from_dbic( scalar $self->_result->library );
273 =head3 extended_attributes
275 my $extended_attributes = $request->extended_attributes;
277 Returns the linked I<Koha::Illrequestattributes> resultset object.
281 sub extended_attributes {
284 my $rs = $self->_result->extended_attributes;
285 # We call search to use the filters in Koha::Illrequestattributes->search
286 return Koha::Illrequestattributes->_new_from_dbic($rs)->search;
291 $Illrequest->status_alias(143);
293 Overloaded getter/setter for status_alias,
294 that only returns authorised values from the
295 correct category and records the fact that the status has changed
300 my ($self, $new_status_alias) = @_;
302 my $current_status_alias = $self->SUPER::status_alias;
304 if ($new_status_alias) {
305 # Keep a record of the previous status before we change it,
307 $self->{previous_status} = $current_status_alias ?
308 $current_status_alias :
309 scalar $self->status;
310 # This is hackery to enable us to undefine
311 # status_alias, since we need to have an overloaded
312 # status_alias method to get us around the problem described
314 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
315 # We need a way of accepting implied undef, so we can nullify
316 # the status_alias column, when called from $self->status
317 my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
318 my $ret = $self->SUPER::status_alias($val);
319 my $val_to_log = $val ? $new_status_alias : scalar $self->status;
321 my $logger = Koha::Illrequest::Logger->new;
322 $logger->log_status_change({
327 delete $self->{previous_status};
331 # We can't know which result is the right one if there are multiple
332 # ILL_STATUS_ALIAS authorised values with the same authorised_value column value
333 # so we just use the first
334 my $alias = Koha::AuthorisedValues->search(
336 category => 'ILL_STATUS_ALIAS',
337 authorised_value => $self->SUPER::status_alias
344 return $alias->authorised_value;
352 $Illrequest->status('CANREQ');
354 Overloaded getter/setter for request status,
355 also nullifies status_alias and records the fact that the status has changed
356 and sends a notice if appropriate
361 my ( $self, $new_status) = @_;
363 my $current_status = $self->SUPER::status;
364 my $current_status_alias = $self->SUPER::status_alias;
367 # Keep a record of the previous status before we change it,
369 $self->{previous_status} = $current_status_alias ?
370 $current_status_alias :
372 my $ret = $self->SUPER::status($new_status)->store;
373 if ($current_status_alias) {
374 # This is hackery to enable us to undefine
375 # status_alias, since we need to have an overloaded
376 # status_alias method to get us around the problem described
378 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
379 # We need a way of passing implied undef to nullify status_alias
380 # so we pass -1, which is special cased in the overloaded setter
381 $self->status_alias("-1");
383 my $logger = Koha::Illrequest::Logger->new;
384 $logger->log_status_change({
389 delete $self->{previous_status};
390 # If status has changed to cancellation requested, send a notice
391 if ($new_status eq 'CANCREQ') {
392 $self->send_staff_notice('ILL_REQUEST_CANCEL');
396 return $current_status;
400 =head3 get_backend_plugin
402 my $backend_plugin = $self->get_backend_plugin($backend_name);
404 Returns the installed I<Koha::Plugin> corresponding to the given backend_id
408 sub get_backend_plugin {
409 my ( $self, $backend_id ) = @_;
411 my @backend_plugins = Koha::Plugins->new()->GetPlugins(
413 method => 'ill_backend',
414 metadata => { name => $backend_id },
419 return $backend_plugins[0];
424 Require "Base.pm" from the relevant ILL backend.
429 my ( $self, $backend_id ) = @_;
431 my $backend_name = $backend_id || $self->backend;
433 unless ( defined $backend_name && $backend_name ne '' ) {
434 Koha::Exceptions::Ill::InvalidBackendId->throw(
435 "An invalid backend ID was requested ('')");
438 # Find plugin implementing the backend for the request
439 my $backend_plugin = $self->get_backend_plugin($backend_name);
440 if ($backend_plugin) {
442 $self->{_my_backend} = $backend_plugin->new_ill_backend(
444 config => $self->_config,
445 logger => Koha::Illrequest::Logger->new
448 } elsif ($backend_name) {
450 # Fallback to loading through backend_dir config
451 my @raw = qw/Koha Illbackends/; # Base Path
452 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
453 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
455 $self->{_my_backend} = $backend_class->new(
457 config => $self->_config,
458 logger => Koha::Illrequest::Logger->new
469 my $backend = $abstract->_backend($new_backend);
470 my $backend = $abstract->_backend;
472 Getter/Setter for our API object.
477 my ( $self, $backend ) = @_;
478 $self->{_my_backend} = $backend if ( $backend );
479 # Dynamically load our backend object, as late as possible.
480 $self->load_backend unless ( $self->{_my_backend} );
481 return $self->{_my_backend};
484 =head3 _backend_capability
486 my $backend_capability_result = $self->_backend_capability($name, $args);
488 This is a helper method to invoke optional capabilities in the backend. If
489 the capability named by $name is not supported, return 0, else invoke it,
490 passing $args along with the invocation, and return its return value.
492 NOTE: this module suffers from a confusion in termninology:
494 in _backend_capability, the notion of capability refers to an optional feature
495 that is implemented in core, but might not be supported by a given backend.
497 in capabilities & custom_capability, capability refers to entries in the
498 status_graph (after union between backend and core).
500 The easiest way to fix this would be to fix the terminology in
501 capabilities & custom_capability and their callers.
505 sub _backend_capability {
506 my ( $self, $name, $args ) = @_;
508 # See if capability is defined in backend
510 $capability = $self->_backend->capabilities($name);
516 if ( $capability && ref($capability) eq 'CODE' ) {
517 return &{$capability}($args);
525 my $config = $abstract->_config($config);
526 my $config = $abstract->_config;
528 Getter/Setter for our config object.
533 my ( $self, $config ) = @_;
534 $self->{_my_config} = $config if ( $config );
535 # Load our config object, as late as possible.
536 unless ( $self->{_my_config} ) {
537 $self->{_my_config} = Koha::Illrequest::Config->new;
539 return $self->{_my_config};
549 if ( $self->_backend->can('metadata') ) {
551 # Old way of loading backends: Previous expected method was 'metadata'
552 return $self->_backend->metadata($self);
555 # New way of loading backends (as plugins): New expected method is 'backend_metadata'
556 return $self->_backend->backend_metadata($self);
560 =head3 _core_status_graph
562 my $core_status_graph = $illrequest->_core_status_graph;
564 Returns ILL module's default status graph. A status graph defines the list of
565 available actions at any stage in the ILL workflow. This is for instance used
566 by the perl script & template to generate the correct buttons to display to
567 the end user at any given point.
571 sub _core_status_graph {
575 prev_actions => [ ], # Actions containing buttons
576 # leading to this status
577 id => 'NEW', # ID of this status
578 name => 'New request', # UI name of this status
579 ui_method_name => 'New request', # UI name of method leading
581 method => 'create', # method to this status
582 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
583 # requests with this status
584 ui_method_icon => 'fa-plus', # UI Style class
587 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
590 ui_method_name => 'Confirm request',
592 next_actions => [ 'REQREV', 'COMP', 'CHK' ],
593 ui_method_icon => 'fa-check',
596 prev_actions => [ 'NEW', 'REQREV' ],
598 name => 'Requested from partners',
599 ui_method_name => 'Place request with partners',
600 method => 'generic_confirm',
601 next_actions => [ 'COMP', 'CHK', 'REQREV' ],
602 ui_method_icon => 'fa-paper-plane',
605 prev_actions => [ 'REQ', 'GENREQ' ],
607 name => 'Request reverted',
608 ui_method_name => 'Revert request',
610 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
611 ui_method_icon => 'fa-times',
616 name => 'Queued request',
619 next_actions => [ 'REQ', 'KILL' ],
623 prev_actions => [ 'NEW' ],
625 name => 'Cancellation requested',
628 next_actions => [ 'KILL', 'REQ' ],
632 prev_actions => [ 'REQ' ],
635 ui_method_name => 'Mark completed',
636 method => 'mark_completed',
637 next_actions => [ 'CHK' ],
638 ui_method_icon => 'fa-check',
641 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
644 ui_method_name => 'Delete request',
647 ui_method_icon => 'fa-trash',
650 prev_actions => [ 'REQ', 'GENREQ', 'COMP' ],
652 name => 'Checked out',
653 ui_method_name => 'Check out',
654 needs_prefs => [ 'CirculateILL' ],
655 needs_perms => [ 'user_circulate_circulate_remaining_permissions' ],
656 # An array of functions that all must return true
657 needs_all => [ sub { my $r = shift; return $r->biblio; } ],
658 method => 'check_out',
660 ui_method_icon => 'fa-upload',
663 prev_actions => [ 'CHK' ],
665 name => 'Returned to library',
666 ui_method_name => 'Check in',
667 method => 'check_in',
668 next_actions => [ 'COMP' ],
669 ui_method_icon => 'fa-download',
674 =head3 _status_graph_union
676 my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
678 Return a new status_graph, the result of merging $origin & new_graph. This is
679 operation is a union over the sets defied by the two graphs.
681 Each entry in $new_graph is added to $origin. We do not provide a syntax for
682 'subtraction' of entries from $origin.
684 Whilst it is not intended that this works, you can override entries in $origin
685 with entries with the same key in $new_graph. This can lead to problematic
686 behaviour when $new_graph adds an entry, which modifies a dependent entry in
687 $origin, only for the entry in $origin to be replaced later with a new entry
690 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
691 i.e. each of the graphs need to be correct at the outset of the operation.
695 sub _status_graph_union {
696 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
697 # Create new status graph with:
698 # - all core_status_graph
699 # - for-each each backend_status_graph
700 # + add to new status graph
701 # + for each core prev_action:
702 # * locate core_status
703 # * update next_actions with additional next action.
704 # + for each core next_action:
705 # * locate core_status
706 # * update prev_actions with additional prev action
708 my @core_status_ids = keys %{$core_status_graph};
709 my $status_graph = clone($core_status_graph);
711 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
712 my $backend_status = $backend_status_graph->{$backend_status_key};
713 # Add to new status graph
714 $status_graph->{$backend_status_key} = $backend_status;
715 # Update all core methods' next_actions.
716 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
717 if ( grep { $prev_action eq $_ } @core_status_ids ) {
719 @{$status_graph->{$prev_action}->{next_actions}};
720 push @next_actions, $backend_status_key
721 if (!grep(/^$backend_status_key$/, @next_actions));
722 $status_graph->{$prev_action}->{next_actions}
726 # Update all core methods' prev_actions
727 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
728 if ( grep { $next_action eq $_ } @core_status_ids ) {
730 @{$status_graph->{$next_action}->{prev_actions}};
731 push @prev_actions, $backend_status_key
732 if (!grep(/^$backend_status_key$/, @prev_actions));
733 $status_graph->{$next_action}->{prev_actions}
739 return $status_graph;
746 my $capabilities = $illrequest->capabilities;
748 Return a hashref mapping methods to operation names supported by the queried
751 Example return value:
753 { create => "Create Request", confirm => "Progress Request" }
755 NOTE: this module suffers from a confusion in termninology:
757 in _backend_capability, the notion of capability refers to an optional feature
758 that is implemented in core, but might not be supported by a given backend.
760 in capabilities & custom_capability, capability refers to entries in the
761 status_graph (after union between backend and core).
763 The easiest way to fix this would be to fix the terminology in
764 capabilities & custom_capability and their callers.
769 my ( $self, $status ) = @_;
770 # Generate up to date status_graph
771 my $status_graph = $self->_status_graph_union(
772 $self->_core_status_graph,
773 $self->_backend->status_graph({
778 # Extract available actions from graph.
779 return $status_graph->{$status} if $status;
780 # Or return entire graph.
781 return $status_graph;
784 =head3 custom_capability
786 Return the result of invoking $CANDIDATE on this request's backend with
787 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
789 NOTE: this module suffers from a confusion in termninology:
791 in _backend_capability, the notion of capability refers to an optional feature
792 that is implemented in core, but might not be supported by a given backend.
794 in capabilities & custom_capability, capability refers to entries in the
795 status_graph (after union between backend and core).
797 The easiest way to fix this would be to fix the terminology in
798 capabilities & custom_capability and their callers.
802 sub custom_capability {
803 my ( $self, $candidate, $params ) = @_;
804 foreach my $capability ( values %{$self->capabilities} ) {
805 if ( $candidate eq $capability->{method} ) {
807 $self->_backend->$candidate({
811 return $self->expand_template($response);
817 =head3 available_backends
819 Return a list of available backends.
823 sub available_backends {
824 my ( $self, $reduced ) = @_;
825 my $backends = $self->_config->available_backends($reduced);
829 =head3 available_actions
831 Return a list of available actions.
835 sub available_actions {
837 my $current_action = $self->capabilities($self->status);
838 my @available_actions = map { $self->capabilities($_) }
839 @{$current_action->{next_actions}};
840 return \@available_actions;
843 =head3 mark_completed
845 Mark a request as completed (status = COMP).
851 $self->status('COMP')->store;
852 $self->completed(dt_from_string())->store;
857 method => 'mark_completed',
863 =head2 backend_illview
865 View and manage an ILL request
869 sub backend_illview {
870 my ( $self, $params ) = @_;
872 my $response = $self->_backend_capability('illview',{
876 return $self->expand_template($response) if $response;
880 =head2 backend_migrate
882 Migrate a request from one backend to another.
886 sub backend_migrate {
887 my ( $self, $params ) = @_;
888 # Set the request's backend to be the destination backend
889 $self->load_backend($params->{backend});
890 my $response = $self->_backend_capability('migrate',{
894 return $self->expand_template($response) if $response;
898 =head2 backend_confirm
900 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
906 =item * accessurl, cost (if available).
912 sub backend_confirm {
913 my ( $self, $params ) = @_;
915 my $response = $self->_backend->confirm({
919 return $self->expand_template($response);
922 =head3 backend_update_status
926 sub backend_update_status {
927 my ( $self, $params ) = @_;
928 return $self->expand_template($self->_backend->update_status($params));
931 =head3 backend_cancel
933 my $ILLResponse = $illRequest->backend_cancel;
935 The standard interface method allowing for request cancellation.
940 my ( $self, $params ) = @_;
942 my $result = $self->_backend->cancel({
947 return $self->expand_template($result);
952 my $renew_response = $illRequest->backend_renew;
954 The standard interface method allowing for request renewal queries.
960 return $self->expand_template(
961 $self->_backend->renew({
967 =head3 backend_create
969 my $create_response = $abstractILL->backend_create($params);
971 Return an array of Record objects created by querying our backend with
974 In the context of the other ILL methods, this is a special method: we only
975 pass it $params, as it does not yet have any other data associated with it.
980 my ( $self, $params ) = @_;
982 # Establish whether we need to do a generic copyright clearance.
983 if ( $params->{opac} ) {
985 my $copyright_content = Koha::AdditionalContents->search_for_display(
987 category => 'html_customizations',
988 location => ['ILLModuleCopyrightClearance'],
989 lang => $params->{lang},
990 library_id => $params->{branchcode},
994 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
995 && $copyright_content->count )
1002 stage => 'copyrightclearance',
1005 backend => $self->_backend->name
1008 } elsif ( defined $params->{stage}
1009 && $params->{stage} eq 'copyrightclearance' ) {
1010 $params->{stage} = 'init';
1013 # First perform API action, then...
1018 my $result = $self->_backend->create($args);
1020 # ... simple case: we're not at 'commit' stage.
1021 my $stage = $result->{stage};
1022 return $self->expand_template($result)
1023 unless ( 'commit' eq $stage );
1025 # ... complex case: commit!
1027 # Do we still have space for an ILL or should we queue?
1028 my $permitted = $self->check_limits(
1029 { patron => $self->patron }, { librarycode => $self->branchcode }
1032 # Now augment our committed request.
1034 $result->{permitted} = $permitted; # Queue request?
1038 # ...Updating status!
1039 $self->status('QUEUED')->store unless ( $permitted );
1041 ## Handle Unmediated ILLs
1043 # For the unmediated workflow we only need to delegate to our backend. If
1044 # that backend supports unmediateld_ill, it will do its thing and return a
1045 # proper response. If it doesn't then _backend_capability returns 0, so
1046 # we keep the current result.
1047 if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
1048 my $unmediated_result = $self->_backend_capability(
1052 $result = $unmediated_result if $unmediated_result;
1055 return $self->expand_template($result);
1058 =head3 backend_get_update
1060 my $update = backend_get_update($request);
1062 Given a request, returns an update in a prescribed
1063 format that can then be passed to update parsers
1067 sub backend_get_update {
1068 my ( $self, $options ) = @_;
1070 my $response = $self->_backend_capability(
1071 'get_supplier_update',
1080 =head3 expand_template
1082 my $params = $abstract->expand_template($params);
1084 Return a version of $PARAMS augmented with our required template path.
1088 sub expand_template {
1089 my ( $self, $params ) = @_;
1090 my $backend = $self->_backend->name;
1091 # Generate path to file to load
1095 my $backend_plugin = $self->get_backend_plugin( $self->_backend->name );
1096 if ($backend_plugin) {
1098 # New way of loading backends: Through plugins
1099 $backend_dir = $backend_plugin->bundle_path;
1100 $backend_tmpl = $backend_dir;
1103 # Old way of loading backends: Through backend_dir config
1104 $backend_dir = $self->_config->backend_dir;
1105 $backend_tmpl = join "/", $backend_dir, $backend;
1108 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
1109 ( $params->{method}//q{} ) . ".inc";
1110 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
1111 ( $params->{method}//q{} ) . ".inc";
1113 $params->{template} = $intra_tmpl;
1114 $params->{opac_template} = $opac_tmpl;
1118 #### Abstract Imports
1122 my $limit_rules = $abstract->getLimits( {
1123 type => 'brw_cat' | 'branch',
1127 Return the ILL limit rules for the supplied combination of type / value.
1129 As the config may have no rules for this particular type / value combination,
1130 or for the default, we must define fall-back values here.
1135 my ( $self, $params ) = @_;
1136 my $limits = $self->_config->getLimitRules($params->{type});
1138 if ( defined $params->{value}
1139 && defined $limits->{$params->{value}} ) {
1140 return $limits->{$params->{value}};
1143 return $limits->{default} || { count => -1, method => 'active' };
1149 my $prefix = $abstract->getPrefix( {
1150 branch => $branch_code
1153 Return the ILL prefix as defined by our $params: either per borrower category,
1154 per branch or the default.
1159 my ( $self, $params ) = @_;
1160 my $brn_prefixes = $self->_config->getPrefixes();
1161 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
1166 my $type = $abstract->get_type();
1168 Return a string representing the material type of this request or undef
1174 my $attr = $self->illrequestattributes->find({ type => 'type'});
1176 return $attr->value;
1179 =head3 get_type_disclaimer_value
1181 my $type = $abstract->get_type_disclaimer_value();
1183 Return the value submitted in the type disclaimer workflow stage
1187 sub get_type_disclaimer_value {
1189 my $attr = $self->extended_attributes->find( { type => 'type_disclaimer_value' } );
1191 return $attr->value;
1194 =head3 get_type_disclaimer_date
1196 my $type = $abstract->type_disclaimer_date();
1198 Return the time the disclaimer was submitted in the type disclaimer workflow stage
1202 sub get_type_disclaimer_date {
1204 my $attr = $self->extended_attributes->find( { type => 'type_disclaimer_date' } );
1206 return $attr->value;
1209 #### Illrequests Imports
1213 my $ok = $illRequests->check_limits( {
1214 borrower => $borrower,
1215 branchcode => 'branchcode' | undef,
1218 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
1219 see whether we are still able to place ILLs.
1221 LimitRules are derived from koha-conf.xml:
1222 + default limit counts, and counting method
1223 + branch specific limit counts & counting method
1224 + borrower category specific limit counts & counting method
1225 + err on the side of caution: a counting fail will cause fail, even if
1226 the other counts passes.
1231 my ( $self, $params ) = @_;
1232 my $patron = $params->{patron};
1233 my $branchcode = $params->{librarycode} || $patron->branchcode;
1235 # Establish maximum number of allowed requests
1236 my ( $branch_rules, $brw_rules ) = (
1239 value => $branchcode
1243 value => $patron->categorycode,
1246 my ( $branch_limit, $brw_limit )
1247 = ( $branch_rules->{count}, $brw_rules->{count} );
1248 # Establish currently existing requests
1249 my ( $branch_count, $brw_count ) = (
1250 $self->_limit_counter(
1251 $branch_rules->{method}, { branchcode => $branchcode }
1253 $self->_limit_counter(
1254 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
1258 # Compare and return
1259 # A limit of -1 means no limit exists.
1260 # We return blocked if either branch limit or brw limit is reached.
1261 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
1262 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
1269 sub _limit_counter {
1270 my ( $self, $method, $target ) = @_;
1272 # Establish parameters of counts
1274 if ($method && $method eq 'annual') {
1275 $resultset = Koha::Illrequests->search({
1278 \"YEAR(placed) = YEAR(NOW())"
1281 } else { # assume 'active'
1282 # XXX: This status list is ugly. There should be a method in config
1284 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1285 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1289 return $resultset->count;
1292 =head3 requires_moderation
1294 my $status = $illRequest->requires_moderation;
1296 Return the name of the status if moderation by staff is required; or 0
1301 sub requires_moderation {
1303 my $require_moderation = {
1304 'CANCREQ' => 'CANCREQ',
1306 return $require_moderation->{$self->status};
1311 my $biblio = $request->biblio;
1313 For a given request, return the biblio associated with it,
1314 or undef if none exists
1320 my $biblio_rs = $self->_result->biblio;
1321 return unless $biblio_rs;
1322 return Koha::Biblio->_new_from_dbic($biblio_rs);
1327 my $stage_summary = $request->check_out;
1329 Handle the check_out method. The first stage involves gathering the required
1330 data from the user via a form, the second stage creates an item and tries to
1331 issue it to the patron. If successful, it notifies the patron, then it
1332 returns a summary of how things went
1337 my ( $self, $params ) = @_;
1339 # Objects required by the template
1340 my $itemtypes = Koha::ItemTypes->search(
1342 { order_by => ['description'] }
1344 my $libraries = Koha::Libraries->search(
1346 { order_by => ['branchcode'] }
1348 my $biblio = $self->biblio;
1350 # Find all statistical patrons
1351 my $statistical_patrons = Koha::Patrons->search(
1352 { 'category_type' => 'x' },
1353 { join => { 'categorycode' => 'borrowers' } }
1356 if (!$params->{stage} || $params->{stage} eq 'init') {
1357 # Present a form to gather the required data
1359 # We may be viewing this page having previously tried to issue
1360 # the item (in which case, we may already have created an item)
1361 # so we pass the biblio for this request
1363 method => 'check_out',
1366 itemtypes => $itemtypes,
1367 libraries => $libraries,
1368 statistical => $statistical_patrons,
1372 } elsif ($params->{stage} eq 'form') {
1373 # Validate what we've got and return with an error if we fail
1375 if (!$params->{item_type} || length $params->{item_type} == 0) {
1376 $errors->{item_type} = 1;
1378 if ($params->{inhouse} && length $params->{inhouse} > 0) {
1379 my $patron_count = Koha::Patrons->search({
1380 cardnumber => $params->{inhouse}
1382 if ($patron_count != 1) {
1383 $errors->{inhouse} = 1;
1387 # Check we don't have more than one item for this bib,
1388 # if we do, something very odd is going on
1389 # Having 1 is OK, it means we're likely trying to issue
1390 # following a previously failed attempt, the item exists
1392 my @items = $biblio->items->as_list;
1393 my $item_count = scalar @items;
1394 if ($item_count > 1) {
1395 $errors->{itemcount} = 1;
1398 # Failed validation, go back to the form
1401 method => 'check_out',
1405 statistical => $statistical_patrons,
1406 itemtypes => $itemtypes,
1407 libraries => $libraries,
1416 # Create an item if one doesn't already exist,
1417 # if one does, use that
1419 if ($item_count == 0) {
1421 biblionumber => $self->biblio_id,
1422 homebranch => $params->{branchcode},
1423 holdingbranch => $params->{branchcode},
1424 location => $params->{branchcode},
1425 itype => $params->{item_type},
1426 barcode => 'ILL-' . $self->illrequest_id
1429 my $item = Koha::Item->new($item_hash)->store;
1430 $itemnumber = $item->itemnumber;
1433 $itemnumber = $items[0]->itemnumber;
1435 # Check we have an item before going forward
1438 method => 'check_out',
1442 itemtypes => $itemtypes,
1443 libraries => $libraries,
1444 statistical => $statistical_patrons,
1445 errors => { item_creation => 1 }
1452 # Gather what we need
1453 my $target_item = Koha::Items->find( $itemnumber );
1454 # Determine who we're issuing to
1455 my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
1456 Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
1461 scalar $target_item->barcode
1463 if ($params->{duedate} && length $params->{duedate} > 0) {
1464 push @issue_args, dt_from_string($params->{duedate});
1466 # Check if we can check out
1467 my ( $error, $confirm, $alerts, $messages ) =
1468 C4::Circulation::CanBookBeIssued(@issue_args);
1470 # If we got anything back saying we can't check out,
1471 # return it to the template
1473 if ( $error && %{$error} ) { $problems->{error} = $error };
1474 if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
1475 if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
1476 if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
1480 method => 'check_out',
1484 itemtypes => $itemtypes,
1485 libraries => $libraries,
1486 statistical => $statistical_patrons,
1489 check_out_errors => $problems
1494 # We can allegedly check out, so make it so
1495 my $issue = C4::Circulation::AddIssue(@issue_args);
1498 # Update the request status
1499 $self->status('CHK')->store;
1501 method => 'check_out',
1502 stage => 'done_check_out',
1511 method => 'check_out',
1515 itemtypes => $itemtypes,
1516 libraries => $libraries,
1517 errors => { item_check_out => 1 }
1525 =head3 generic_confirm
1527 my $stage_summary = $illRequest->generic_confirm;
1529 Handle the generic_confirm extended method. The first stage involves creating
1530 a template email for the end user to edit in the browser. The second stage
1531 attempts to submit the email.
1535 sub generic_confirm {
1536 my ( $self, $params ) = @_;
1537 my $branch = Koha::Libraries->find($params->{current_branchcode})
1538 || die "Invalid current branchcode. Are you logged in as the database user?";
1539 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
1540 # Get the message body from the notice definition
1541 my $letter = $self->get_notice({
1542 notice_code => 'ILL_PARTNER_REQ',
1543 transport => 'email'
1546 my $partners = Koha::Patrons->search({
1547 categorycode => $self->_config->partner_code
1553 method => 'generic_confirm',
1557 subject => $letter->{title},
1558 body => $letter->{content}
1560 partners => $partners,
1564 } elsif ( 'draft' eq $params->{stage} ) {
1565 # Create the to header
1566 my $to = $params->{partners};
1567 if ( defined $to ) {
1568 $to =~ s/^\x00//; # Strip leading NULLs
1570 Koha::Exceptions::Ill::NoTargetEmail->throw(
1571 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
1574 # Take the null delimited string that we receive and create
1575 # an array of associated patron objects
1576 my @to_patrons = map {
1577 Koha::Patrons->find({ borrowernumber => $_ })
1578 } split(/\x00/, $to);
1580 # Create the from, replyto and sender headers
1581 my $from = $branch->from_email_address;
1582 my $replyto = $branch->inbound_ill_address;
1583 Koha::Exceptions::Ill::NoLibraryEmail->throw(
1584 "Your library has no usable email address. Please set it.")
1587 # So we get a notice hashref, then substitute the possibly
1588 # modified title and body from the draft stage
1589 my $letter = $self->get_notice({
1590 notice_code => 'ILL_PARTNER_REQ',
1591 transport => 'email'
1593 $letter->{title} = $params->{subject};
1594 $letter->{content} = $params->{body};
1598 # Keep track of who received this notice
1600 # Iterate our array of recipient patron objects
1601 foreach my $patron(@to_patrons) {
1602 # Create the params we pass to the notice
1605 borrowernumber => $patron->borrowernumber,
1606 message_transport_type => 'email',
1607 to_address => $patron->email,
1608 from_address => $from,
1609 reply_address => $replyto
1611 my $result = C4::Letters::EnqueueLetter($params);
1613 push @queued, $patron->email;
1617 # If all notices were queued successfully,
1619 if (scalar @queued == scalar @to_patrons) {
1620 $self->status("GENREQ")->store;
1621 $self->_backend_capability(
1622 'set_requested_partners',
1625 to => join("; ", @queued)
1632 method => 'generic_confirm',
1641 status => 'email_failed',
1642 message => 'Email queueing failed',
1643 method => 'generic_confirm',
1647 die "Unknown stage, should not have happened."
1651 =head3 send_patron_notice
1653 my $result = $request->send_patron_notice($notice_code);
1655 Send a specified notice regarding this request to a patron
1659 sub send_patron_notice {
1660 my ( $self, $notice_code, $additional_text ) = @_;
1662 # We need a notice code
1663 if (!$notice_code) {
1665 error => 'notice_no_type'
1669 # Map from the notice code to the messaging preference
1670 my %message_name = (
1671 ILL_PICKUP_READY => 'Ill_ready',
1672 ILL_REQUEST_UNAVAIL => 'Ill_unavailable',
1673 ILL_REQUEST_UPDATE => 'Ill_update'
1676 # Get the patron's messaging preferences
1677 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
1678 borrowernumber => $self->borrowernumber,
1679 message_name => $message_name{$notice_code}
1681 my @transports = keys %{ $borrower_preferences->{transports} };
1683 # Notice should come from the library where the request was placed,
1684 # not the patrons home library
1685 my $branch = Koha::Libraries->find($self->branchcode);
1686 my $from_address = $branch->from_email_address;
1687 my $reply_address = $branch->inbound_ill_address;
1689 # Send the notice to the patron via the chosen transport methods
1690 # and record the results
1693 for my $transport (@transports) {
1694 my $letter = $self->get_notice({
1695 notice_code => $notice_code,
1696 transport => $transport,
1697 additional_text => $additional_text
1700 my $result = C4::Letters::EnqueueLetter({
1702 borrowernumber => $self->borrowernumber,
1703 message_transport_type => $transport,
1704 from_address => $from_address,
1705 reply_address => $reply_address
1708 push @success, $transport;
1710 push @fail, $transport;
1713 push @fail, $transport;
1716 if (scalar @success > 0) {
1717 my $logger = Koha::Illrequest::Logger->new;
1718 $logger->log_patron_notice({
1720 notice_code => $notice_code
1725 success => \@success,
1731 =head3 send_staff_notice
1733 my $result = $request->send_staff_notice($notice_code);
1735 Send a specified notice regarding this request to staff
1739 sub send_staff_notice {
1740 my ( $self, $notice_code ) = @_;
1742 # We need a notice code
1743 if (!$notice_code) {
1745 error => 'notice_no_type'
1749 # Get the staff notices that have been assigned for sending in
1751 my $staff_to_send = C4::Context->preference('ILLSendStaffNotices') // q{};
1753 # If it hasn't been enabled in the syspref, we don't want to send it
1754 if ($staff_to_send !~ /\b$notice_code\b/) {
1756 error => 'notice_not_enabled'
1760 my $letter = $self->get_notice({
1761 notice_code => $notice_code,
1762 transport => 'email'
1765 # Try and get an address to which to send staff notices
1766 my $branch = Koha::Libraries->find($self->branchcode);
1767 my $to_address = $branch->inbound_ill_address;
1768 my $from_address = $branch->inbound_ill_address;
1772 borrowernumber => $self->borrowernumber,
1773 message_transport_type => 'email',
1774 from_address => $from_address
1778 $params->{to_address} = $to_address;
1781 error => 'notice_no_create'
1786 C4::Letters::EnqueueLetter($params)
1787 or warn "can't enqueue letter $letter";
1789 success => 'notice_queued'
1793 error => 'notice_no_create'
1800 my $notice = $request->get_notice($params);
1802 Return a compiled notice hashref for the passed notice code
1808 my ( $self, $params ) = @_;
1810 my $title = $self->illrequestattributes->find(
1813 my $author = $self->illrequestattributes->find(
1814 { type => 'author' }
1816 my $metahash = $self->metadata;
1818 foreach my $key (sort { lc $a cmp lc $b } keys %{$metahash}) {
1819 my $value = $metahash->{$key};
1820 push @metaarray, "- $key: $value" if $value;
1822 my $metastring = join("\n", @metaarray);
1824 my $illrequestattributes = {
1825 map { $_->type => $_->value } $self->illrequestattributes->as_list
1828 my $letter = C4::Letters::GetPreparedLetter(
1830 letter_code => $params->{notice_code},
1831 branchcode => $self->branchcode,
1832 message_transport_type => $params->{transport},
1833 lang => $self->patron->lang,
1835 illrequests => $self->illrequest_id,
1836 borrowers => $self->borrowernumber,
1837 biblio => $self->biblio_id,
1838 branches => $self->branchcode,
1841 ill_bib_title => $title ? $title->value : '',
1842 ill_bib_author => $author ? $author->value : '',
1843 ill_full_metadata => $metastring,
1844 additional_text => $params->{additional_text},
1845 illrequestattributes => $illrequestattributes,
1853 =head3 attach_processors
1855 Receive a Koha::Illrequest::SupplierUpdate and attach
1856 any processors we have for it
1860 sub attach_processors {
1861 my ( $self, $update ) = @_;
1863 foreach my $processor(@{$self->{processors}}) {
1865 $processor->{target_source_type} eq $update->{source_type} &&
1866 $processor->{target_source_name} eq $update->{source_name}
1868 $update->attach_processor($processor);
1873 =head3 append_to_note
1875 append_to_note("Some text");
1877 Append some text to the staff note
1881 sub append_to_note {
1882 my ($self, $text) = @_;
1883 my $current = $self->notesstaff;
1884 $text = ($current && length $current > 0) ? "$current\n\n$text" : $text;
1885 $self->notesstaff($text)->store;
1890 my $prefix = $record->id_prefix;
1892 Return the prefix appropriate for the current Illrequest as derived from the
1893 borrower and branch associated with this request's Status, and the config
1900 my $prefix = $self->getPrefix( {
1901 branch => $self->branchcode,
1903 $prefix .= "-" if ( $prefix );
1909 my $params = $illRequest->_censor($params);
1911 Return $params, modified to reflect our censorship requirements.
1916 my ( $self, $params ) = @_;
1917 my $censorship = $self->_config->censorship;
1918 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1919 if ( $params->{opac} );
1920 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1929 Overloaded I<store> method that, in addition to performing the 'store',
1930 possibly records the fact that something happened
1935 my ( $self, $attrs ) = @_;
1937 my %updated_columns = $self->_result->get_dirty_columns;
1940 if( $self->in_storage and defined $updated_columns{'borrowernumber'} and
1941 Koha::Patrons->find( $updated_columns{'borrowernumber'} ) )
1943 # borrowernumber has changed
1944 my $old_illreq = $self->get_from_storage;
1945 @holds = Koha::Holds->search( {
1946 borrowernumber => $old_illreq->borrowernumber,
1947 biblionumber => $self->biblio_id,
1948 } )->as_list if $old_illreq;
1951 my $ret = $self->SUPER::store;
1953 if ( scalar @holds ) {
1954 # move holds to the changed borrowernumber
1955 foreach my $hold ( @holds ) {
1956 $hold->borrowernumber( $updated_columns{'borrowernumber'} )->store;
1960 $attrs->{log_origin} = 'core';
1962 if ($ret && defined $attrs) {
1963 my $logger = Koha::Illrequest::Logger->new;
1964 $logger->log_maybe({
1973 =head3 requested_partners
1975 my $partners_string = $illRequest->requested_partners;
1977 Return the string representing the email addresses of the partners to
1978 whom a request has been sent
1982 sub requested_partners {
1984 return $self->_backend_capability(
1985 'get_requested_partners',
1986 { request => $self }
1992 $json = $illrequest->TO_JSON
1994 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1995 into the unblessed representation of the object.
1997 TODO: This method does nothing and is not called anywhere. However, bug 74325
1998 touches it, so keeping this for now until both this and bug 74325 are merged,
1999 at which point we can sort it out and remove it completely
2004 my ( $self, $embed ) = @_;
2006 my $object = $self->SUPER::TO_JSON();
2011 =head2 Internal methods
2013 =head3 to_api_mapping
2017 sub to_api_mapping {
2019 accessurl => 'access_url',
2020 batch_id => 'ill_batch_id',
2021 backend => 'ill_backend_id',
2022 borrowernumber => 'patron_id',
2023 branchcode => 'library_id',
2024 completed => 'completed_date',
2025 deleted_biblio_id => undef,
2026 illrequest_id => 'ill_request_id',
2027 notesopac => 'opac_notes',
2028 notesstaff => 'staff_notes',
2029 orderid => 'ill_backend_request_id',
2030 placed => 'requested_date',
2031 price_paid => 'paid_price',
2032 replied => 'replied_date',
2033 status_alias => 'status_av',
2034 updated => 'timestamp',
2040 my $strings = $self->string_map({ [ public => 0|1 ] });
2042 Returns a map of column name to string representations. Extra information
2043 is returned depending on the column characteristics as shown below.
2045 Accepts a param hashref where the I<public> key denotes whether we want the public
2046 or staff client strings.
2052 backend => 'backendName',
2053 str => 'Status description',
2054 type => 'ill_status',
2057 category => 'ILL_STATUS_ALIAS,
2058 str => $value, # the AV description, depending on $params->{public}
2066 my ( $self, $params ) = @_;
2068 my $cache = Koha::Cache::Memory::Lite->get_instance();
2069 my $cache_key = 'ill:status_graph:' . $self->backend;
2071 my $status_graph_union = $cache->get($cache_key);
2072 unless ($status_graph_union) {
2073 $status_graph_union = $self->capabilities;
2074 $cache->set( $cache_key, $status_graph_union );
2078 ( exists $status_graph_union->{ $self->status } && defined $status_graph_union->{ $self->status }->{name} )
2079 ? $status_graph_union->{ $self->status }->{name}
2083 ( exists $status_graph_union->{ $self->status } && defined $status_graph_union->{ $self->status }->{id} )
2084 ? $status_graph_union->{ $self->status }->{id}
2089 backend => $self->backend, # the backend identifier
2090 str => $status_string, # the status description, taken from the status graph
2091 code => $status_code, # the status id, taken from the status graph
2092 type => 'ill_status', # fixed type
2096 my $status_alias = $self->statusalias;
2097 if ($status_alias) {
2098 $strings->{"status_alias"} = {
2099 category => 'ILL_STATUS_ALIAS',
2100 str => $params->{public} ? $status_alias->lib_opac : $status_alias->lib,
2101 code => $status_alias->authorised_value,
2109 =head3 get_op_param_deprecation
2111 my $op = Koha::Illrequest->check_url_param_deprecation($params);
2113 Issues a deprecation message for the given parameters, if needed.
2114 Returns the appropriate operation based on the interface type.
2120 The interface this is running on: 'opac' or 'intranet'
2134 sub get_op_param_deprecation {
2135 my ( $self, $interface, $params ) = @_;
2137 my $deprecation_message;
2139 $deprecation_message = '"method" form param is DEPRECATED in favor of "op". ' if ( exists( $params->{'method'} ) );
2141 if ( exists( $params->{'op'} ) && $params->{'op'} eq 'create'
2142 || exists( $params->{'method'} ) && $params->{'method'} eq 'create' )
2144 $deprecation_message .= '"create" op is DEPRECATED in favor of "cud-create" or "add_form".';
2147 $deprecation_message .= ' Used by ' . $params->{'backend'} . ' backend.'
2148 if $params->{'backend'} && $deprecation_message;
2150 deprecated $deprecation_message if $deprecation_message;
2153 if ( $interface eq 'opac' ) {
2154 $op = $params->{'op'} // $params->{'method'} // 'list';
2155 $op = 'cud-create' if $op eq 'create' || $op eq 'add_form';
2156 } elsif ( $interface eq 'intranet' ) {
2157 $op = $params->{op} // $params->{method} // 'illlist';
2158 $op = 'cud-create' if $op eq 'create';
2159 $op = 'cud-edit_action' if $op eq 'edit_action';
2160 $op = 'cud-generic_confirm' if $op eq 'generic_confirm';
2161 $op = 'cud-cancel' if $op eq 'cancel';
2162 $op = 'cud-delete' if $op eq 'delete';
2167 =head3 get_staff_table_actions
2169 my $ill_table_actions = $self->get_staff_table_actions;
2171 Returns the table actions available in the Staff ILL list table
2172 A total join of core static actions with custom actions provided by
2173 installed plugins that implement the ill_table_actions hook
2177 sub get_staff_table_actions {
2178 my ( $self, $params ) = @_;
2180 my $ill_table_actions = [
2182 button_class => 'btn btn-default btn-sm',
2183 button_link => '/cgi-bin/koha/ill/ill-requests.pl?op=illview&illrequest_id=',
2184 append_column_data_to_link => 1,
2185 button_link_translatable_text => 'ill_manage',
2189 Koha::Plugins->call( 'ill_table_actions', \$ill_table_actions );
2191 return $ill_table_actions;
2199 return 'Illrequest';
2204 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
2205 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>