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::Illcomments;
34 use Koha::Illrequestattributes;
35 use Koha::AuthorisedValue;
36 use Koha::Illrequest::Logger;
38 use Koha::AuthorisedValues;
44 use C4::Circulation qw( CanBookBeIssued AddIssue );
46 use base qw(Koha::Object);
50 Koha::Illrequest - Koha Illrequest Object class
54 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
55 of related Illrequestattributes.
57 The former encapsulates the basic necessary information that any ILL requires
58 to be usable in Koha. The latter is a set of additional properties used by
61 The former subsumes the legacy "Status" object. The latter remains
62 encapsulated in the "Record" object.
66 - Anything invoking the ->status method; annotated with:
67 + # Old use of ->status !
71 =head2 Backend API Response Principles
73 All methods should return a hashref in the following format:
79 This should be set to 1 if an error was encountered.
83 The status should be a string from the list of statuses detailed below.
87 The message is a free text field that can be passed on to the end user.
91 The value returned by the method.
95 =head2 Interface Status Messages
99 =item * branch_address_incomplete
101 An interface request has determined branch address details are incomplete.
103 =item * cancel_success
105 The interface's cancel_request method was successful in cancelling the
106 Illrequest using the API.
110 The interface's cancel_request method failed to cancel the Illrequest using
115 The interface's request method returned saying that the desired item is not
116 available for request.
122 =head3 init_processors
124 $request->init_processors()
126 Initialises an empty processors arrayref
130 sub init_processors {
133 $self->{processors} = [];
136 =head3 push_processor
138 $request->push_processors(sub { ...something... });
140 Pushes a passed processor function into our processors arrayref
145 my ( $self, $processor ) = @_;
146 push @{$self->{processors}}, $processor;
151 my $statusalias = $request->statusalias;
153 Returns a request's status alias, as a Koha::AuthorisedValue instance
154 or implicit undef. This is distinct from status_alias, which only returns
155 the value in the status_alias column, this method returns the entire
156 AuthorisedValue object
162 return unless $self->status_alias;
163 # We can't know which result is the right one if there are multiple
164 # ILL_STATUS_ALIAS authorised values with the same authorised_value column value
165 # so we just use the first
166 return Koha::AuthorisedValues->search(
168 category => 'ILL_STATUS_ALIAS',
169 authorised_value => $self->SUPER::status_alias
176 =head3 illrequestattributes
180 sub illrequestattributes {
181 deprecated 'illrequestattributes is DEPRECATED in favor of extended_attributes';
183 return Koha::Illrequestattributes->_new_from_dbic(
184 scalar $self->_result->illrequestattributes
194 return Koha::Illcomments->_new_from_dbic(
195 scalar $self->_result->illcomments
201 my $ill_comments = $req->comments;
203 Returns a I<Koha::Illcomments> resultset for the linked comments.
209 return Koha::Illcomments->_new_from_dbic(
210 scalar $self->_result->comments
220 my $logger = Koha::Illrequest::Logger->new;
221 return $logger->get_request_logs($self);
226 my $patron = $request->patron;
228 Returns the linked I<Koha::Patron> object.
235 return Koha::Patron->_new_from_dbic( scalar $self->_result->patron );
240 my $library = $request->library;
242 Returns the linked I<Koha::Library> object.
249 return Koha::Library->_new_from_dbic( scalar $self->_result->library );
252 =head3 extended_attributes
254 my $extended_attributes = $request->extended_attributes;
256 Returns the linked I<Koha::Illrequestattributes> resultset object.
260 sub extended_attributes {
263 my $rs = $self->_result->extended_attributes;
264 # We call search to use the filters in Koha::Illrequestattributes->search
265 return Koha::Illrequestattributes->_new_from_dbic($rs)->search;
270 $Illrequest->status_alias(143);
272 Overloaded getter/setter for status_alias,
273 that only returns authorised values from the
274 correct category and records the fact that the status has changed
279 my ($self, $new_status_alias) = @_;
281 my $current_status_alias = $self->SUPER::status_alias;
283 if ($new_status_alias) {
284 # Keep a record of the previous status before we change it,
286 $self->{previous_status} = $current_status_alias ?
287 $current_status_alias :
288 scalar $self->status;
289 # This is hackery to enable us to undefine
290 # status_alias, since we need to have an overloaded
291 # status_alias method to get us around the problem described
293 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
294 # We need a way of accepting implied undef, so we can nullify
295 # the status_alias column, when called from $self->status
296 my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
297 my $ret = $self->SUPER::status_alias($val);
298 my $val_to_log = $val ? $new_status_alias : scalar $self->status;
300 my $logger = Koha::Illrequest::Logger->new;
301 $logger->log_status_change({
306 delete $self->{previous_status};
310 # We can't know which result is the right one if there are multiple
311 # ILL_STATUS_ALIAS authorised values with the same authorised_value column value
312 # so we just use the first
313 my $alias = Koha::AuthorisedValues->search(
315 category => 'ILL_STATUS_ALIAS',
316 authorised_value => $self->SUPER::status_alias
323 return $alias->authorised_value;
331 $Illrequest->status('CANREQ');
333 Overloaded getter/setter for request status,
334 also nullifies status_alias and records the fact that the status has changed
335 and sends a notice if appropriate
340 my ( $self, $new_status) = @_;
342 my $current_status = $self->SUPER::status;
343 my $current_status_alias = $self->SUPER::status_alias;
346 # Keep a record of the previous status before we change it,
348 $self->{previous_status} = $current_status_alias ?
349 $current_status_alias :
351 my $ret = $self->SUPER::status($new_status)->store;
352 if ($current_status_alias) {
353 # This is hackery to enable us to undefine
354 # status_alias, since we need to have an overloaded
355 # status_alias method to get us around the problem described
357 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
358 # We need a way of passing implied undef to nullify status_alias
359 # so we pass -1, which is special cased in the overloaded setter
360 $self->status_alias("-1");
362 my $logger = Koha::Illrequest::Logger->new;
363 $logger->log_status_change({
368 delete $self->{previous_status};
369 # If status has changed to cancellation requested, send a notice
370 if ($new_status eq 'CANCREQ') {
371 $self->send_staff_notice('ILL_REQUEST_CANCEL');
375 return $current_status;
381 Require "Base.pm" from the relevant ILL backend.
386 my ( $self, $backend_id ) = @_;
388 my @raw = qw/Koha Illbackends/; # Base Path
390 my $backend_name = $backend_id || $self->backend;
392 unless ( defined $backend_name && $backend_name ne '' ) {
393 Koha::Exceptions::Ill::InvalidBackendId->throw(
394 "An invalid backend ID was requested ('')");
397 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
398 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
400 $self->{_my_backend} = $backend_class->new({
401 config => $self->_config,
402 logger => Koha::Illrequest::Logger->new
410 my $backend = $abstract->_backend($new_backend);
411 my $backend = $abstract->_backend;
413 Getter/Setter for our API object.
418 my ( $self, $backend ) = @_;
419 $self->{_my_backend} = $backend if ( $backend );
420 # Dynamically load our backend object, as late as possible.
421 $self->load_backend unless ( $self->{_my_backend} );
422 return $self->{_my_backend};
425 =head3 _backend_capability
427 my $backend_capability_result = $self->_backend_capability($name, $args);
429 This is a helper method to invoke optional capabilities in the backend. If
430 the capability named by $name is not supported, return 0, else invoke it,
431 passing $args along with the invocation, and return its return value.
433 NOTE: this module suffers from a confusion in termninology:
435 in _backend_capability, the notion of capability refers to an optional feature
436 that is implemented in core, but might not be supported by a given backend.
438 in capabilities & custom_capability, capability refers to entries in the
439 status_graph (after union between backend and core).
441 The easiest way to fix this would be to fix the terminology in
442 capabilities & custom_capability and their callers.
446 sub _backend_capability {
447 my ( $self, $name, $args ) = @_;
449 # See if capability is defined in backend
451 $capability = $self->_backend->capabilities($name);
457 if ( $capability && ref($capability) eq 'CODE' ) {
458 return &{$capability}($args);
466 my $config = $abstract->_config($config);
467 my $config = $abstract->_config;
469 Getter/Setter for our config object.
474 my ( $self, $config ) = @_;
475 $self->{_my_config} = $config if ( $config );
476 # Load our config object, as late as possible.
477 unless ( $self->{_my_config} ) {
478 $self->{_my_config} = Koha::Illrequest::Config->new;
480 return $self->{_my_config};
489 return $self->_backend->metadata($self);
492 =head3 _core_status_graph
494 my $core_status_graph = $illrequest->_core_status_graph;
496 Returns ILL module's default status graph. A status graph defines the list of
497 available actions at any stage in the ILL workflow. This is for instance used
498 by the perl script & template to generate the correct buttons to display to
499 the end user at any given point.
503 sub _core_status_graph {
507 prev_actions => [ ], # Actions containing buttons
508 # leading to this status
509 id => 'NEW', # ID of this status
510 name => 'New request', # UI name of this status
511 ui_method_name => 'New request', # UI name of method leading
513 method => 'create', # method to this status
514 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
515 # requests with this status
516 ui_method_icon => 'fa-plus', # UI Style class
519 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
522 ui_method_name => 'Confirm request',
524 next_actions => [ 'REQREV', 'COMP', 'CHK' ],
525 ui_method_icon => 'fa-check',
528 prev_actions => [ 'NEW', 'REQREV' ],
530 name => 'Requested from partners',
531 ui_method_name => 'Place request with partners',
532 method => 'generic_confirm',
533 next_actions => [ 'COMP', 'CHK' ],
534 ui_method_icon => 'fa-send-o',
537 prev_actions => [ 'REQ' ],
539 name => 'Request reverted',
540 ui_method_name => 'Revert Request',
542 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
543 ui_method_icon => 'fa-times',
548 name => 'Queued request',
551 next_actions => [ 'REQ', 'KILL' ],
555 prev_actions => [ 'NEW' ],
557 name => 'Cancellation requested',
560 next_actions => [ 'KILL', 'REQ' ],
564 prev_actions => [ 'REQ' ],
567 ui_method_name => 'Mark completed',
568 method => 'mark_completed',
569 next_actions => [ 'CHK' ],
570 ui_method_icon => 'fa-check',
573 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
576 ui_method_name => 'Delete request',
579 ui_method_icon => 'fa-trash',
582 prev_actions => [ 'REQ', 'GENREQ', 'COMP' ],
584 name => 'Checked out',
585 ui_method_name => 'Check out',
586 needs_prefs => [ 'CirculateILL' ],
587 needs_perms => [ 'user_circulate_circulate_remaining_permissions' ],
588 # An array of functions that all must return true
589 needs_all => [ sub { my $r = shift; return $r->biblio; } ],
590 method => 'check_out',
592 ui_method_icon => 'fa-upload',
595 prev_actions => [ 'CHK' ],
597 name => 'Returned to library',
598 ui_method_name => 'Check in',
599 method => 'check_in',
600 next_actions => [ 'COMP' ],
601 ui_method_icon => 'fa-download',
606 =head3 _status_graph_union
608 my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
610 Return a new status_graph, the result of merging $origin & new_graph. This is
611 operation is a union over the sets defied by the two graphs.
613 Each entry in $new_graph is added to $origin. We do not provide a syntax for
614 'subtraction' of entries from $origin.
616 Whilst it is not intended that this works, you can override entries in $origin
617 with entries with the same key in $new_graph. This can lead to problematic
618 behaviour when $new_graph adds an entry, which modifies a dependent entry in
619 $origin, only for the entry in $origin to be replaced later with a new entry
622 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
623 i.e. each of the graphs need to be correct at the outset of the operation.
627 sub _status_graph_union {
628 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
629 # Create new status graph with:
630 # - all core_status_graph
631 # - for-each each backend_status_graph
632 # + add to new status graph
633 # + for each core prev_action:
634 # * locate core_status
635 # * update next_actions with additional next action.
636 # + for each core next_action:
637 # * locate core_status
638 # * update prev_actions with additional prev action
640 my @core_status_ids = keys %{$core_status_graph};
641 my $status_graph = clone($core_status_graph);
643 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
644 my $backend_status = $backend_status_graph->{$backend_status_key};
645 # Add to new status graph
646 $status_graph->{$backend_status_key} = $backend_status;
647 # Update all core methods' next_actions.
648 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
649 if ( grep { $prev_action eq $_ } @core_status_ids ) {
651 @{$status_graph->{$prev_action}->{next_actions}};
652 push @next_actions, $backend_status_key
653 if (!grep(/^$backend_status_key$/, @next_actions));
654 $status_graph->{$prev_action}->{next_actions}
658 # Update all core methods' prev_actions
659 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
660 if ( grep { $next_action eq $_ } @core_status_ids ) {
662 @{$status_graph->{$next_action}->{prev_actions}};
663 push @prev_actions, $backend_status_key
664 if (!grep(/^$backend_status_key$/, @prev_actions));
665 $status_graph->{$next_action}->{prev_actions}
671 return $status_graph;
678 my $capabilities = $illrequest->capabilities;
680 Return a hashref mapping methods to operation names supported by the queried
683 Example return value:
685 { create => "Create Request", confirm => "Progress Request" }
687 NOTE: this module suffers from a confusion in termninology:
689 in _backend_capability, the notion of capability refers to an optional feature
690 that is implemented in core, but might not be supported by a given backend.
692 in capabilities & custom_capability, capability refers to entries in the
693 status_graph (after union between backend and core).
695 The easiest way to fix this would be to fix the terminology in
696 capabilities & custom_capability and their callers.
701 my ( $self, $status ) = @_;
702 # Generate up to date status_graph
703 my $status_graph = $self->_status_graph_union(
704 $self->_core_status_graph,
705 $self->_backend->status_graph({
710 # Extract available actions from graph.
711 return $status_graph->{$status} if $status;
712 # Or return entire graph.
713 return $status_graph;
716 =head3 custom_capability
718 Return the result of invoking $CANDIDATE on this request's backend with
719 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
721 NOTE: this module suffers from a confusion in termninology:
723 in _backend_capability, the notion of capability refers to an optional feature
724 that is implemented in core, but might not be supported by a given backend.
726 in capabilities & custom_capability, capability refers to entries in the
727 status_graph (after union between backend and core).
729 The easiest way to fix this would be to fix the terminology in
730 capabilities & custom_capability and their callers.
734 sub custom_capability {
735 my ( $self, $candidate, $params ) = @_;
736 foreach my $capability ( values %{$self->capabilities} ) {
737 if ( $candidate eq $capability->{method} ) {
739 $self->_backend->$candidate({
743 return $self->expandTemplate($response);
749 =head3 available_backends
751 Return a list of available backends.
755 sub available_backends {
756 my ( $self, $reduced ) = @_;
757 my $backends = $self->_config->available_backends($reduced);
761 =head3 available_actions
763 Return a list of available actions.
767 sub available_actions {
769 my $current_action = $self->capabilities($self->status);
770 my @available_actions = map { $self->capabilities($_) }
771 @{$current_action->{next_actions}};
772 return \@available_actions;
775 =head3 mark_completed
777 Mark a request as completed (status = COMP).
783 $self->status('COMP')->store;
784 $self->completed(dt_from_string())->store;
789 method => 'mark_completed',
795 =head2 backend_illview
797 View and manage an ILL request
801 sub backend_illview {
802 my ( $self, $params ) = @_;
804 my $response = $self->_backend_capability('illview',{
808 return $self->expandTemplate($response) if $response;
812 =head2 backend_migrate
814 Migrate a request from one backend to another.
818 sub backend_migrate {
819 my ( $self, $params ) = @_;
820 # Set the request's backend to be the destination backend
821 $self->load_backend($params->{backend});
822 my $response = $self->_backend_capability('migrate',{
826 return $self->expandTemplate($response) if $response;
830 =head2 backend_confirm
832 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
838 =item * accessurl, cost (if available).
844 sub backend_confirm {
845 my ( $self, $params ) = @_;
847 my $response = $self->_backend->confirm({
851 return $self->expandTemplate($response);
854 =head3 backend_update_status
858 sub backend_update_status {
859 my ( $self, $params ) = @_;
860 return $self->expandTemplate($self->_backend->update_status($params));
863 =head3 backend_cancel
865 my $ILLResponse = $illRequest->backend_cancel;
867 The standard interface method allowing for request cancellation.
872 my ( $self, $params ) = @_;
874 my $result = $self->_backend->cancel({
879 return $self->expandTemplate($result);
884 my $renew_response = $illRequest->backend_renew;
886 The standard interface method allowing for request renewal queries.
892 return $self->expandTemplate(
893 $self->_backend->renew({
899 =head3 backend_create
901 my $create_response = $abstractILL->backend_create($params);
903 Return an array of Record objects created by querying our backend with
906 In the context of the other ILL methods, this is a special method: we only
907 pass it $params, as it does not yet have any other data associated with it.
912 my ( $self, $params ) = @_;
914 # Establish whether we need to do a generic copyright clearance.
915 if ($params->{opac}) {
916 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
917 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
923 stage => 'copyrightclearance',
926 backend => $self->_backend->name
929 } elsif ( defined $params->{stage}
930 && $params->{stage} eq 'copyrightclearance' ) {
931 $params->{stage} = 'init';
934 # First perform API action, then...
939 my $result = $self->_backend->create($args);
941 # ... simple case: we're not at 'commit' stage.
942 my $stage = $result->{stage};
943 return $self->expandTemplate($result)
944 unless ( 'commit' eq $stage );
946 # ... complex case: commit!
948 # Do we still have space for an ILL or should we queue?
949 my $permitted = $self->check_limits(
950 { patron => $self->patron }, { librarycode => $self->branchcode }
953 # Now augment our committed request.
955 $result->{permitted} = $permitted; # Queue request?
959 # ...Updating status!
960 $self->status('QUEUED')->store unless ( $permitted );
962 ## Handle Unmediated ILLs
964 # For the unmediated workflow we only need to delegate to our backend. If
965 # that backend supports unmediateld_ill, it will do its thing and return a
966 # proper response. If it doesn't then _backend_capability returns 0, so
967 # we keep the current result.
968 if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
969 my $unmediated_result = $self->_backend_capability(
973 $result = $unmediated_result if $unmediated_result;
976 return $self->expandTemplate($result);
979 =head3 backend_get_update
981 my $update = backend_get_update($request);
983 Given a request, returns an update in a prescribed
984 format that can then be passed to update parsers
988 sub backend_get_update {
989 my ( $self, $options ) = @_;
991 my $response = $self->_backend_capability(
992 'get_supplier_update',
1001 =head3 expandTemplate
1003 my $params = $abstract->expandTemplate($params);
1005 Return a version of $PARAMS augmented with our required template path.
1009 sub expandTemplate {
1010 my ( $self, $params ) = @_;
1011 my $backend = $self->_backend->name;
1012 # Generate path to file to load
1013 my $backend_dir = $self->_config->backend_dir;
1014 my $backend_tmpl = join "/", $backend_dir, $backend;
1015 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
1016 ( $params->{method}//q{} ) . ".inc";
1017 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
1018 ( $params->{method}//q{} ) . ".inc";
1020 $params->{template} = $intra_tmpl;
1021 $params->{opac_template} = $opac_tmpl;
1025 #### Abstract Imports
1029 my $limit_rules = $abstract->getLimits( {
1030 type => 'brw_cat' | 'branch',
1034 Return the ILL limit rules for the supplied combination of type / value.
1036 As the config may have no rules for this particular type / value combination,
1037 or for the default, we must define fall-back values here.
1042 my ( $self, $params ) = @_;
1043 my $limits = $self->_config->getLimitRules($params->{type});
1045 if ( defined $params->{value}
1046 && defined $limits->{$params->{value}} ) {
1047 return $limits->{$params->{value}};
1050 return $limits->{default} || { count => -1, method => 'active' };
1056 my $prefix = $abstract->getPrefix( {
1057 branch => $branch_code
1060 Return the ILL prefix as defined by our $params: either per borrower category,
1061 per branch or the default.
1066 my ( $self, $params ) = @_;
1067 my $brn_prefixes = $self->_config->getPrefixes();
1068 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
1073 my $type = $abstract->get_type();
1075 Return a string representing the material type of this request or undef
1081 my $attr = $self->illrequestattributes->find({ type => 'type'});
1083 return $attr->value;
1086 #### Illrequests Imports
1090 my $ok = $illRequests->check_limits( {
1091 borrower => $borrower,
1092 branchcode => 'branchcode' | undef,
1095 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
1096 see whether we are still able to place ILLs.
1098 LimitRules are derived from koha-conf.xml:
1099 + default limit counts, and counting method
1100 + branch specific limit counts & counting method
1101 + borrower category specific limit counts & counting method
1102 + err on the side of caution: a counting fail will cause fail, even if
1103 the other counts passes.
1108 my ( $self, $params ) = @_;
1109 my $patron = $params->{patron};
1110 my $branchcode = $params->{librarycode} || $patron->branchcode;
1112 # Establish maximum number of allowed requests
1113 my ( $branch_rules, $brw_rules ) = (
1116 value => $branchcode
1120 value => $patron->categorycode,
1123 my ( $branch_limit, $brw_limit )
1124 = ( $branch_rules->{count}, $brw_rules->{count} );
1125 # Establish currently existing requests
1126 my ( $branch_count, $brw_count ) = (
1127 $self->_limit_counter(
1128 $branch_rules->{method}, { branchcode => $branchcode }
1130 $self->_limit_counter(
1131 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
1135 # Compare and return
1136 # A limit of -1 means no limit exists.
1137 # We return blocked if either branch limit or brw limit is reached.
1138 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
1139 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
1146 sub _limit_counter {
1147 my ( $self, $method, $target ) = @_;
1149 # Establish parameters of counts
1151 if ($method && $method eq 'annual') {
1152 $resultset = Koha::Illrequests->search({
1155 \"YEAR(placed) = YEAR(NOW())"
1158 } else { # assume 'active'
1159 # XXX: This status list is ugly. There should be a method in config
1161 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1162 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1166 return $resultset->count;
1169 =head3 requires_moderation
1171 my $status = $illRequest->requires_moderation;
1173 Return the name of the status if moderation by staff is required; or 0
1178 sub requires_moderation {
1180 my $require_moderation = {
1181 'CANCREQ' => 'CANCREQ',
1183 return $require_moderation->{$self->status};
1188 my $biblio = $request->biblio;
1190 For a given request, return the biblio associated with it,
1191 or undef if none exists
1197 my $biblio_rs = $self->_result->biblio;
1198 return unless $biblio_rs;
1199 return Koha::Biblio->_new_from_dbic($biblio_rs);
1204 my $stage_summary = $request->check_out;
1206 Handle the check_out method. The first stage involves gathering the required
1207 data from the user via a form, the second stage creates an item and tries to
1208 issue it to the patron. If successful, it notifies the patron, then it
1209 returns a summary of how things went
1214 my ( $self, $params ) = @_;
1216 # Objects required by the template
1217 my $itemtypes = Koha::ItemTypes->search(
1219 { order_by => ['description'] }
1221 my $libraries = Koha::Libraries->search(
1223 { order_by => ['branchcode'] }
1225 my $biblio = $self->biblio;
1227 # Find all statistical patrons
1228 my $statistical_patrons = Koha::Patrons->search(
1229 { 'category_type' => 'x' },
1230 { join => { 'categorycode' => 'borrowers' } }
1233 if (!$params->{stage} || $params->{stage} eq 'init') {
1234 # Present a form to gather the required data
1236 # We may be viewing this page having previously tried to issue
1237 # the item (in which case, we may already have created an item)
1238 # so we pass the biblio for this request
1240 method => 'check_out',
1243 itemtypes => $itemtypes,
1244 libraries => $libraries,
1245 statistical => $statistical_patrons,
1249 } elsif ($params->{stage} eq 'form') {
1250 # Validate what we've got and return with an error if we fail
1252 if (!$params->{item_type} || length $params->{item_type} == 0) {
1253 $errors->{item_type} = 1;
1255 if ($params->{inhouse} && length $params->{inhouse} > 0) {
1256 my $patron_count = Koha::Patrons->search({
1257 cardnumber => $params->{inhouse}
1259 if ($patron_count != 1) {
1260 $errors->{inhouse} = 1;
1264 # Check we don't have more than one item for this bib,
1265 # if we do, something very odd is going on
1266 # Having 1 is OK, it means we're likely trying to issue
1267 # following a previously failed attempt, the item exists
1269 my @items = $biblio->items->as_list;
1270 my $item_count = scalar @items;
1271 if ($item_count > 1) {
1272 $errors->{itemcount} = 1;
1275 # Failed validation, go back to the form
1278 method => 'check_out',
1282 statistical => $statistical_patrons,
1283 itemtypes => $itemtypes,
1284 libraries => $libraries,
1293 # Create an item if one doesn't already exist,
1294 # if one does, use that
1296 if ($item_count == 0) {
1298 biblionumber => $self->biblio_id,
1299 homebranch => $params->{branchcode},
1300 holdingbranch => $params->{branchcode},
1301 location => $params->{branchcode},
1302 itype => $params->{item_type},
1303 barcode => 'ILL-' . $self->illrequest_id
1306 my $item = Koha::Item->new($item_hash)->store;
1307 $itemnumber = $item->itemnumber;
1310 $itemnumber = $items[0]->itemnumber;
1312 # Check we have an item before going forward
1315 method => 'check_out',
1319 itemtypes => $itemtypes,
1320 libraries => $libraries,
1321 statistical => $statistical_patrons,
1322 errors => { item_creation => 1 }
1329 # Gather what we need
1330 my $target_item = Koha::Items->find( $itemnumber );
1331 # Determine who we're issuing to
1332 my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
1333 Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
1338 scalar $target_item->barcode
1340 if ($params->{duedate} && length $params->{duedate} > 0) {
1341 push @issue_args, dt_from_string($params->{duedate});
1343 # Check if we can check out
1344 my ( $error, $confirm, $alerts, $messages ) =
1345 C4::Circulation::CanBookBeIssued(@issue_args);
1347 # If we got anything back saying we can't check out,
1348 # return it to the template
1350 if ( $error && %{$error} ) { $problems->{error} = $error };
1351 if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
1352 if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
1353 if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
1357 method => 'check_out',
1361 itemtypes => $itemtypes,
1362 libraries => $libraries,
1363 statistical => $statistical_patrons,
1366 check_out_errors => $problems
1371 # We can allegedly check out, so make it so
1372 # For some reason, AddIssue requires an unblessed Patron
1373 $issue_args[0] = $patron->unblessed;
1374 my $issue = C4::Circulation::AddIssue(@issue_args);
1377 # Update the request status
1378 $self->status('CHK')->store;
1380 method => 'check_out',
1381 stage => 'done_check_out',
1390 method => 'check_out',
1394 itemtypes => $itemtypes,
1395 libraries => $libraries,
1396 errors => { item_check_out => 1 }
1404 =head3 generic_confirm
1406 my $stage_summary = $illRequest->generic_confirm;
1408 Handle the generic_confirm extended method. The first stage involves creating
1409 a template email for the end user to edit in the browser. The second stage
1410 attempts to submit the email.
1414 sub generic_confirm {
1415 my ( $self, $params ) = @_;
1416 my $branch = Koha::Libraries->find($params->{current_branchcode})
1417 || die "Invalid current branchcode. Are you logged in as the database user?";
1418 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
1419 # Get the message body from the notice definition
1420 my $letter = $self->get_notice({
1421 notice_code => 'ILL_PARTNER_REQ',
1422 transport => 'email'
1425 my $partners = Koha::Patrons->search({
1426 categorycode => $self->_config->partner_code
1432 method => 'generic_confirm',
1436 subject => $letter->{title},
1437 body => $letter->{content}
1439 partners => $partners,
1443 } elsif ( 'draft' eq $params->{stage} ) {
1444 # Create the to header
1445 my $to = $params->{partners};
1446 if ( defined $to ) {
1447 $to =~ s/^\x00//; # Strip leading NULLs
1449 Koha::Exceptions::Ill::NoTargetEmail->throw(
1450 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
1453 # Take the null delimited string that we receive and create
1454 # an array of associated patron objects
1455 my @to_patrons = map {
1456 Koha::Patrons->find({ borrowernumber => $_ })
1457 } split(/\x00/, $to);
1459 # Create the from, replyto and sender headers
1460 my $from = $branch->from_email_address;
1461 my $replyto = $branch->inbound_ill_address;
1462 Koha::Exceptions::Ill::NoLibraryEmail->throw(
1463 "Your library has no usable email address. Please set it.")
1466 # So we get a notice hashref, then substitute the possibly
1467 # modified title and body from the draft stage
1468 my $letter = $self->get_notice({
1469 notice_code => 'ILL_PARTNER_REQ',
1470 transport => 'email'
1472 $letter->{title} = $params->{subject};
1473 $letter->{content} = $params->{body};
1477 # Keep track of who received this notice
1479 # Iterate our array of recipient patron objects
1480 foreach my $patron(@to_patrons) {
1481 # Create the params we pass to the notice
1484 borrowernumber => $patron->borrowernumber,
1485 message_transport_type => 'email',
1486 to_address => $patron->email,
1487 from_address => $from,
1488 reply_address => $replyto
1490 my $result = C4::Letters::EnqueueLetter($params);
1492 push @queued, $patron->email;
1496 # If all notices were queued successfully,
1498 if (scalar @queued == scalar @to_patrons) {
1499 $self->status("GENREQ")->store;
1500 $self->_backend_capability(
1501 'set_requested_partners',
1504 to => join("; ", @queued)
1511 method => 'generic_confirm',
1520 status => 'email_failed',
1521 message => 'Email queueing failed',
1522 method => 'generic_confirm',
1526 die "Unknown stage, should not have happened."
1530 =head3 send_patron_notice
1532 my $result = $request->send_patron_notice($notice_code);
1534 Send a specified notice regarding this request to a patron
1538 sub send_patron_notice {
1539 my ( $self, $notice_code, $additional_text ) = @_;
1541 # We need a notice code
1542 if (!$notice_code) {
1544 error => 'notice_no_type'
1548 # Map from the notice code to the messaging preference
1549 my %message_name = (
1550 ILL_PICKUP_READY => 'Ill_ready',
1551 ILL_REQUEST_UNAVAIL => 'Ill_unavailable',
1552 ILL_REQUEST_UPDATE => 'Ill_update'
1555 # Get the patron's messaging preferences
1556 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
1557 borrowernumber => $self->borrowernumber,
1558 message_name => $message_name{$notice_code}
1560 my @transports = keys %{ $borrower_preferences->{transports} };
1562 # Notice should come from the library where the request was placed,
1563 # not the patrons home library
1564 my $branch = Koha::Libraries->find($self->branchcode);
1565 my $from_address = $branch->from_email_address;
1566 my $reply_address = $branch->inbound_ill_address;
1568 # Send the notice to the patron via the chosen transport methods
1569 # and record the results
1572 for my $transport (@transports) {
1573 my $letter = $self->get_notice({
1574 notice_code => $notice_code,
1575 transport => $transport,
1576 additional_text => $additional_text
1579 my $result = C4::Letters::EnqueueLetter({
1581 borrowernumber => $self->borrowernumber,
1582 message_transport_type => $transport,
1583 from_address => $from_address,
1584 reply_address => $reply_address
1587 push @success, $transport;
1589 push @fail, $transport;
1592 push @fail, $transport;
1595 if (scalar @success > 0) {
1596 my $logger = Koha::Illrequest::Logger->new;
1597 $logger->log_patron_notice({
1599 notice_code => $notice_code
1604 success => \@success,
1610 =head3 send_staff_notice
1612 my $result = $request->send_staff_notice($notice_code);
1614 Send a specified notice regarding this request to staff
1618 sub send_staff_notice {
1619 my ( $self, $notice_code ) = @_;
1621 # We need a notice code
1622 if (!$notice_code) {
1624 error => 'notice_no_type'
1628 # Get the staff notices that have been assigned for sending in
1630 my $staff_to_send = C4::Context->preference('ILLSendStaffNotices') // q{};
1632 # If it hasn't been enabled in the syspref, we don't want to send it
1633 if ($staff_to_send !~ /\b$notice_code\b/) {
1635 error => 'notice_not_enabled'
1639 my $letter = $self->get_notice({
1640 notice_code => $notice_code,
1641 transport => 'email'
1644 # Try and get an address to which to send staff notices
1645 my $branch = Koha::Libraries->find($self->branchcode);
1646 my $to_address = $branch->inbound_ill_address;
1647 my $from_address = $branch->inbound_ill_address;
1651 borrowernumber => $self->borrowernumber,
1652 message_transport_type => 'email',
1653 from_address => $from_address
1657 $params->{to_address} = $to_address;
1660 error => 'notice_no_create'
1665 C4::Letters::EnqueueLetter($params)
1666 or warn "can't enqueue letter $letter";
1668 success => 'notice_queued'
1672 error => 'notice_no_create'
1679 my $notice = $request->get_notice($params);
1681 Return a compiled notice hashref for the passed notice code
1687 my ( $self, $params ) = @_;
1689 my $title = $self->illrequestattributes->find(
1692 my $author = $self->illrequestattributes->find(
1693 { type => 'author' }
1695 my $metahash = $self->metadata;
1697 foreach my $key (sort { lc $a cmp lc $b } keys %{$metahash}) {
1698 my $value = $metahash->{$key};
1699 push @metaarray, "- $key: $value" if $value;
1701 my $metastring = join("\n", @metaarray);
1703 my $illrequestattributes = {
1704 map { $_->type => $_->value } $self->illrequestattributes->as_list
1707 my $letter = C4::Letters::GetPreparedLetter(
1709 letter_code => $params->{notice_code},
1710 branchcode => $self->branchcode,
1711 message_transport_type => $params->{transport},
1712 lang => $self->patron->lang,
1714 illrequests => $self->illrequest_id,
1715 borrowers => $self->borrowernumber,
1716 biblio => $self->biblio_id,
1717 branches => $self->branchcode,
1720 ill_bib_title => $title ? $title->value : '',
1721 ill_bib_author => $author ? $author->value : '',
1722 ill_full_metadata => $metastring,
1723 additional_text => $params->{additional_text},
1724 illrequestattributes => $illrequestattributes,
1732 =head3 attach_processors
1734 Receive a Koha::Illrequest::SupplierUpdate and attach
1735 any processors we have for it
1739 sub attach_processors {
1740 my ( $self, $update ) = @_;
1742 foreach my $processor(@{$self->{processors}}) {
1744 $processor->{target_source_type} eq $update->{source_type} &&
1745 $processor->{target_source_name} eq $update->{source_name}
1747 $update->attach_processor($processor);
1752 =head3 append_to_note
1754 append_to_note("Some text");
1756 Append some text to the staff note
1760 sub append_to_note {
1761 my ($self, $text) = @_;
1762 my $current = $self->notesstaff;
1763 $text = ($current && length $current > 0) ? "$current\n\n$text" : $text;
1764 $self->notesstaff($text)->store;
1769 my $prefix = $record->id_prefix;
1771 Return the prefix appropriate for the current Illrequest as derived from the
1772 borrower and branch associated with this request's Status, and the config
1779 my $prefix = $self->getPrefix( {
1780 branch => $self->branchcode,
1782 $prefix .= "-" if ( $prefix );
1788 my $params = $illRequest->_censor($params);
1790 Return $params, modified to reflect our censorship requirements.
1795 my ( $self, $params ) = @_;
1796 my $censorship = $self->_config->censorship;
1797 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1798 if ( $params->{opac} );
1799 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1808 Overloaded I<store> method that, in addition to performing the 'store',
1809 possibly records the fact that something happened
1814 my ( $self, $attrs ) = @_;
1816 my %updated_columns = $self->_result->get_dirty_columns;
1819 if( $self->in_storage and defined $updated_columns{'borrowernumber'} and
1820 Koha::Patrons->find( $updated_columns{'borrowernumber'} ) )
1822 # borrowernumber has changed
1823 my $old_illreq = $self->get_from_storage;
1824 @holds = Koha::Holds->search( {
1825 borrowernumber => $old_illreq->borrowernumber,
1826 biblionumber => $self->biblio_id,
1827 } )->as_list if $old_illreq;
1830 my $ret = $self->SUPER::store;
1832 if ( scalar @holds ) {
1833 # move holds to the changed borrowernumber
1834 foreach my $hold ( @holds ) {
1835 $hold->borrowernumber( $updated_columns{'borrowernumber'} )->store;
1839 $attrs->{log_origin} = 'core';
1841 if ($ret && defined $attrs) {
1842 my $logger = Koha::Illrequest::Logger->new;
1843 $logger->log_maybe({
1852 =head3 requested_partners
1854 my $partners_string = $illRequest->requested_partners;
1856 Return the string representing the email addresses of the partners to
1857 whom a request has been sent
1861 sub requested_partners {
1863 return $self->_backend_capability(
1864 'get_requested_partners',
1865 { request => $self }
1871 $json = $illrequest->TO_JSON
1873 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1874 into the unblessed representation of the object.
1876 TODO: This method does nothing and is not called anywhere. However, bug 74325
1877 touches it, so keeping this for now until both this and bug 74325 are merged,
1878 at which point we can sort it out and remove it completely
1883 my ( $self, $embed ) = @_;
1885 my $object = $self->SUPER::TO_JSON();
1890 =head2 Internal methods
1892 =head3 to_api_mapping
1896 sub to_api_mapping {
1898 accessurl => 'access_url',
1899 backend => 'ill_backend_id',
1900 borrowernumber => 'patron_id',
1901 branchcode => 'library_id',
1902 completed => 'completed_date',
1903 deleted_biblio_id => undef,
1904 illrequest_id => 'ill_request_id',
1905 notesopac => 'opac_notes',
1906 notesstaff => 'staff_notes',
1907 orderid => 'ill_backend_request_id',
1908 placed => 'requested_date',
1909 price_paid => 'paid_price',
1910 replied => 'replied_date',
1911 status_alias => 'status_av',
1912 updated => 'timestamp',
1918 my $strings = $self->string_map({ [ public => 0|1 ] });
1920 Returns a map of column name to string representations. Extra information
1921 is returned depending on the column characteristics as shown below.
1923 Accepts a param hashref where the I<public> key denotes whether we want the public
1924 or staff client strings.
1930 backend => 'backendName',
1931 str => 'Status description',
1932 type => 'ill_status',
1935 category => 'ILL_STATUS_ALIAS,
1936 str => $value, # the AV description, depending on $params->{public}
1944 my ( $self, $params ) = @_;
1946 my $cache = Koha::Cache::Memory::Lite->get_instance();
1947 my $cache_key = 'ill:status_graph:' . $self->backend;
1949 my $status_graph_union = $cache->get($cache_key);
1950 unless ($status_graph_union) {
1951 $status_graph_union = $self->capabilities;
1952 $cache->set( $cache_key, $status_graph_union );
1956 ( exists $status_graph_union->{ $self->status } && defined $status_graph_union->{ $self->status }->{name} )
1957 ? $status_graph_union->{ $self->status }->{name}
1961 ( exists $status_graph_union->{ $self->status } && defined $status_graph_union->{ $self->status }->{id} )
1962 ? $status_graph_union->{ $self->status }->{id}
1967 backend => $self->backend, # the backend identifier
1968 str => $status_string, # the status description, taken from the status graph
1969 code => $status_code, # the status id, taken from the status graph
1970 type => 'ill_status', # fixed type
1974 my $status_alias = $self->statusalias;
1975 if ($status_alias) {
1976 $strings->{"status_alias"} = {
1977 category => 'ILL_STATUS_ALIAS',
1978 str => $params->{public} ? $status_alias->lib_opac : $status_alias->lib,
1979 code => $status_alias->authorised_value,
1992 return 'Illrequest';
1997 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1998 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>