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 For a given request, return the linked I<Koha::Patron> object
229 associated with it, or undef if none exists
236 my $patron_rs = $self->_result->patron;
237 return unless $patron_rs;
238 return Koha::Patron->_new_from_dbic($patron_rs);
243 my $library = $request->library;
245 Returns the linked I<Koha::Library> object.
252 return Koha::Library->_new_from_dbic( scalar $self->_result->library );
255 =head3 extended_attributes
257 my $extended_attributes = $request->extended_attributes;
259 Returns the linked I<Koha::Illrequestattributes> resultset object.
263 sub extended_attributes {
266 my $rs = $self->_result->extended_attributes;
267 # We call search to use the filters in Koha::Illrequestattributes->search
268 return Koha::Illrequestattributes->_new_from_dbic($rs)->search;
273 $Illrequest->status_alias(143);
275 Overloaded getter/setter for status_alias,
276 that only returns authorised values from the
277 correct category and records the fact that the status has changed
282 my ($self, $new_status_alias) = @_;
284 my $current_status_alias = $self->SUPER::status_alias;
286 if ($new_status_alias) {
287 # Keep a record of the previous status before we change it,
289 $self->{previous_status} = $current_status_alias ?
290 $current_status_alias :
291 scalar $self->status;
292 # This is hackery to enable us to undefine
293 # status_alias, since we need to have an overloaded
294 # status_alias method to get us around the problem described
296 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
297 # We need a way of accepting implied undef, so we can nullify
298 # the status_alias column, when called from $self->status
299 my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
300 my $ret = $self->SUPER::status_alias($val);
301 my $val_to_log = $val ? $new_status_alias : scalar $self->status;
303 my $logger = Koha::Illrequest::Logger->new;
304 $logger->log_status_change({
309 delete $self->{previous_status};
313 # We can't know which result is the right one if there are multiple
314 # ILL_STATUS_ALIAS authorised values with the same authorised_value column value
315 # so we just use the first
316 my $alias = Koha::AuthorisedValues->search(
318 category => 'ILL_STATUS_ALIAS',
319 authorised_value => $self->SUPER::status_alias
326 return $alias->authorised_value;
334 $Illrequest->status('CANREQ');
336 Overloaded getter/setter for request status,
337 also nullifies status_alias and records the fact that the status has changed
338 and sends a notice if appropriate
343 my ( $self, $new_status) = @_;
345 my $current_status = $self->SUPER::status;
346 my $current_status_alias = $self->SUPER::status_alias;
349 # Keep a record of the previous status before we change it,
351 $self->{previous_status} = $current_status_alias ?
352 $current_status_alias :
354 my $ret = $self->SUPER::status($new_status)->store;
355 if ($current_status_alias) {
356 # This is hackery to enable us to undefine
357 # status_alias, since we need to have an overloaded
358 # status_alias method to get us around the problem described
360 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
361 # We need a way of passing implied undef to nullify status_alias
362 # so we pass -1, which is special cased in the overloaded setter
363 $self->status_alias("-1");
365 my $logger = Koha::Illrequest::Logger->new;
366 $logger->log_status_change({
371 delete $self->{previous_status};
372 # If status has changed to cancellation requested, send a notice
373 if ($new_status eq 'CANCREQ') {
374 $self->send_staff_notice('ILL_REQUEST_CANCEL');
378 return $current_status;
384 Require "Base.pm" from the relevant ILL backend.
389 my ( $self, $backend_id ) = @_;
391 my @raw = qw/Koha Illbackends/; # Base Path
393 my $backend_name = $backend_id || $self->backend;
395 unless ( defined $backend_name && $backend_name ne '' ) {
396 Koha::Exceptions::Ill::InvalidBackendId->throw(
397 "An invalid backend ID was requested ('')");
400 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
401 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
403 $self->{_my_backend} = $backend_class->new({
404 config => $self->_config,
405 logger => Koha::Illrequest::Logger->new
413 my $backend = $abstract->_backend($new_backend);
414 my $backend = $abstract->_backend;
416 Getter/Setter for our API object.
421 my ( $self, $backend ) = @_;
422 $self->{_my_backend} = $backend if ( $backend );
423 # Dynamically load our backend object, as late as possible.
424 $self->load_backend unless ( $self->{_my_backend} );
425 return $self->{_my_backend};
428 =head3 _backend_capability
430 my $backend_capability_result = $self->_backend_capability($name, $args);
432 This is a helper method to invoke optional capabilities in the backend. If
433 the capability named by $name is not supported, return 0, else invoke it,
434 passing $args along with the invocation, and return its return value.
436 NOTE: this module suffers from a confusion in termninology:
438 in _backend_capability, the notion of capability refers to an optional feature
439 that is implemented in core, but might not be supported by a given backend.
441 in capabilities & custom_capability, capability refers to entries in the
442 status_graph (after union between backend and core).
444 The easiest way to fix this would be to fix the terminology in
445 capabilities & custom_capability and their callers.
449 sub _backend_capability {
450 my ( $self, $name, $args ) = @_;
452 # See if capability is defined in backend
454 $capability = $self->_backend->capabilities($name);
460 if ( $capability && ref($capability) eq 'CODE' ) {
461 return &{$capability}($args);
469 my $config = $abstract->_config($config);
470 my $config = $abstract->_config;
472 Getter/Setter for our config object.
477 my ( $self, $config ) = @_;
478 $self->{_my_config} = $config if ( $config );
479 # Load our config object, as late as possible.
480 unless ( $self->{_my_config} ) {
481 $self->{_my_config} = Koha::Illrequest::Config->new;
483 return $self->{_my_config};
492 return $self->_backend->metadata($self);
495 =head3 _core_status_graph
497 my $core_status_graph = $illrequest->_core_status_graph;
499 Returns ILL module's default status graph. A status graph defines the list of
500 available actions at any stage in the ILL workflow. This is for instance used
501 by the perl script & template to generate the correct buttons to display to
502 the end user at any given point.
506 sub _core_status_graph {
510 prev_actions => [ ], # Actions containing buttons
511 # leading to this status
512 id => 'NEW', # ID of this status
513 name => 'New request', # UI name of this status
514 ui_method_name => 'New request', # UI name of method leading
516 method => 'create', # method to this status
517 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
518 # requests with this status
519 ui_method_icon => 'fa-plus', # UI Style class
522 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
525 ui_method_name => 'Confirm request',
527 next_actions => [ 'REQREV', 'COMP', 'CHK' ],
528 ui_method_icon => 'fa-check',
531 prev_actions => [ 'NEW', 'REQREV' ],
533 name => 'Requested from partners',
534 ui_method_name => 'Place request with partners',
535 method => 'generic_confirm',
536 next_actions => [ 'COMP', 'CHK' ],
537 ui_method_icon => 'fa-send-o',
540 prev_actions => [ 'REQ' ],
542 name => 'Request reverted',
543 ui_method_name => 'Revert Request',
545 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
546 ui_method_icon => 'fa-times',
551 name => 'Queued request',
554 next_actions => [ 'REQ', 'KILL' ],
558 prev_actions => [ 'NEW' ],
560 name => 'Cancellation requested',
563 next_actions => [ 'KILL', 'REQ' ],
567 prev_actions => [ 'REQ' ],
570 ui_method_name => 'Mark completed',
571 method => 'mark_completed',
572 next_actions => [ 'CHK' ],
573 ui_method_icon => 'fa-check',
576 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
579 ui_method_name => 'Delete request',
582 ui_method_icon => 'fa-trash',
585 prev_actions => [ 'REQ', 'GENREQ', 'COMP' ],
587 name => 'Checked out',
588 ui_method_name => 'Check out',
589 needs_prefs => [ 'CirculateILL' ],
590 needs_perms => [ 'user_circulate_circulate_remaining_permissions' ],
591 # An array of functions that all must return true
592 needs_all => [ sub { my $r = shift; return $r->biblio; } ],
593 method => 'check_out',
595 ui_method_icon => 'fa-upload',
598 prev_actions => [ 'CHK' ],
600 name => 'Returned to library',
601 ui_method_name => 'Check in',
602 method => 'check_in',
603 next_actions => [ 'COMP' ],
604 ui_method_icon => 'fa-download',
609 =head3 _status_graph_union
611 my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
613 Return a new status_graph, the result of merging $origin & new_graph. This is
614 operation is a union over the sets defied by the two graphs.
616 Each entry in $new_graph is added to $origin. We do not provide a syntax for
617 'subtraction' of entries from $origin.
619 Whilst it is not intended that this works, you can override entries in $origin
620 with entries with the same key in $new_graph. This can lead to problematic
621 behaviour when $new_graph adds an entry, which modifies a dependent entry in
622 $origin, only for the entry in $origin to be replaced later with a new entry
625 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
626 i.e. each of the graphs need to be correct at the outset of the operation.
630 sub _status_graph_union {
631 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
632 # Create new status graph with:
633 # - all core_status_graph
634 # - for-each each backend_status_graph
635 # + add to new status graph
636 # + for each core prev_action:
637 # * locate core_status
638 # * update next_actions with additional next action.
639 # + for each core next_action:
640 # * locate core_status
641 # * update prev_actions with additional prev action
643 my @core_status_ids = keys %{$core_status_graph};
644 my $status_graph = clone($core_status_graph);
646 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
647 my $backend_status = $backend_status_graph->{$backend_status_key};
648 # Add to new status graph
649 $status_graph->{$backend_status_key} = $backend_status;
650 # Update all core methods' next_actions.
651 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
652 if ( grep { $prev_action eq $_ } @core_status_ids ) {
654 @{$status_graph->{$prev_action}->{next_actions}};
655 push @next_actions, $backend_status_key
656 if (!grep(/^$backend_status_key$/, @next_actions));
657 $status_graph->{$prev_action}->{next_actions}
661 # Update all core methods' prev_actions
662 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
663 if ( grep { $next_action eq $_ } @core_status_ids ) {
665 @{$status_graph->{$next_action}->{prev_actions}};
666 push @prev_actions, $backend_status_key
667 if (!grep(/^$backend_status_key$/, @prev_actions));
668 $status_graph->{$next_action}->{prev_actions}
674 return $status_graph;
681 my $capabilities = $illrequest->capabilities;
683 Return a hashref mapping methods to operation names supported by the queried
686 Example return value:
688 { create => "Create Request", confirm => "Progress Request" }
690 NOTE: this module suffers from a confusion in termninology:
692 in _backend_capability, the notion of capability refers to an optional feature
693 that is implemented in core, but might not be supported by a given backend.
695 in capabilities & custom_capability, capability refers to entries in the
696 status_graph (after union between backend and core).
698 The easiest way to fix this would be to fix the terminology in
699 capabilities & custom_capability and their callers.
704 my ( $self, $status ) = @_;
705 # Generate up to date status_graph
706 my $status_graph = $self->_status_graph_union(
707 $self->_core_status_graph,
708 $self->_backend->status_graph({
713 # Extract available actions from graph.
714 return $status_graph->{$status} if $status;
715 # Or return entire graph.
716 return $status_graph;
719 =head3 custom_capability
721 Return the result of invoking $CANDIDATE on this request's backend with
722 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
724 NOTE: this module suffers from a confusion in termninology:
726 in _backend_capability, the notion of capability refers to an optional feature
727 that is implemented in core, but might not be supported by a given backend.
729 in capabilities & custom_capability, capability refers to entries in the
730 status_graph (after union between backend and core).
732 The easiest way to fix this would be to fix the terminology in
733 capabilities & custom_capability and their callers.
737 sub custom_capability {
738 my ( $self, $candidate, $params ) = @_;
739 foreach my $capability ( values %{$self->capabilities} ) {
740 if ( $candidate eq $capability->{method} ) {
742 $self->_backend->$candidate({
746 return $self->expandTemplate($response);
752 =head3 available_backends
754 Return a list of available backends.
758 sub available_backends {
759 my ( $self, $reduced ) = @_;
760 my $backends = $self->_config->available_backends($reduced);
764 =head3 available_actions
766 Return a list of available actions.
770 sub available_actions {
772 my $current_action = $self->capabilities($self->status);
773 my @available_actions = map { $self->capabilities($_) }
774 @{$current_action->{next_actions}};
775 return \@available_actions;
778 =head3 mark_completed
780 Mark a request as completed (status = COMP).
786 $self->status('COMP')->store;
787 $self->completed(dt_from_string())->store;
792 method => 'mark_completed',
798 =head2 backend_illview
800 View and manage an ILL request
804 sub backend_illview {
805 my ( $self, $params ) = @_;
807 my $response = $self->_backend_capability('illview',{
811 return $self->expandTemplate($response) if $response;
815 =head2 backend_migrate
817 Migrate a request from one backend to another.
821 sub backend_migrate {
822 my ( $self, $params ) = @_;
823 # Set the request's backend to be the destination backend
824 $self->load_backend($params->{backend});
825 my $response = $self->_backend_capability('migrate',{
829 return $self->expandTemplate($response) if $response;
833 =head2 backend_confirm
835 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
841 =item * accessurl, cost (if available).
847 sub backend_confirm {
848 my ( $self, $params ) = @_;
850 my $response = $self->_backend->confirm({
854 return $self->expandTemplate($response);
857 =head3 backend_update_status
861 sub backend_update_status {
862 my ( $self, $params ) = @_;
863 return $self->expandTemplate($self->_backend->update_status($params));
866 =head3 backend_cancel
868 my $ILLResponse = $illRequest->backend_cancel;
870 The standard interface method allowing for request cancellation.
875 my ( $self, $params ) = @_;
877 my $result = $self->_backend->cancel({
882 return $self->expandTemplate($result);
887 my $renew_response = $illRequest->backend_renew;
889 The standard interface method allowing for request renewal queries.
895 return $self->expandTemplate(
896 $self->_backend->renew({
902 =head3 backend_create
904 my $create_response = $abstractILL->backend_create($params);
906 Return an array of Record objects created by querying our backend with
909 In the context of the other ILL methods, this is a special method: we only
910 pass it $params, as it does not yet have any other data associated with it.
915 my ( $self, $params ) = @_;
917 # Establish whether we need to do a generic copyright clearance.
918 if ($params->{opac}) {
919 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
920 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
926 stage => 'copyrightclearance',
929 backend => $self->_backend->name
932 } elsif ( defined $params->{stage}
933 && $params->{stage} eq 'copyrightclearance' ) {
934 $params->{stage} = 'init';
937 # First perform API action, then...
942 my $result = $self->_backend->create($args);
944 # ... simple case: we're not at 'commit' stage.
945 my $stage = $result->{stage};
946 return $self->expandTemplate($result)
947 unless ( 'commit' eq $stage );
949 # ... complex case: commit!
951 # Do we still have space for an ILL or should we queue?
952 my $permitted = $self->check_limits(
953 { patron => $self->patron }, { librarycode => $self->branchcode }
956 # Now augment our committed request.
958 $result->{permitted} = $permitted; # Queue request?
962 # ...Updating status!
963 $self->status('QUEUED')->store unless ( $permitted );
965 ## Handle Unmediated ILLs
967 # For the unmediated workflow we only need to delegate to our backend. If
968 # that backend supports unmediateld_ill, it will do its thing and return a
969 # proper response. If it doesn't then _backend_capability returns 0, so
970 # we keep the current result.
971 if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
972 my $unmediated_result = $self->_backend_capability(
976 $result = $unmediated_result if $unmediated_result;
979 return $self->expandTemplate($result);
982 =head3 backend_get_update
984 my $update = backend_get_update($request);
986 Given a request, returns an update in a prescribed
987 format that can then be passed to update parsers
991 sub backend_get_update {
992 my ( $self, $options ) = @_;
994 my $response = $self->_backend_capability(
995 'get_supplier_update',
1004 =head3 expandTemplate
1006 my $params = $abstract->expandTemplate($params);
1008 Return a version of $PARAMS augmented with our required template path.
1012 sub expandTemplate {
1013 my ( $self, $params ) = @_;
1014 my $backend = $self->_backend->name;
1015 # Generate path to file to load
1016 my $backend_dir = $self->_config->backend_dir;
1017 my $backend_tmpl = join "/", $backend_dir, $backend;
1018 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
1019 ( $params->{method}//q{} ) . ".inc";
1020 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
1021 ( $params->{method}//q{} ) . ".inc";
1023 $params->{template} = $intra_tmpl;
1024 $params->{opac_template} = $opac_tmpl;
1028 #### Abstract Imports
1032 my $limit_rules = $abstract->getLimits( {
1033 type => 'brw_cat' | 'branch',
1037 Return the ILL limit rules for the supplied combination of type / value.
1039 As the config may have no rules for this particular type / value combination,
1040 or for the default, we must define fall-back values here.
1045 my ( $self, $params ) = @_;
1046 my $limits = $self->_config->getLimitRules($params->{type});
1048 if ( defined $params->{value}
1049 && defined $limits->{$params->{value}} ) {
1050 return $limits->{$params->{value}};
1053 return $limits->{default} || { count => -1, method => 'active' };
1059 my $prefix = $abstract->getPrefix( {
1060 branch => $branch_code
1063 Return the ILL prefix as defined by our $params: either per borrower category,
1064 per branch or the default.
1069 my ( $self, $params ) = @_;
1070 my $brn_prefixes = $self->_config->getPrefixes();
1071 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
1076 my $type = $abstract->get_type();
1078 Return a string representing the material type of this request or undef
1084 my $attr = $self->illrequestattributes->find({ type => 'type'});
1086 return $attr->value;
1089 #### Illrequests Imports
1093 my $ok = $illRequests->check_limits( {
1094 borrower => $borrower,
1095 branchcode => 'branchcode' | undef,
1098 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
1099 see whether we are still able to place ILLs.
1101 LimitRules are derived from koha-conf.xml:
1102 + default limit counts, and counting method
1103 + branch specific limit counts & counting method
1104 + borrower category specific limit counts & counting method
1105 + err on the side of caution: a counting fail will cause fail, even if
1106 the other counts passes.
1111 my ( $self, $params ) = @_;
1112 my $patron = $params->{patron};
1113 my $branchcode = $params->{librarycode} || $patron->branchcode;
1115 # Establish maximum number of allowed requests
1116 my ( $branch_rules, $brw_rules ) = (
1119 value => $branchcode
1123 value => $patron->categorycode,
1126 my ( $branch_limit, $brw_limit )
1127 = ( $branch_rules->{count}, $brw_rules->{count} );
1128 # Establish currently existing requests
1129 my ( $branch_count, $brw_count ) = (
1130 $self->_limit_counter(
1131 $branch_rules->{method}, { branchcode => $branchcode }
1133 $self->_limit_counter(
1134 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
1138 # Compare and return
1139 # A limit of -1 means no limit exists.
1140 # We return blocked if either branch limit or brw limit is reached.
1141 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
1142 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
1149 sub _limit_counter {
1150 my ( $self, $method, $target ) = @_;
1152 # Establish parameters of counts
1154 if ($method && $method eq 'annual') {
1155 $resultset = Koha::Illrequests->search({
1158 \"YEAR(placed) = YEAR(NOW())"
1161 } else { # assume 'active'
1162 # XXX: This status list is ugly. There should be a method in config
1164 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1165 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1169 return $resultset->count;
1172 =head3 requires_moderation
1174 my $status = $illRequest->requires_moderation;
1176 Return the name of the status if moderation by staff is required; or 0
1181 sub requires_moderation {
1183 my $require_moderation = {
1184 'CANCREQ' => 'CANCREQ',
1186 return $require_moderation->{$self->status};
1191 my $biblio = $request->biblio;
1193 For a given request, return the biblio associated with it,
1194 or undef if none exists
1200 my $biblio_rs = $self->_result->biblio;
1201 return unless $biblio_rs;
1202 return Koha::Biblio->_new_from_dbic($biblio_rs);
1207 my $stage_summary = $request->check_out;
1209 Handle the check_out method. The first stage involves gathering the required
1210 data from the user via a form, the second stage creates an item and tries to
1211 issue it to the patron. If successful, it notifies the patron, then it
1212 returns a summary of how things went
1217 my ( $self, $params ) = @_;
1219 # Objects required by the template
1220 my $itemtypes = Koha::ItemTypes->search(
1222 { order_by => ['description'] }
1224 my $libraries = Koha::Libraries->search(
1226 { order_by => ['branchcode'] }
1228 my $biblio = $self->biblio;
1230 # Find all statistical patrons
1231 my $statistical_patrons = Koha::Patrons->search(
1232 { 'category_type' => 'x' },
1233 { join => { 'categorycode' => 'borrowers' } }
1236 if (!$params->{stage} || $params->{stage} eq 'init') {
1237 # Present a form to gather the required data
1239 # We may be viewing this page having previously tried to issue
1240 # the item (in which case, we may already have created an item)
1241 # so we pass the biblio for this request
1243 method => 'check_out',
1246 itemtypes => $itemtypes,
1247 libraries => $libraries,
1248 statistical => $statistical_patrons,
1252 } elsif ($params->{stage} eq 'form') {
1253 # Validate what we've got and return with an error if we fail
1255 if (!$params->{item_type} || length $params->{item_type} == 0) {
1256 $errors->{item_type} = 1;
1258 if ($params->{inhouse} && length $params->{inhouse} > 0) {
1259 my $patron_count = Koha::Patrons->search({
1260 cardnumber => $params->{inhouse}
1262 if ($patron_count != 1) {
1263 $errors->{inhouse} = 1;
1267 # Check we don't have more than one item for this bib,
1268 # if we do, something very odd is going on
1269 # Having 1 is OK, it means we're likely trying to issue
1270 # following a previously failed attempt, the item exists
1272 my @items = $biblio->items->as_list;
1273 my $item_count = scalar @items;
1274 if ($item_count > 1) {
1275 $errors->{itemcount} = 1;
1278 # Failed validation, go back to the form
1281 method => 'check_out',
1285 statistical => $statistical_patrons,
1286 itemtypes => $itemtypes,
1287 libraries => $libraries,
1296 # Create an item if one doesn't already exist,
1297 # if one does, use that
1299 if ($item_count == 0) {
1301 biblionumber => $self->biblio_id,
1302 homebranch => $params->{branchcode},
1303 holdingbranch => $params->{branchcode},
1304 location => $params->{branchcode},
1305 itype => $params->{item_type},
1306 barcode => 'ILL-' . $self->illrequest_id
1309 my $item = Koha::Item->new($item_hash)->store;
1310 $itemnumber = $item->itemnumber;
1313 $itemnumber = $items[0]->itemnumber;
1315 # Check we have an item before going forward
1318 method => 'check_out',
1322 itemtypes => $itemtypes,
1323 libraries => $libraries,
1324 statistical => $statistical_patrons,
1325 errors => { item_creation => 1 }
1332 # Gather what we need
1333 my $target_item = Koha::Items->find( $itemnumber );
1334 # Determine who we're issuing to
1335 my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
1336 Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
1341 scalar $target_item->barcode
1343 if ($params->{duedate} && length $params->{duedate} > 0) {
1344 push @issue_args, dt_from_string($params->{duedate});
1346 # Check if we can check out
1347 my ( $error, $confirm, $alerts, $messages ) =
1348 C4::Circulation::CanBookBeIssued(@issue_args);
1350 # If we got anything back saying we can't check out,
1351 # return it to the template
1353 if ( $error && %{$error} ) { $problems->{error} = $error };
1354 if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
1355 if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
1356 if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
1360 method => 'check_out',
1364 itemtypes => $itemtypes,
1365 libraries => $libraries,
1366 statistical => $statistical_patrons,
1369 check_out_errors => $problems
1374 # We can allegedly check out, so make it so
1375 # For some reason, AddIssue requires an unblessed Patron
1376 $issue_args[0] = $patron->unblessed;
1377 my $issue = C4::Circulation::AddIssue(@issue_args);
1380 # Update the request status
1381 $self->status('CHK')->store;
1383 method => 'check_out',
1384 stage => 'done_check_out',
1393 method => 'check_out',
1397 itemtypes => $itemtypes,
1398 libraries => $libraries,
1399 errors => { item_check_out => 1 }
1407 =head3 generic_confirm
1409 my $stage_summary = $illRequest->generic_confirm;
1411 Handle the generic_confirm extended method. The first stage involves creating
1412 a template email for the end user to edit in the browser. The second stage
1413 attempts to submit the email.
1417 sub generic_confirm {
1418 my ( $self, $params ) = @_;
1419 my $branch = Koha::Libraries->find($params->{current_branchcode})
1420 || die "Invalid current branchcode. Are you logged in as the database user?";
1421 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
1422 # Get the message body from the notice definition
1423 my $letter = $self->get_notice({
1424 notice_code => 'ILL_PARTNER_REQ',
1425 transport => 'email'
1428 my $partners = Koha::Patrons->search({
1429 categorycode => $self->_config->partner_code
1435 method => 'generic_confirm',
1439 subject => $letter->{title},
1440 body => $letter->{content}
1442 partners => $partners,
1446 } elsif ( 'draft' eq $params->{stage} ) {
1447 # Create the to header
1448 my $to = $params->{partners};
1449 if ( defined $to ) {
1450 $to =~ s/^\x00//; # Strip leading NULLs
1452 Koha::Exceptions::Ill::NoTargetEmail->throw(
1453 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
1456 # Take the null delimited string that we receive and create
1457 # an array of associated patron objects
1458 my @to_patrons = map {
1459 Koha::Patrons->find({ borrowernumber => $_ })
1460 } split(/\x00/, $to);
1462 # Create the from, replyto and sender headers
1463 my $from = $branch->from_email_address;
1464 my $replyto = $branch->inbound_ill_address;
1465 Koha::Exceptions::Ill::NoLibraryEmail->throw(
1466 "Your library has no usable email address. Please set it.")
1469 # So we get a notice hashref, then substitute the possibly
1470 # modified title and body from the draft stage
1471 my $letter = $self->get_notice({
1472 notice_code => 'ILL_PARTNER_REQ',
1473 transport => 'email'
1475 $letter->{title} = $params->{subject};
1476 $letter->{content} = $params->{body};
1480 # Keep track of who received this notice
1482 # Iterate our array of recipient patron objects
1483 foreach my $patron(@to_patrons) {
1484 # Create the params we pass to the notice
1487 borrowernumber => $patron->borrowernumber,
1488 message_transport_type => 'email',
1489 to_address => $patron->email,
1490 from_address => $from,
1491 reply_address => $replyto
1493 my $result = C4::Letters::EnqueueLetter($params);
1495 push @queued, $patron->email;
1499 # If all notices were queued successfully,
1501 if (scalar @queued == scalar @to_patrons) {
1502 $self->status("GENREQ")->store;
1503 $self->_backend_capability(
1504 'set_requested_partners',
1507 to => join("; ", @queued)
1514 method => 'generic_confirm',
1523 status => 'email_failed',
1524 message => 'Email queueing failed',
1525 method => 'generic_confirm',
1529 die "Unknown stage, should not have happened."
1533 =head3 send_patron_notice
1535 my $result = $request->send_patron_notice($notice_code);
1537 Send a specified notice regarding this request to a patron
1541 sub send_patron_notice {
1542 my ( $self, $notice_code, $additional_text ) = @_;
1544 # We need a notice code
1545 if (!$notice_code) {
1547 error => 'notice_no_type'
1551 # Map from the notice code to the messaging preference
1552 my %message_name = (
1553 ILL_PICKUP_READY => 'Ill_ready',
1554 ILL_REQUEST_UNAVAIL => 'Ill_unavailable',
1555 ILL_REQUEST_UPDATE => 'Ill_update'
1558 # Get the patron's messaging preferences
1559 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
1560 borrowernumber => $self->borrowernumber,
1561 message_name => $message_name{$notice_code}
1563 my @transports = keys %{ $borrower_preferences->{transports} };
1565 # Notice should come from the library where the request was placed,
1566 # not the patrons home library
1567 my $branch = Koha::Libraries->find($self->branchcode);
1568 my $from_address = $branch->from_email_address;
1569 my $reply_address = $branch->inbound_ill_address;
1571 # Send the notice to the patron via the chosen transport methods
1572 # and record the results
1575 for my $transport (@transports) {
1576 my $letter = $self->get_notice({
1577 notice_code => $notice_code,
1578 transport => $transport,
1579 additional_text => $additional_text
1582 my $result = C4::Letters::EnqueueLetter({
1584 borrowernumber => $self->borrowernumber,
1585 message_transport_type => $transport,
1586 from_address => $from_address,
1587 reply_address => $reply_address
1590 push @success, $transport;
1592 push @fail, $transport;
1595 push @fail, $transport;
1598 if (scalar @success > 0) {
1599 my $logger = Koha::Illrequest::Logger->new;
1600 $logger->log_patron_notice({
1602 notice_code => $notice_code
1607 success => \@success,
1613 =head3 send_staff_notice
1615 my $result = $request->send_staff_notice($notice_code);
1617 Send a specified notice regarding this request to staff
1621 sub send_staff_notice {
1622 my ( $self, $notice_code ) = @_;
1624 # We need a notice code
1625 if (!$notice_code) {
1627 error => 'notice_no_type'
1631 # Get the staff notices that have been assigned for sending in
1633 my $staff_to_send = C4::Context->preference('ILLSendStaffNotices') // q{};
1635 # If it hasn't been enabled in the syspref, we don't want to send it
1636 if ($staff_to_send !~ /\b$notice_code\b/) {
1638 error => 'notice_not_enabled'
1642 my $letter = $self->get_notice({
1643 notice_code => $notice_code,
1644 transport => 'email'
1647 # Try and get an address to which to send staff notices
1648 my $branch = Koha::Libraries->find($self->branchcode);
1649 my $to_address = $branch->inbound_ill_address;
1650 my $from_address = $branch->inbound_ill_address;
1654 borrowernumber => $self->borrowernumber,
1655 message_transport_type => 'email',
1656 from_address => $from_address
1660 $params->{to_address} = $to_address;
1663 error => 'notice_no_create'
1668 C4::Letters::EnqueueLetter($params)
1669 or warn "can't enqueue letter $letter";
1671 success => 'notice_queued'
1675 error => 'notice_no_create'
1682 my $notice = $request->get_notice($params);
1684 Return a compiled notice hashref for the passed notice code
1690 my ( $self, $params ) = @_;
1692 my $title = $self->illrequestattributes->find(
1695 my $author = $self->illrequestattributes->find(
1696 { type => 'author' }
1698 my $metahash = $self->metadata;
1700 foreach my $key (sort { lc $a cmp lc $b } keys %{$metahash}) {
1701 my $value = $metahash->{$key};
1702 push @metaarray, "- $key: $value" if $value;
1704 my $metastring = join("\n", @metaarray);
1705 my $letter = C4::Letters::GetPreparedLetter(
1707 letter_code => $params->{notice_code},
1708 branchcode => $self->branchcode,
1709 message_transport_type => $params->{transport},
1710 lang => $self->patron->lang,
1712 illrequests => $self->illrequest_id,
1713 borrowers => $self->borrowernumber,
1714 biblio => $self->biblio_id,
1715 branches => $self->branchcode,
1718 ill_bib_title => $title ? $title->value : '',
1719 ill_bib_author => $author ? $author->value : '',
1720 ill_full_metadata => $metastring,
1721 additional_text => $params->{additional_text}
1729 =head3 attach_processors
1731 Receive a Koha::Illrequest::SupplierUpdate and attach
1732 any processors we have for it
1736 sub attach_processors {
1737 my ( $self, $update ) = @_;
1739 foreach my $processor(@{$self->{processors}}) {
1741 $processor->{target_source_type} eq $update->{source_type} &&
1742 $processor->{target_source_name} eq $update->{source_name}
1744 $update->attach_processor($processor);
1749 =head3 append_to_note
1751 append_to_note("Some text");
1753 Append some text to the staff note
1757 sub append_to_note {
1758 my ($self, $text) = @_;
1759 my $current = $self->notesstaff;
1760 $text = ($current && length $current > 0) ? "$current\n\n$text" : $text;
1761 $self->notesstaff($text)->store;
1766 my $prefix = $record->id_prefix;
1768 Return the prefix appropriate for the current Illrequest as derived from the
1769 borrower and branch associated with this request's Status, and the config
1776 my $prefix = $self->getPrefix( {
1777 branch => $self->branchcode,
1779 $prefix .= "-" if ( $prefix );
1785 my $params = $illRequest->_censor($params);
1787 Return $params, modified to reflect our censorship requirements.
1792 my ( $self, $params ) = @_;
1793 my $censorship = $self->_config->censorship;
1794 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1795 if ( $params->{opac} );
1796 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1805 Overloaded I<store> method that, in addition to performing the 'store',
1806 possibly records the fact that something happened
1811 my ( $self, $attrs ) = @_;
1813 my %updated_columns = $self->_result->get_dirty_columns;
1816 if( $self->in_storage and defined $updated_columns{'borrowernumber'} and
1817 Koha::Patrons->find( $updated_columns{'borrowernumber'} ) )
1819 # borrowernumber has changed
1820 my $old_illreq = $self->get_from_storage;
1821 @holds = Koha::Holds->search( {
1822 borrowernumber => $old_illreq->borrowernumber,
1823 biblionumber => $self->biblio_id,
1824 } )->as_list if $old_illreq;
1827 my $ret = $self->SUPER::store;
1829 if ( scalar @holds ) {
1830 # move holds to the changed borrowernumber
1831 foreach my $hold ( @holds ) {
1832 $hold->borrowernumber( $updated_columns{'borrowernumber'} )->store;
1836 $attrs->{log_origin} = 'core';
1838 if ($ret && defined $attrs) {
1839 my $logger = Koha::Illrequest::Logger->new;
1840 $logger->log_maybe({
1849 =head3 requested_partners
1851 my $partners_string = $illRequest->requested_partners;
1853 Return the string representing the email addresses of the partners to
1854 whom a request has been sent
1858 sub requested_partners {
1860 return $self->_backend_capability(
1861 'get_requested_partners',
1862 { request => $self }
1868 $json = $illrequest->TO_JSON
1870 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1871 into the unblessed representation of the object.
1873 TODO: This method does nothing and is not called anywhere. However, bug 74325
1874 touches it, so keeping this for now until both this and bug 74325 are merged,
1875 at which point we can sort it out and remove it completely
1880 my ( $self, $embed ) = @_;
1882 my $object = $self->SUPER::TO_JSON();
1887 =head2 Internal methods
1889 =head3 to_api_mapping
1893 sub to_api_mapping {
1895 accessurl => 'access_url',
1896 backend => 'ill_backend_id',
1897 borrowernumber => 'patron_id',
1898 branchcode => 'library_id',
1899 completed => 'completed_date',
1900 deleted_biblio_id => undef,
1901 illrequest_id => 'ill_request_id',
1902 notesopac => 'opac_notes',
1903 notesstaff => 'staff_notes',
1904 orderid => 'ill_backend_request_id',
1905 placed => 'requested_date',
1906 price_paid => 'paid_price',
1907 replied => 'replied_date',
1908 status_alias => 'status_av',
1909 updated => 'timestamp',
1915 my $strings = $self->string_map({ [ public => 0|1 ] });
1917 Returns a map of column name to string representations. Extra information
1918 is returned depending on the column characteristics as shown below.
1920 Accepts a param hashref where the I<public> key denotes whether we want the public
1921 or staff client strings.
1927 backend => 'backendName',
1928 str => 'Status description',
1929 type => 'ill_status',
1932 category => 'ILL_STATUS_ALIAS,
1933 str => $value, # the AV description, depending on $params->{public}
1941 my ( $self, $params ) = @_;
1943 my $cache = Koha::Cache::Memory::Lite->get_instance();
1944 my $cache_key = 'ill:status_graph:' . $self->backend;
1946 my $status_graph_union = $cache->get($cache_key);
1947 unless ($status_graph_union) {
1948 $status_graph_union = $self->capabilities;
1949 $cache->set( $cache_key, $status_graph_union );
1953 ( exists $status_graph_union->{ $self->status } && defined $status_graph_union->{ $self->status }->{name} )
1954 ? $status_graph_union->{ $self->status }->{name}
1958 ( exists $status_graph_union->{ $self->status } && defined $status_graph_union->{ $self->status }->{id} )
1959 ? $status_graph_union->{ $self->status }->{id}
1964 backend => $self->backend, # the backend identifier
1965 str => $status_string, # the status description, taken from the status graph
1966 code => $status_code, # the status id, taken from the status graph
1967 type => 'ill_status', # fixed type
1971 my $status_alias = $self->statusalias;
1972 if ($status_alias) {
1973 $strings->{"status_alias"} = {
1974 category => 'ILL_STATUS_ALIAS',
1975 str => $params->{public} ? $status_alias->lib_opac : $status_alias->lib,
1976 code => $status_alias->authorised_value,
1989 return 'Illrequest';
1994 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1995 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>