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;
39 use Koha::AuthorisedValues;
45 use C4::Circulation qw( CanBookBeIssued AddIssue );
47 use base qw(Koha::Object);
51 Koha::Illrequest - Koha Illrequest Object class
55 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
56 of related Illrequestattributes.
58 The former encapsulates the basic necessary information that any ILL requires
59 to be usable in Koha. The latter is a set of additional properties used by
62 The former subsumes the legacy "Status" object. The latter remains
63 encapsulated in the "Record" object.
67 - Anything invoking the ->status method; annotated with:
68 + # Old use of ->status !
72 =head2 Backend API Response Principles
74 All methods should return a hashref in the following format:
80 This should be set to 1 if an error was encountered.
84 The status should be a string from the list of statuses detailed below.
88 The message is a free text field that can be passed on to the end user.
92 The value returned by the method.
96 =head2 Interface Status Messages
100 =item * branch_address_incomplete
102 An interface request has determined branch address details are incomplete.
104 =item * cancel_success
106 The interface's cancel_request method was successful in cancelling the
107 Illrequest using the API.
111 The interface's cancel_request method failed to cancel the Illrequest using
116 The interface's request method returned saying that the desired item is not
117 available for request.
123 =head3 init_processors
125 $request->init_processors()
127 Initialises an empty processors arrayref
131 sub init_processors {
134 $self->{processors} = [];
137 =head3 push_processor
139 $request->push_processors(sub { ...something... });
141 Pushes a passed processor function into our processors arrayref
146 my ( $self, $processor ) = @_;
147 push @{$self->{processors}}, $processor;
152 my $batch = $request->batch;
154 Returns the batch associated with a request
161 return Koha::Illbatches->find($self->_result->batch_id);
166 my $statusalias = $request->statusalias;
168 Returns a request's status alias, as a Koha::AuthorisedValue instance
169 or implicit undef. This is distinct from status_alias, which only returns
170 the value in the status_alias column, this method returns the entire
171 AuthorisedValue object
177 return unless $self->status_alias;
178 # We can't know which result is the right one if there are multiple
179 # ILL_STATUS_ALIAS authorised values with the same authorised_value column value
180 # so we just use the first
181 return Koha::AuthorisedValues->search(
183 category => 'ILL_STATUS_ALIAS',
184 authorised_value => $self->SUPER::status_alias
191 =head3 illrequestattributes
195 sub illrequestattributes {
196 deprecated 'illrequestattributes is DEPRECATED in favor of extended_attributes';
198 return Koha::Illrequestattributes->_new_from_dbic(
199 scalar $self->_result->illrequestattributes
209 return Koha::Illcomments->_new_from_dbic(
210 scalar $self->_result->illcomments
216 my $ill_comments = $req->comments;
218 Returns a I<Koha::Illcomments> resultset for the linked comments.
224 return Koha::Illcomments->_new_from_dbic(
225 scalar $self->_result->comments
235 my $logger = Koha::Illrequest::Logger->new;
236 return $logger->get_request_logs($self);
241 my $patron = $request->patron;
243 Returns the linked I<Koha::Patron> object.
250 return Koha::Patron->_new_from_dbic( scalar $self->_result->patron );
255 my $library = $request->library;
257 Returns the linked I<Koha::Library> object.
264 return Koha::Library->_new_from_dbic( scalar $self->_result->library );
267 =head3 extended_attributes
269 my $extended_attributes = $request->extended_attributes;
271 Returns the linked I<Koha::Illrequestattributes> resultset object.
275 sub extended_attributes {
278 my $rs = $self->_result->extended_attributes;
279 # We call search to use the filters in Koha::Illrequestattributes->search
280 return Koha::Illrequestattributes->_new_from_dbic($rs)->search;
285 $Illrequest->status_alias(143);
287 Overloaded getter/setter for status_alias,
288 that only returns authorised values from the
289 correct category and records the fact that the status has changed
294 my ($self, $new_status_alias) = @_;
296 my $current_status_alias = $self->SUPER::status_alias;
298 if ($new_status_alias) {
299 # Keep a record of the previous status before we change it,
301 $self->{previous_status} = $current_status_alias ?
302 $current_status_alias :
303 scalar $self->status;
304 # This is hackery to enable us to undefine
305 # status_alias, since we need to have an overloaded
306 # status_alias method to get us around the problem described
308 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
309 # We need a way of accepting implied undef, so we can nullify
310 # the status_alias column, when called from $self->status
311 my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
312 my $ret = $self->SUPER::status_alias($val);
313 my $val_to_log = $val ? $new_status_alias : scalar $self->status;
315 my $logger = Koha::Illrequest::Logger->new;
316 $logger->log_status_change({
321 delete $self->{previous_status};
325 # We can't know which result is the right one if there are multiple
326 # ILL_STATUS_ALIAS authorised values with the same authorised_value column value
327 # so we just use the first
328 my $alias = Koha::AuthorisedValues->search(
330 category => 'ILL_STATUS_ALIAS',
331 authorised_value => $self->SUPER::status_alias
338 return $alias->authorised_value;
346 $Illrequest->status('CANREQ');
348 Overloaded getter/setter for request status,
349 also nullifies status_alias and records the fact that the status has changed
350 and sends a notice if appropriate
355 my ( $self, $new_status) = @_;
357 my $current_status = $self->SUPER::status;
358 my $current_status_alias = $self->SUPER::status_alias;
361 # Keep a record of the previous status before we change it,
363 $self->{previous_status} = $current_status_alias ?
364 $current_status_alias :
366 my $ret = $self->SUPER::status($new_status)->store;
367 if ($current_status_alias) {
368 # This is hackery to enable us to undefine
369 # status_alias, since we need to have an overloaded
370 # status_alias method to get us around the problem described
372 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
373 # We need a way of passing implied undef to nullify status_alias
374 # so we pass -1, which is special cased in the overloaded setter
375 $self->status_alias("-1");
377 my $logger = Koha::Illrequest::Logger->new;
378 $logger->log_status_change({
383 delete $self->{previous_status};
384 # If status has changed to cancellation requested, send a notice
385 if ($new_status eq 'CANCREQ') {
386 $self->send_staff_notice('ILL_REQUEST_CANCEL');
390 return $current_status;
396 Require "Base.pm" from the relevant ILL backend.
401 my ( $self, $backend_id ) = @_;
403 my @raw = qw/Koha Illbackends/; # Base Path
405 my $backend_name = $backend_id || $self->backend;
407 unless ( defined $backend_name && $backend_name ne '' ) {
408 Koha::Exceptions::Ill::InvalidBackendId->throw(
409 "An invalid backend ID was requested ('')");
412 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
413 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
415 $self->{_my_backend} = $backend_class->new({
416 config => $self->_config,
417 logger => Koha::Illrequest::Logger->new
425 my $backend = $abstract->_backend($new_backend);
426 my $backend = $abstract->_backend;
428 Getter/Setter for our API object.
433 my ( $self, $backend ) = @_;
434 $self->{_my_backend} = $backend if ( $backend );
435 # Dynamically load our backend object, as late as possible.
436 $self->load_backend unless ( $self->{_my_backend} );
437 return $self->{_my_backend};
440 =head3 _backend_capability
442 my $backend_capability_result = $self->_backend_capability($name, $args);
444 This is a helper method to invoke optional capabilities in the backend. If
445 the capability named by $name is not supported, return 0, else invoke it,
446 passing $args along with the invocation, and return its return value.
448 NOTE: this module suffers from a confusion in termninology:
450 in _backend_capability, the notion of capability refers to an optional feature
451 that is implemented in core, but might not be supported by a given backend.
453 in capabilities & custom_capability, capability refers to entries in the
454 status_graph (after union between backend and core).
456 The easiest way to fix this would be to fix the terminology in
457 capabilities & custom_capability and their callers.
461 sub _backend_capability {
462 my ( $self, $name, $args ) = @_;
464 # See if capability is defined in backend
466 $capability = $self->_backend->capabilities($name);
472 if ( $capability && ref($capability) eq 'CODE' ) {
473 return &{$capability}($args);
481 my $config = $abstract->_config($config);
482 my $config = $abstract->_config;
484 Getter/Setter for our config object.
489 my ( $self, $config ) = @_;
490 $self->{_my_config} = $config if ( $config );
491 # Load our config object, as late as possible.
492 unless ( $self->{_my_config} ) {
493 $self->{_my_config} = Koha::Illrequest::Config->new;
495 return $self->{_my_config};
504 return $self->_backend->metadata($self);
507 =head3 _core_status_graph
509 my $core_status_graph = $illrequest->_core_status_graph;
511 Returns ILL module's default status graph. A status graph defines the list of
512 available actions at any stage in the ILL workflow. This is for instance used
513 by the perl script & template to generate the correct buttons to display to
514 the end user at any given point.
518 sub _core_status_graph {
522 prev_actions => [ ], # Actions containing buttons
523 # leading to this status
524 id => 'NEW', # ID of this status
525 name => 'New request', # UI name of this status
526 ui_method_name => 'New request', # UI name of method leading
528 method => 'create', # method to this status
529 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
530 # requests with this status
531 ui_method_icon => 'fa-plus', # UI Style class
534 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
537 ui_method_name => 'Confirm request',
539 next_actions => [ 'REQREV', 'COMP', 'CHK' ],
540 ui_method_icon => 'fa-check',
543 prev_actions => [ 'NEW', 'REQREV' ],
545 name => 'Requested from partners',
546 ui_method_name => 'Place request with partners',
547 method => 'generic_confirm',
548 next_actions => [ 'COMP', 'CHK', 'REQREV' ],
549 ui_method_icon => 'fa-paper-plane',
552 prev_actions => [ 'REQ', 'GENREQ' ],
554 name => 'Request reverted',
555 ui_method_name => 'Revert request',
557 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
558 ui_method_icon => 'fa-times',
563 name => 'Queued request',
566 next_actions => [ 'REQ', 'KILL' ],
570 prev_actions => [ 'NEW' ],
572 name => 'Cancellation requested',
575 next_actions => [ 'KILL', 'REQ' ],
579 prev_actions => [ 'REQ' ],
582 ui_method_name => 'Mark completed',
583 method => 'mark_completed',
584 next_actions => [ 'CHK' ],
585 ui_method_icon => 'fa-check',
588 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
591 ui_method_name => 'Delete request',
594 ui_method_icon => 'fa-trash',
597 prev_actions => [ 'REQ', 'GENREQ', 'COMP' ],
599 name => 'Checked out',
600 ui_method_name => 'Check out',
601 needs_prefs => [ 'CirculateILL' ],
602 needs_perms => [ 'user_circulate_circulate_remaining_permissions' ],
603 # An array of functions that all must return true
604 needs_all => [ sub { my $r = shift; return $r->biblio; } ],
605 method => 'check_out',
607 ui_method_icon => 'fa-upload',
610 prev_actions => [ 'CHK' ],
612 name => 'Returned to library',
613 ui_method_name => 'Check in',
614 method => 'check_in',
615 next_actions => [ 'COMP' ],
616 ui_method_icon => 'fa-download',
621 =head3 _status_graph_union
623 my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
625 Return a new status_graph, the result of merging $origin & new_graph. This is
626 operation is a union over the sets defied by the two graphs.
628 Each entry in $new_graph is added to $origin. We do not provide a syntax for
629 'subtraction' of entries from $origin.
631 Whilst it is not intended that this works, you can override entries in $origin
632 with entries with the same key in $new_graph. This can lead to problematic
633 behaviour when $new_graph adds an entry, which modifies a dependent entry in
634 $origin, only for the entry in $origin to be replaced later with a new entry
637 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
638 i.e. each of the graphs need to be correct at the outset of the operation.
642 sub _status_graph_union {
643 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
644 # Create new status graph with:
645 # - all core_status_graph
646 # - for-each each backend_status_graph
647 # + add to new status graph
648 # + for each core prev_action:
649 # * locate core_status
650 # * update next_actions with additional next action.
651 # + for each core next_action:
652 # * locate core_status
653 # * update prev_actions with additional prev action
655 my @core_status_ids = keys %{$core_status_graph};
656 my $status_graph = clone($core_status_graph);
658 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
659 my $backend_status = $backend_status_graph->{$backend_status_key};
660 # Add to new status graph
661 $status_graph->{$backend_status_key} = $backend_status;
662 # Update all core methods' next_actions.
663 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
664 if ( grep { $prev_action eq $_ } @core_status_ids ) {
666 @{$status_graph->{$prev_action}->{next_actions}};
667 push @next_actions, $backend_status_key
668 if (!grep(/^$backend_status_key$/, @next_actions));
669 $status_graph->{$prev_action}->{next_actions}
673 # Update all core methods' prev_actions
674 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
675 if ( grep { $next_action eq $_ } @core_status_ids ) {
677 @{$status_graph->{$next_action}->{prev_actions}};
678 push @prev_actions, $backend_status_key
679 if (!grep(/^$backend_status_key$/, @prev_actions));
680 $status_graph->{$next_action}->{prev_actions}
686 return $status_graph;
693 my $capabilities = $illrequest->capabilities;
695 Return a hashref mapping methods to operation names supported by the queried
698 Example return value:
700 { create => "Create Request", confirm => "Progress Request" }
702 NOTE: this module suffers from a confusion in termninology:
704 in _backend_capability, the notion of capability refers to an optional feature
705 that is implemented in core, but might not be supported by a given backend.
707 in capabilities & custom_capability, capability refers to entries in the
708 status_graph (after union between backend and core).
710 The easiest way to fix this would be to fix the terminology in
711 capabilities & custom_capability and their callers.
716 my ( $self, $status ) = @_;
717 # Generate up to date status_graph
718 my $status_graph = $self->_status_graph_union(
719 $self->_core_status_graph,
720 $self->_backend->status_graph({
725 # Extract available actions from graph.
726 return $status_graph->{$status} if $status;
727 # Or return entire graph.
728 return $status_graph;
731 =head3 custom_capability
733 Return the result of invoking $CANDIDATE on this request's backend with
734 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
736 NOTE: this module suffers from a confusion in termninology:
738 in _backend_capability, the notion of capability refers to an optional feature
739 that is implemented in core, but might not be supported by a given backend.
741 in capabilities & custom_capability, capability refers to entries in the
742 status_graph (after union between backend and core).
744 The easiest way to fix this would be to fix the terminology in
745 capabilities & custom_capability and their callers.
749 sub custom_capability {
750 my ( $self, $candidate, $params ) = @_;
751 foreach my $capability ( values %{$self->capabilities} ) {
752 if ( $candidate eq $capability->{method} ) {
754 $self->_backend->$candidate({
758 return $self->expandTemplate($response);
764 =head3 available_backends
766 Return a list of available backends.
770 sub available_backends {
771 my ( $self, $reduced ) = @_;
772 my $backends = $self->_config->available_backends($reduced);
776 =head3 available_actions
778 Return a list of available actions.
782 sub available_actions {
784 my $current_action = $self->capabilities($self->status);
785 my @available_actions = map { $self->capabilities($_) }
786 @{$current_action->{next_actions}};
787 return \@available_actions;
790 =head3 mark_completed
792 Mark a request as completed (status = COMP).
798 $self->status('COMP')->store;
799 $self->completed(dt_from_string())->store;
804 method => 'mark_completed',
810 =head2 backend_illview
812 View and manage an ILL request
816 sub backend_illview {
817 my ( $self, $params ) = @_;
819 my $response = $self->_backend_capability('illview',{
823 return $self->expandTemplate($response) if $response;
827 =head2 backend_migrate
829 Migrate a request from one backend to another.
833 sub backend_migrate {
834 my ( $self, $params ) = @_;
835 # Set the request's backend to be the destination backend
836 $self->load_backend($params->{backend});
837 my $response = $self->_backend_capability('migrate',{
841 return $self->expandTemplate($response) if $response;
845 =head2 backend_confirm
847 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
853 =item * accessurl, cost (if available).
859 sub backend_confirm {
860 my ( $self, $params ) = @_;
862 my $response = $self->_backend->confirm({
866 return $self->expandTemplate($response);
869 =head3 backend_update_status
873 sub backend_update_status {
874 my ( $self, $params ) = @_;
875 return $self->expandTemplate($self->_backend->update_status($params));
878 =head3 backend_cancel
880 my $ILLResponse = $illRequest->backend_cancel;
882 The standard interface method allowing for request cancellation.
887 my ( $self, $params ) = @_;
889 my $result = $self->_backend->cancel({
894 return $self->expandTemplate($result);
899 my $renew_response = $illRequest->backend_renew;
901 The standard interface method allowing for request renewal queries.
907 return $self->expandTemplate(
908 $self->_backend->renew({
914 =head3 backend_create
916 my $create_response = $abstractILL->backend_create($params);
918 Return an array of Record objects created by querying our backend with
921 In the context of the other ILL methods, this is a special method: we only
922 pass it $params, as it does not yet have any other data associated with it.
927 my ( $self, $params ) = @_;
929 # Establish whether we need to do a generic copyright clearance.
930 if ($params->{opac}) {
931 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
932 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
938 stage => 'copyrightclearance',
941 backend => $self->_backend->name
944 } elsif ( defined $params->{stage}
945 && $params->{stage} eq 'copyrightclearance' ) {
946 $params->{stage} = 'init';
949 # First perform API action, then...
954 my $result = $self->_backend->create($args);
956 # ... simple case: we're not at 'commit' stage.
957 my $stage = $result->{stage};
958 return $self->expandTemplate($result)
959 unless ( 'commit' eq $stage );
961 # ... complex case: commit!
963 # Do we still have space for an ILL or should we queue?
964 my $permitted = $self->check_limits(
965 { patron => $self->patron }, { librarycode => $self->branchcode }
968 # Now augment our committed request.
970 $result->{permitted} = $permitted; # Queue request?
974 # ...Updating status!
975 $self->status('QUEUED')->store unless ( $permitted );
977 ## Handle Unmediated ILLs
979 # For the unmediated workflow we only need to delegate to our backend. If
980 # that backend supports unmediateld_ill, it will do its thing and return a
981 # proper response. If it doesn't then _backend_capability returns 0, so
982 # we keep the current result.
983 if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
984 my $unmediated_result = $self->_backend_capability(
988 $result = $unmediated_result if $unmediated_result;
991 return $self->expandTemplate($result);
994 =head3 backend_get_update
996 my $update = backend_get_update($request);
998 Given a request, returns an update in a prescribed
999 format that can then be passed to update parsers
1003 sub backend_get_update {
1004 my ( $self, $options ) = @_;
1006 my $response = $self->_backend_capability(
1007 'get_supplier_update',
1016 =head3 expandTemplate
1018 my $params = $abstract->expandTemplate($params);
1020 Return a version of $PARAMS augmented with our required template path.
1024 sub expandTemplate {
1025 my ( $self, $params ) = @_;
1026 my $backend = $self->_backend->name;
1027 # Generate path to file to load
1028 my $backend_dir = $self->_config->backend_dir;
1029 my $backend_tmpl = join "/", $backend_dir, $backend;
1030 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
1031 ( $params->{method}//q{} ) . ".inc";
1032 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
1033 ( $params->{method}//q{} ) . ".inc";
1035 $params->{template} = $intra_tmpl;
1036 $params->{opac_template} = $opac_tmpl;
1040 #### Abstract Imports
1044 my $limit_rules = $abstract->getLimits( {
1045 type => 'brw_cat' | 'branch',
1049 Return the ILL limit rules for the supplied combination of type / value.
1051 As the config may have no rules for this particular type / value combination,
1052 or for the default, we must define fall-back values here.
1057 my ( $self, $params ) = @_;
1058 my $limits = $self->_config->getLimitRules($params->{type});
1060 if ( defined $params->{value}
1061 && defined $limits->{$params->{value}} ) {
1062 return $limits->{$params->{value}};
1065 return $limits->{default} || { count => -1, method => 'active' };
1071 my $prefix = $abstract->getPrefix( {
1072 branch => $branch_code
1075 Return the ILL prefix as defined by our $params: either per borrower category,
1076 per branch or the default.
1081 my ( $self, $params ) = @_;
1082 my $brn_prefixes = $self->_config->getPrefixes();
1083 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
1088 my $type = $abstract->get_type();
1090 Return a string representing the material type of this request or undef
1096 my $attr = $self->illrequestattributes->find({ type => 'type'});
1098 return $attr->value;
1101 #### Illrequests Imports
1105 my $ok = $illRequests->check_limits( {
1106 borrower => $borrower,
1107 branchcode => 'branchcode' | undef,
1110 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
1111 see whether we are still able to place ILLs.
1113 LimitRules are derived from koha-conf.xml:
1114 + default limit counts, and counting method
1115 + branch specific limit counts & counting method
1116 + borrower category specific limit counts & counting method
1117 + err on the side of caution: a counting fail will cause fail, even if
1118 the other counts passes.
1123 my ( $self, $params ) = @_;
1124 my $patron = $params->{patron};
1125 my $branchcode = $params->{librarycode} || $patron->branchcode;
1127 # Establish maximum number of allowed requests
1128 my ( $branch_rules, $brw_rules ) = (
1131 value => $branchcode
1135 value => $patron->categorycode,
1138 my ( $branch_limit, $brw_limit )
1139 = ( $branch_rules->{count}, $brw_rules->{count} );
1140 # Establish currently existing requests
1141 my ( $branch_count, $brw_count ) = (
1142 $self->_limit_counter(
1143 $branch_rules->{method}, { branchcode => $branchcode }
1145 $self->_limit_counter(
1146 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
1150 # Compare and return
1151 # A limit of -1 means no limit exists.
1152 # We return blocked if either branch limit or brw limit is reached.
1153 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
1154 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
1161 sub _limit_counter {
1162 my ( $self, $method, $target ) = @_;
1164 # Establish parameters of counts
1166 if ($method && $method eq 'annual') {
1167 $resultset = Koha::Illrequests->search({
1170 \"YEAR(placed) = YEAR(NOW())"
1173 } else { # assume 'active'
1174 # XXX: This status list is ugly. There should be a method in config
1176 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1177 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1181 return $resultset->count;
1184 =head3 requires_moderation
1186 my $status = $illRequest->requires_moderation;
1188 Return the name of the status if moderation by staff is required; or 0
1193 sub requires_moderation {
1195 my $require_moderation = {
1196 'CANCREQ' => 'CANCREQ',
1198 return $require_moderation->{$self->status};
1203 my $biblio = $request->biblio;
1205 For a given request, return the biblio associated with it,
1206 or undef if none exists
1212 my $biblio_rs = $self->_result->biblio;
1213 return unless $biblio_rs;
1214 return Koha::Biblio->_new_from_dbic($biblio_rs);
1219 my $stage_summary = $request->check_out;
1221 Handle the check_out method. The first stage involves gathering the required
1222 data from the user via a form, the second stage creates an item and tries to
1223 issue it to the patron. If successful, it notifies the patron, then it
1224 returns a summary of how things went
1229 my ( $self, $params ) = @_;
1231 # Objects required by the template
1232 my $itemtypes = Koha::ItemTypes->search(
1234 { order_by => ['description'] }
1236 my $libraries = Koha::Libraries->search(
1238 { order_by => ['branchcode'] }
1240 my $biblio = $self->biblio;
1242 # Find all statistical patrons
1243 my $statistical_patrons = Koha::Patrons->search(
1244 { 'category_type' => 'x' },
1245 { join => { 'categorycode' => 'borrowers' } }
1248 if (!$params->{stage} || $params->{stage} eq 'init') {
1249 # Present a form to gather the required data
1251 # We may be viewing this page having previously tried to issue
1252 # the item (in which case, we may already have created an item)
1253 # so we pass the biblio for this request
1255 method => 'check_out',
1258 itemtypes => $itemtypes,
1259 libraries => $libraries,
1260 statistical => $statistical_patrons,
1264 } elsif ($params->{stage} eq 'form') {
1265 # Validate what we've got and return with an error if we fail
1267 if (!$params->{item_type} || length $params->{item_type} == 0) {
1268 $errors->{item_type} = 1;
1270 if ($params->{inhouse} && length $params->{inhouse} > 0) {
1271 my $patron_count = Koha::Patrons->search({
1272 cardnumber => $params->{inhouse}
1274 if ($patron_count != 1) {
1275 $errors->{inhouse} = 1;
1279 # Check we don't have more than one item for this bib,
1280 # if we do, something very odd is going on
1281 # Having 1 is OK, it means we're likely trying to issue
1282 # following a previously failed attempt, the item exists
1284 my @items = $biblio->items->as_list;
1285 my $item_count = scalar @items;
1286 if ($item_count > 1) {
1287 $errors->{itemcount} = 1;
1290 # Failed validation, go back to the form
1293 method => 'check_out',
1297 statistical => $statistical_patrons,
1298 itemtypes => $itemtypes,
1299 libraries => $libraries,
1308 # Create an item if one doesn't already exist,
1309 # if one does, use that
1311 if ($item_count == 0) {
1313 biblionumber => $self->biblio_id,
1314 homebranch => $params->{branchcode},
1315 holdingbranch => $params->{branchcode},
1316 location => $params->{branchcode},
1317 itype => $params->{item_type},
1318 barcode => 'ILL-' . $self->illrequest_id
1321 my $item = Koha::Item->new($item_hash)->store;
1322 $itemnumber = $item->itemnumber;
1325 $itemnumber = $items[0]->itemnumber;
1327 # Check we have an item before going forward
1330 method => 'check_out',
1334 itemtypes => $itemtypes,
1335 libraries => $libraries,
1336 statistical => $statistical_patrons,
1337 errors => { item_creation => 1 }
1344 # Gather what we need
1345 my $target_item = Koha::Items->find( $itemnumber );
1346 # Determine who we're issuing to
1347 my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
1348 Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
1353 scalar $target_item->barcode
1355 if ($params->{duedate} && length $params->{duedate} > 0) {
1356 push @issue_args, dt_from_string($params->{duedate});
1358 # Check if we can check out
1359 my ( $error, $confirm, $alerts, $messages ) =
1360 C4::Circulation::CanBookBeIssued(@issue_args);
1362 # If we got anything back saying we can't check out,
1363 # return it to the template
1365 if ( $error && %{$error} ) { $problems->{error} = $error };
1366 if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
1367 if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
1368 if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
1372 method => 'check_out',
1376 itemtypes => $itemtypes,
1377 libraries => $libraries,
1378 statistical => $statistical_patrons,
1381 check_out_errors => $problems
1386 # We can allegedly check out, so make it so
1387 my $issue = C4::Circulation::AddIssue(@issue_args);
1390 # Update the request status
1391 $self->status('CHK')->store;
1393 method => 'check_out',
1394 stage => 'done_check_out',
1403 method => 'check_out',
1407 itemtypes => $itemtypes,
1408 libraries => $libraries,
1409 errors => { item_check_out => 1 }
1417 =head3 generic_confirm
1419 my $stage_summary = $illRequest->generic_confirm;
1421 Handle the generic_confirm extended method. The first stage involves creating
1422 a template email for the end user to edit in the browser. The second stage
1423 attempts to submit the email.
1427 sub generic_confirm {
1428 my ( $self, $params ) = @_;
1429 my $branch = Koha::Libraries->find($params->{current_branchcode})
1430 || die "Invalid current branchcode. Are you logged in as the database user?";
1431 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
1432 # Get the message body from the notice definition
1433 my $letter = $self->get_notice({
1434 notice_code => 'ILL_PARTNER_REQ',
1435 transport => 'email'
1438 my $partners = Koha::Patrons->search({
1439 categorycode => $self->_config->partner_code
1445 method => 'generic_confirm',
1449 subject => $letter->{title},
1450 body => $letter->{content}
1452 partners => $partners,
1456 } elsif ( 'draft' eq $params->{stage} ) {
1457 # Create the to header
1458 my $to = $params->{partners};
1459 if ( defined $to ) {
1460 $to =~ s/^\x00//; # Strip leading NULLs
1462 Koha::Exceptions::Ill::NoTargetEmail->throw(
1463 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
1466 # Take the null delimited string that we receive and create
1467 # an array of associated patron objects
1468 my @to_patrons = map {
1469 Koha::Patrons->find({ borrowernumber => $_ })
1470 } split(/\x00/, $to);
1472 # Create the from, replyto and sender headers
1473 my $from = $branch->from_email_address;
1474 my $replyto = $branch->inbound_ill_address;
1475 Koha::Exceptions::Ill::NoLibraryEmail->throw(
1476 "Your library has no usable email address. Please set it.")
1479 # So we get a notice hashref, then substitute the possibly
1480 # modified title and body from the draft stage
1481 my $letter = $self->get_notice({
1482 notice_code => 'ILL_PARTNER_REQ',
1483 transport => 'email'
1485 $letter->{title} = $params->{subject};
1486 $letter->{content} = $params->{body};
1490 # Keep track of who received this notice
1492 # Iterate our array of recipient patron objects
1493 foreach my $patron(@to_patrons) {
1494 # Create the params we pass to the notice
1497 borrowernumber => $patron->borrowernumber,
1498 message_transport_type => 'email',
1499 to_address => $patron->email,
1500 from_address => $from,
1501 reply_address => $replyto
1503 my $result = C4::Letters::EnqueueLetter($params);
1505 push @queued, $patron->email;
1509 # If all notices were queued successfully,
1511 if (scalar @queued == scalar @to_patrons) {
1512 $self->status("GENREQ")->store;
1513 $self->_backend_capability(
1514 'set_requested_partners',
1517 to => join("; ", @queued)
1524 method => 'generic_confirm',
1533 status => 'email_failed',
1534 message => 'Email queueing failed',
1535 method => 'generic_confirm',
1539 die "Unknown stage, should not have happened."
1543 =head3 send_patron_notice
1545 my $result = $request->send_patron_notice($notice_code);
1547 Send a specified notice regarding this request to a patron
1551 sub send_patron_notice {
1552 my ( $self, $notice_code, $additional_text ) = @_;
1554 # We need a notice code
1555 if (!$notice_code) {
1557 error => 'notice_no_type'
1561 # Map from the notice code to the messaging preference
1562 my %message_name = (
1563 ILL_PICKUP_READY => 'Ill_ready',
1564 ILL_REQUEST_UNAVAIL => 'Ill_unavailable',
1565 ILL_REQUEST_UPDATE => 'Ill_update'
1568 # Get the patron's messaging preferences
1569 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
1570 borrowernumber => $self->borrowernumber,
1571 message_name => $message_name{$notice_code}
1573 my @transports = keys %{ $borrower_preferences->{transports} };
1575 # Notice should come from the library where the request was placed,
1576 # not the patrons home library
1577 my $branch = Koha::Libraries->find($self->branchcode);
1578 my $from_address = $branch->from_email_address;
1579 my $reply_address = $branch->inbound_ill_address;
1581 # Send the notice to the patron via the chosen transport methods
1582 # and record the results
1585 for my $transport (@transports) {
1586 my $letter = $self->get_notice({
1587 notice_code => $notice_code,
1588 transport => $transport,
1589 additional_text => $additional_text
1592 my $result = C4::Letters::EnqueueLetter({
1594 borrowernumber => $self->borrowernumber,
1595 message_transport_type => $transport,
1596 from_address => $from_address,
1597 reply_address => $reply_address
1600 push @success, $transport;
1602 push @fail, $transport;
1605 push @fail, $transport;
1608 if (scalar @success > 0) {
1609 my $logger = Koha::Illrequest::Logger->new;
1610 $logger->log_patron_notice({
1612 notice_code => $notice_code
1617 success => \@success,
1623 =head3 send_staff_notice
1625 my $result = $request->send_staff_notice($notice_code);
1627 Send a specified notice regarding this request to staff
1631 sub send_staff_notice {
1632 my ( $self, $notice_code ) = @_;
1634 # We need a notice code
1635 if (!$notice_code) {
1637 error => 'notice_no_type'
1641 # Get the staff notices that have been assigned for sending in
1643 my $staff_to_send = C4::Context->preference('ILLSendStaffNotices') // q{};
1645 # If it hasn't been enabled in the syspref, we don't want to send it
1646 if ($staff_to_send !~ /\b$notice_code\b/) {
1648 error => 'notice_not_enabled'
1652 my $letter = $self->get_notice({
1653 notice_code => $notice_code,
1654 transport => 'email'
1657 # Try and get an address to which to send staff notices
1658 my $branch = Koha::Libraries->find($self->branchcode);
1659 my $to_address = $branch->inbound_ill_address;
1660 my $from_address = $branch->inbound_ill_address;
1664 borrowernumber => $self->borrowernumber,
1665 message_transport_type => 'email',
1666 from_address => $from_address
1670 $params->{to_address} = $to_address;
1673 error => 'notice_no_create'
1678 C4::Letters::EnqueueLetter($params)
1679 or warn "can't enqueue letter $letter";
1681 success => 'notice_queued'
1685 error => 'notice_no_create'
1692 my $notice = $request->get_notice($params);
1694 Return a compiled notice hashref for the passed notice code
1700 my ( $self, $params ) = @_;
1702 my $title = $self->illrequestattributes->find(
1705 my $author = $self->illrequestattributes->find(
1706 { type => 'author' }
1708 my $metahash = $self->metadata;
1710 foreach my $key (sort { lc $a cmp lc $b } keys %{$metahash}) {
1711 my $value = $metahash->{$key};
1712 push @metaarray, "- $key: $value" if $value;
1714 my $metastring = join("\n", @metaarray);
1716 my $illrequestattributes = {
1717 map { $_->type => $_->value } $self->illrequestattributes->as_list
1720 my $letter = C4::Letters::GetPreparedLetter(
1722 letter_code => $params->{notice_code},
1723 branchcode => $self->branchcode,
1724 message_transport_type => $params->{transport},
1725 lang => $self->patron->lang,
1727 illrequests => $self->illrequest_id,
1728 borrowers => $self->borrowernumber,
1729 biblio => $self->biblio_id,
1730 branches => $self->branchcode,
1733 ill_bib_title => $title ? $title->value : '',
1734 ill_bib_author => $author ? $author->value : '',
1735 ill_full_metadata => $metastring,
1736 additional_text => $params->{additional_text},
1737 illrequestattributes => $illrequestattributes,
1745 =head3 attach_processors
1747 Receive a Koha::Illrequest::SupplierUpdate and attach
1748 any processors we have for it
1752 sub attach_processors {
1753 my ( $self, $update ) = @_;
1755 foreach my $processor(@{$self->{processors}}) {
1757 $processor->{target_source_type} eq $update->{source_type} &&
1758 $processor->{target_source_name} eq $update->{source_name}
1760 $update->attach_processor($processor);
1765 =head3 append_to_note
1767 append_to_note("Some text");
1769 Append some text to the staff note
1773 sub append_to_note {
1774 my ($self, $text) = @_;
1775 my $current = $self->notesstaff;
1776 $text = ($current && length $current > 0) ? "$current\n\n$text" : $text;
1777 $self->notesstaff($text)->store;
1782 my $prefix = $record->id_prefix;
1784 Return the prefix appropriate for the current Illrequest as derived from the
1785 borrower and branch associated with this request's Status, and the config
1792 my $prefix = $self->getPrefix( {
1793 branch => $self->branchcode,
1795 $prefix .= "-" if ( $prefix );
1801 my $params = $illRequest->_censor($params);
1803 Return $params, modified to reflect our censorship requirements.
1808 my ( $self, $params ) = @_;
1809 my $censorship = $self->_config->censorship;
1810 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1811 if ( $params->{opac} );
1812 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1821 Overloaded I<store> method that, in addition to performing the 'store',
1822 possibly records the fact that something happened
1827 my ( $self, $attrs ) = @_;
1829 my %updated_columns = $self->_result->get_dirty_columns;
1832 if( $self->in_storage and defined $updated_columns{'borrowernumber'} and
1833 Koha::Patrons->find( $updated_columns{'borrowernumber'} ) )
1835 # borrowernumber has changed
1836 my $old_illreq = $self->get_from_storage;
1837 @holds = Koha::Holds->search( {
1838 borrowernumber => $old_illreq->borrowernumber,
1839 biblionumber => $self->biblio_id,
1840 } )->as_list if $old_illreq;
1843 my $ret = $self->SUPER::store;
1845 if ( scalar @holds ) {
1846 # move holds to the changed borrowernumber
1847 foreach my $hold ( @holds ) {
1848 $hold->borrowernumber( $updated_columns{'borrowernumber'} )->store;
1852 $attrs->{log_origin} = 'core';
1854 if ($ret && defined $attrs) {
1855 my $logger = Koha::Illrequest::Logger->new;
1856 $logger->log_maybe({
1865 =head3 requested_partners
1867 my $partners_string = $illRequest->requested_partners;
1869 Return the string representing the email addresses of the partners to
1870 whom a request has been sent
1874 sub requested_partners {
1876 return $self->_backend_capability(
1877 'get_requested_partners',
1878 { request => $self }
1884 $json = $illrequest->TO_JSON
1886 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1887 into the unblessed representation of the object.
1889 TODO: This method does nothing and is not called anywhere. However, bug 74325
1890 touches it, so keeping this for now until both this and bug 74325 are merged,
1891 at which point we can sort it out and remove it completely
1896 my ( $self, $embed ) = @_;
1898 my $object = $self->SUPER::TO_JSON();
1903 =head2 Internal methods
1905 =head3 to_api_mapping
1909 sub to_api_mapping {
1911 accessurl => 'access_url',
1912 backend => 'ill_backend_id',
1913 borrowernumber => 'patron_id',
1914 branchcode => 'library_id',
1915 completed => 'completed_date',
1916 deleted_biblio_id => undef,
1917 illrequest_id => 'ill_request_id',
1918 notesopac => 'opac_notes',
1919 notesstaff => 'staff_notes',
1920 orderid => 'ill_backend_request_id',
1921 placed => 'requested_date',
1922 price_paid => 'paid_price',
1923 replied => 'replied_date',
1924 status_alias => 'status_av',
1925 updated => 'timestamp',
1931 my $strings = $self->string_map({ [ public => 0|1 ] });
1933 Returns a map of column name to string representations. Extra information
1934 is returned depending on the column characteristics as shown below.
1936 Accepts a param hashref where the I<public> key denotes whether we want the public
1937 or staff client strings.
1943 backend => 'backendName',
1944 str => 'Status description',
1945 type => 'ill_status',
1948 category => 'ILL_STATUS_ALIAS,
1949 str => $value, # the AV description, depending on $params->{public}
1957 my ( $self, $params ) = @_;
1959 my $cache = Koha::Cache::Memory::Lite->get_instance();
1960 my $cache_key = 'ill:status_graph:' . $self->backend;
1962 my $status_graph_union = $cache->get($cache_key);
1963 unless ($status_graph_union) {
1964 $status_graph_union = $self->capabilities;
1965 $cache->set( $cache_key, $status_graph_union );
1969 ( exists $status_graph_union->{ $self->status } && defined $status_graph_union->{ $self->status }->{name} )
1970 ? $status_graph_union->{ $self->status }->{name}
1974 ( exists $status_graph_union->{ $self->status } && defined $status_graph_union->{ $self->status }->{id} )
1975 ? $status_graph_union->{ $self->status }->{id}
1980 backend => $self->backend, # the backend identifier
1981 str => $status_string, # the status description, taken from the status graph
1982 code => $status_code, # the status id, taken from the status graph
1983 type => 'ill_status', # fixed type
1987 my $status_alias = $self->statusalias;
1988 if ($status_alias) {
1989 $strings->{"status_alias"} = {
1990 category => 'ILL_STATUS_ALIAS',
1991 str => $params->{public} ? $status_alias->lib_opac : $status_alias->lib,
1992 code => $status_alias->authorised_value,
1997 my $batch = $self->batch;
1999 $strings->{"batch"} = {
2001 name => $batch->name,
2002 backend => $batch->backend,
2003 borrowernumber => $batch->borrowernumber,
2004 branchcode => $batch->branchcode,
2005 statuscode => $batch->statuscode
2017 return 'Illrequest';
2022 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
2023 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>