1 package Koha::Illrequest;
3 # Copyright PTFS Europe 2016,2018
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use Clone qw( clone );
23 use Try::Tiny qw( catch try );
28 use Koha::Cache::Memory::Lite;
30 use Koha::DateUtils qw( dt_from_string );
31 use Koha::Exceptions::Ill;
32 use Koha::Illcomments;
33 use Koha::Illrequestattributes;
34 use Koha::AuthorisedValue;
35 use Koha::Illrequest::Logger;
37 use Koha::AuthorisedValues;
43 use C4::Circulation qw( CanBookBeIssued AddIssue );
45 use base qw(Koha::Object);
49 Koha::Illrequest - Koha Illrequest Object class
53 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
54 of related Illrequestattributes.
56 The former encapsulates the basic necessary information that any ILL requires
57 to be usable in Koha. The latter is a set of additional properties used by
60 The former subsumes the legacy "Status" object. The latter remains
61 encapsulated in the "Record" object.
65 - Anything invoking the ->status method; annotated with:
66 + # Old use of ->status !
70 =head2 Backend API Response Principles
72 All methods should return a hashref in the following format:
78 This should be set to 1 if an error was encountered.
82 The status should be a string from the list of statuses detailed below.
86 The message is a free text field that can be passed on to the end user.
90 The value returned by the method.
94 =head2 Interface Status Messages
98 =item * branch_address_incomplete
100 An interface request has determined branch address details are incomplete.
102 =item * cancel_success
104 The interface's cancel_request method was successful in cancelling the
105 Illrequest using the API.
109 The interface's cancel_request method failed to cancel the Illrequest using
114 The interface's request method returned saying that the desired item is not
115 available for request.
121 =head3 init_processors
123 $request->init_processors()
125 Initialises an empty processors arrayref
129 sub init_processors {
132 $self->{processors} = [];
135 =head3 push_processor
137 $request->push_processors(sub { ...something... });
139 Pushes a passed processor function into our processors arrayref
144 my ( $self, $processor ) = @_;
145 push @{$self->{processors}}, $processor;
150 my $statusalias = $request->statusalias;
152 Returns a request's status alias, as a Koha::AuthorisedValue instance
153 or implicit undef. This is distinct from status_alias, which only returns
154 the value in the status_alias column, this method returns the entire
155 AuthorisedValue object
161 return unless $self->status_alias;
162 # We can't know which result is the right one if there are multiple
163 # ILL_STATUS_ALIAS authorised values with the same authorised_value column value
164 # so we just use the first
165 return Koha::AuthorisedValues->search(
167 category => 'ILL_STATUS_ALIAS',
168 authorised_value => $self->SUPER::status_alias
175 =head3 illrequestattributes
179 sub illrequestattributes {
181 return Koha::Illrequestattributes->_new_from_dbic(
182 scalar $self->_result->illrequestattributes
192 return Koha::Illcomments->_new_from_dbic(
193 scalar $self->_result->illcomments
199 my $ill_comments = $req->comments;
201 Returns a I<Koha::Illcomments> resultset for the linked comments.
207 return Koha::Illcomments->_new_from_dbic(
208 scalar $self->_result->comments
218 my $logger = Koha::Illrequest::Logger->new;
219 return $logger->get_request_logs($self);
224 my $patron = $request->patron;
226 Returns the linked I<Koha::Patron> object.
233 return Koha::Patron->_new_from_dbic( scalar $self->_result->patron );
238 my $library = $request->library;
240 Returns the linked I<Koha::Library> object.
247 return Koha::Library->_new_from_dbic( scalar $self->_result->library );
250 =head3 ill_extended_attributes
252 my $ill_extended_attributes = $request->ill_extended_attributes;
254 Returns the linked I<Koha::Illrequestattributes> resultset object.
258 sub ill_extended_attributes {
261 return Koha::Illrequestattributes->_new_from_dbic(
262 scalar $self->_result->ill_extended_attributes
268 $Illrequest->status_alias(143);
270 Overloaded getter/setter for status_alias,
271 that only returns authorised values from the
272 correct category and records the fact that the status has changed
277 my ($self, $new_status_alias) = @_;
279 my $current_status_alias = $self->SUPER::status_alias;
281 if ($new_status_alias) {
282 # Keep a record of the previous status before we change it,
284 $self->{previous_status} = $current_status_alias ?
285 $current_status_alias :
286 scalar $self->status;
287 # This is hackery to enable us to undefine
288 # status_alias, since we need to have an overloaded
289 # status_alias method to get us around the problem described
291 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
292 # We need a way of accepting implied undef, so we can nullify
293 # the status_alias column, when called from $self->status
294 my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
295 my $ret = $self->SUPER::status_alias($val);
296 my $val_to_log = $val ? $new_status_alias : scalar $self->status;
298 my $logger = Koha::Illrequest::Logger->new;
299 $logger->log_status_change({
304 delete $self->{previous_status};
308 # We can't know which result is the right one if there are multiple
309 # ILL_STATUS_ALIAS authorised values with the same authorised_value column value
310 # so we just use the first
311 my $alias = Koha::AuthorisedValues->search(
313 category => 'ILL_STATUS_ALIAS',
314 authorised_value => $self->SUPER::status_alias
321 return $alias->authorised_value;
329 $Illrequest->status('CANREQ');
331 Overloaded getter/setter for request status,
332 also nullifies status_alias and records the fact that the status has changed
333 and sends a notice if appropriate
338 my ( $self, $new_status) = @_;
340 my $current_status = $self->SUPER::status;
341 my $current_status_alias = $self->SUPER::status_alias;
344 # Keep a record of the previous status before we change it,
346 $self->{previous_status} = $current_status_alias ?
347 $current_status_alias :
349 my $ret = $self->SUPER::status($new_status)->store;
350 if ($current_status_alias) {
351 # This is hackery to enable us to undefine
352 # status_alias, since we need to have an overloaded
353 # status_alias method to get us around the problem described
355 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
356 # We need a way of passing implied undef to nullify status_alias
357 # so we pass -1, which is special cased in the overloaded setter
358 $self->status_alias("-1");
360 my $logger = Koha::Illrequest::Logger->new;
361 $logger->log_status_change({
366 delete $self->{previous_status};
367 # If status has changed to cancellation requested, send a notice
368 if ($new_status eq 'CANCREQ') {
369 $self->send_staff_notice('ILL_REQUEST_CANCEL');
373 return $current_status;
379 Require "Base.pm" from the relevant ILL backend.
384 my ( $self, $backend_id ) = @_;
386 my @raw = qw/Koha Illbackends/; # Base Path
388 my $backend_name = $backend_id || $self->backend;
390 unless ( defined $backend_name && $backend_name ne '' ) {
391 Koha::Exceptions::Ill::InvalidBackendId->throw(
392 "An invalid backend ID was requested ('')");
395 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
396 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
398 $self->{_my_backend} = $backend_class->new({
399 config => $self->_config,
400 logger => Koha::Illrequest::Logger->new
408 my $backend = $abstract->_backend($new_backend);
409 my $backend = $abstract->_backend;
411 Getter/Setter for our API object.
416 my ( $self, $backend ) = @_;
417 $self->{_my_backend} = $backend if ( $backend );
418 # Dynamically load our backend object, as late as possible.
419 $self->load_backend unless ( $self->{_my_backend} );
420 return $self->{_my_backend};
423 =head3 _backend_capability
425 my $backend_capability_result = $self->_backend_capability($name, $args);
427 This is a helper method to invoke optional capabilities in the backend. If
428 the capability named by $name is not supported, return 0, else invoke it,
429 passing $args along with the invocation, and return its return value.
431 NOTE: this module suffers from a confusion in termninology:
433 in _backend_capability, the notion of capability refers to an optional feature
434 that is implemented in core, but might not be supported by a given backend.
436 in capabilities & custom_capability, capability refers to entries in the
437 status_graph (after union between backend and core).
439 The easiest way to fix this would be to fix the terminology in
440 capabilities & custom_capability and their callers.
444 sub _backend_capability {
445 my ( $self, $name, $args ) = @_;
447 # See if capability is defined in backend
449 $capability = $self->_backend->capabilities($name);
455 if ( $capability && ref($capability) eq 'CODE' ) {
456 return &{$capability}($args);
464 my $config = $abstract->_config($config);
465 my $config = $abstract->_config;
467 Getter/Setter for our config object.
472 my ( $self, $config ) = @_;
473 $self->{_my_config} = $config if ( $config );
474 # Load our config object, as late as possible.
475 unless ( $self->{_my_config} ) {
476 $self->{_my_config} = Koha::Illrequest::Config->new;
478 return $self->{_my_config};
487 return $self->_backend->metadata($self);
490 =head3 _core_status_graph
492 my $core_status_graph = $illrequest->_core_status_graph;
494 Returns ILL module's default status graph. A status graph defines the list of
495 available actions at any stage in the ILL workflow. This is for instance used
496 by the perl script & template to generate the correct buttons to display to
497 the end user at any given point.
501 sub _core_status_graph {
505 prev_actions => [ ], # Actions containing buttons
506 # leading to this status
507 id => 'NEW', # ID of this status
508 name => 'New request', # UI name of this status
509 ui_method_name => 'New request', # UI name of method leading
511 method => 'create', # method to this status
512 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
513 # requests with this status
514 ui_method_icon => 'fa-plus', # UI Style class
517 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
520 ui_method_name => 'Confirm request',
522 next_actions => [ 'REQREV', 'COMP', 'CHK' ],
523 ui_method_icon => 'fa-check',
526 prev_actions => [ 'NEW', 'REQREV' ],
528 name => 'Requested from partners',
529 ui_method_name => 'Place request with partners',
530 method => 'generic_confirm',
531 next_actions => [ 'COMP', 'CHK' ],
532 ui_method_icon => 'fa-send-o',
535 prev_actions => [ 'REQ' ],
537 name => 'Request reverted',
538 ui_method_name => 'Revert Request',
540 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
541 ui_method_icon => 'fa-times',
546 name => 'Queued request',
549 next_actions => [ 'REQ', 'KILL' ],
553 prev_actions => [ 'NEW' ],
555 name => 'Cancellation requested',
558 next_actions => [ 'KILL', 'REQ' ],
562 prev_actions => [ 'REQ' ],
565 ui_method_name => 'Mark completed',
566 method => 'mark_completed',
567 next_actions => [ 'CHK' ],
568 ui_method_icon => 'fa-check',
571 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
574 ui_method_name => 'Delete request',
577 ui_method_icon => 'fa-trash',
580 prev_actions => [ 'REQ', 'GENREQ', 'COMP' ],
582 name => 'Checked out',
583 ui_method_name => 'Check out',
584 needs_prefs => [ 'CirculateILL' ],
585 needs_perms => [ 'user_circulate_circulate_remaining_permissions' ],
586 # An array of functions that all must return true
587 needs_all => [ sub { my $r = shift; return $r->biblio; } ],
588 method => 'check_out',
590 ui_method_icon => 'fa-upload',
593 prev_actions => [ 'CHK' ],
595 name => 'Returned to library',
596 ui_method_name => 'Check in',
597 method => 'check_in',
598 next_actions => [ 'COMP' ],
599 ui_method_icon => 'fa-download',
604 =head3 _status_graph_union
606 my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
608 Return a new status_graph, the result of merging $origin & new_graph. This is
609 operation is a union over the sets defied by the two graphs.
611 Each entry in $new_graph is added to $origin. We do not provide a syntax for
612 'subtraction' of entries from $origin.
614 Whilst it is not intended that this works, you can override entries in $origin
615 with entries with the same key in $new_graph. This can lead to problematic
616 behaviour when $new_graph adds an entry, which modifies a dependent entry in
617 $origin, only for the entry in $origin to be replaced later with a new entry
620 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
621 i.e. each of the graphs need to be correct at the outset of the operation.
625 sub _status_graph_union {
626 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
627 # Create new status graph with:
628 # - all core_status_graph
629 # - for-each each backend_status_graph
630 # + add to new status graph
631 # + for each core prev_action:
632 # * locate core_status
633 # * update next_actions with additional next action.
634 # + for each core next_action:
635 # * locate core_status
636 # * update prev_actions with additional prev action
638 my @core_status_ids = keys %{$core_status_graph};
639 my $status_graph = clone($core_status_graph);
641 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
642 my $backend_status = $backend_status_graph->{$backend_status_key};
643 # Add to new status graph
644 $status_graph->{$backend_status_key} = $backend_status;
645 # Update all core methods' next_actions.
646 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
647 if ( grep { $prev_action eq $_ } @core_status_ids ) {
649 @{$status_graph->{$prev_action}->{next_actions}};
650 push @next_actions, $backend_status_key
651 if (!grep(/^$backend_status_key$/, @next_actions));
652 $status_graph->{$prev_action}->{next_actions}
656 # Update all core methods' prev_actions
657 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
658 if ( grep { $next_action eq $_ } @core_status_ids ) {
660 @{$status_graph->{$next_action}->{prev_actions}};
661 push @prev_actions, $backend_status_key
662 if (!grep(/^$backend_status_key$/, @prev_actions));
663 $status_graph->{$next_action}->{prev_actions}
669 return $status_graph;
676 my $capabilities = $illrequest->capabilities;
678 Return a hashref mapping methods to operation names supported by the queried
681 Example return value:
683 { create => "Create Request", confirm => "Progress Request" }
685 NOTE: this module suffers from a confusion in termninology:
687 in _backend_capability, the notion of capability refers to an optional feature
688 that is implemented in core, but might not be supported by a given backend.
690 in capabilities & custom_capability, capability refers to entries in the
691 status_graph (after union between backend and core).
693 The easiest way to fix this would be to fix the terminology in
694 capabilities & custom_capability and their callers.
699 my ( $self, $status ) = @_;
700 # Generate up to date status_graph
701 my $status_graph = $self->_status_graph_union(
702 $self->_core_status_graph,
703 $self->_backend->status_graph({
708 # Extract available actions from graph.
709 return $status_graph->{$status} if $status;
710 # Or return entire graph.
711 return $status_graph;
714 =head3 custom_capability
716 Return the result of invoking $CANDIDATE on this request's backend with
717 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
719 NOTE: this module suffers from a confusion in termninology:
721 in _backend_capability, the notion of capability refers to an optional feature
722 that is implemented in core, but might not be supported by a given backend.
724 in capabilities & custom_capability, capability refers to entries in the
725 status_graph (after union between backend and core).
727 The easiest way to fix this would be to fix the terminology in
728 capabilities & custom_capability and their callers.
732 sub custom_capability {
733 my ( $self, $candidate, $params ) = @_;
734 foreach my $capability ( values %{$self->capabilities} ) {
735 if ( $candidate eq $capability->{method} ) {
737 $self->_backend->$candidate({
741 return $self->expandTemplate($response);
747 =head3 available_backends
749 Return a list of available backends.
753 sub available_backends {
754 my ( $self, $reduced ) = @_;
755 my $backends = $self->_config->available_backends($reduced);
759 =head3 available_actions
761 Return a list of available actions.
765 sub available_actions {
767 my $current_action = $self->capabilities($self->status);
768 my @available_actions = map { $self->capabilities($_) }
769 @{$current_action->{next_actions}};
770 return \@available_actions;
773 =head3 mark_completed
775 Mark a request as completed (status = COMP).
781 $self->status('COMP')->store;
782 $self->completed(dt_from_string())->store;
787 method => 'mark_completed',
793 =head2 backend_illview
795 View and manage an ILL request
799 sub backend_illview {
800 my ( $self, $params ) = @_;
802 my $response = $self->_backend_capability('illview',{
806 return $self->expandTemplate($response) if $response;
810 =head2 backend_migrate
812 Migrate a request from one backend to another.
816 sub backend_migrate {
817 my ( $self, $params ) = @_;
818 # Set the request's backend to be the destination backend
819 $self->load_backend($params->{backend});
820 my $response = $self->_backend_capability('migrate',{
824 return $self->expandTemplate($response) if $response;
828 =head2 backend_confirm
830 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
836 =item * accessurl, cost (if available).
842 sub backend_confirm {
843 my ( $self, $params ) = @_;
845 my $response = $self->_backend->confirm({
849 return $self->expandTemplate($response);
852 =head3 backend_update_status
856 sub backend_update_status {
857 my ( $self, $params ) = @_;
858 return $self->expandTemplate($self->_backend->update_status($params));
861 =head3 backend_cancel
863 my $ILLResponse = $illRequest->backend_cancel;
865 The standard interface method allowing for request cancellation.
870 my ( $self, $params ) = @_;
872 my $result = $self->_backend->cancel({
877 return $self->expandTemplate($result);
882 my $renew_response = $illRequest->backend_renew;
884 The standard interface method allowing for request renewal queries.
890 return $self->expandTemplate(
891 $self->_backend->renew({
897 =head3 backend_create
899 my $create_response = $abstractILL->backend_create($params);
901 Return an array of Record objects created by querying our backend with
904 In the context of the other ILL methods, this is a special method: we only
905 pass it $params, as it does not yet have any other data associated with it.
910 my ( $self, $params ) = @_;
912 # Establish whether we need to do a generic copyright clearance.
913 if ($params->{opac}) {
914 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
915 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
921 stage => 'copyrightclearance',
924 backend => $self->_backend->name
927 } elsif ( defined $params->{stage}
928 && $params->{stage} eq 'copyrightclearance' ) {
929 $params->{stage} = 'init';
932 # First perform API action, then...
937 my $result = $self->_backend->create($args);
939 # ... simple case: we're not at 'commit' stage.
940 my $stage = $result->{stage};
941 return $self->expandTemplate($result)
942 unless ( 'commit' eq $stage );
944 # ... complex case: commit!
946 # Do we still have space for an ILL or should we queue?
947 my $permitted = $self->check_limits(
948 { patron => $self->patron }, { librarycode => $self->branchcode }
951 # Now augment our committed request.
953 $result->{permitted} = $permitted; # Queue request?
957 # ...Updating status!
958 $self->status('QUEUED')->store unless ( $permitted );
960 ## Handle Unmediated ILLs
962 # For the unmediated workflow we only need to delegate to our backend. If
963 # that backend supports unmediateld_ill, it will do its thing and return a
964 # proper response. If it doesn't then _backend_capability returns 0, so
965 # we keep the current result.
966 if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
967 my $unmediated_result = $self->_backend_capability(
971 $result = $unmediated_result if $unmediated_result;
974 return $self->expandTemplate($result);
977 =head3 backend_get_update
979 my $update = backend_get_update($request);
981 Given a request, returns an update in a prescribed
982 format that can then be passed to update parsers
986 sub backend_get_update {
987 my ( $self, $options ) = @_;
989 my $response = $self->_backend_capability(
990 'get_supplier_update',
999 =head3 expandTemplate
1001 my $params = $abstract->expandTemplate($params);
1003 Return a version of $PARAMS augmented with our required template path.
1007 sub expandTemplate {
1008 my ( $self, $params ) = @_;
1009 my $backend = $self->_backend->name;
1010 # Generate path to file to load
1011 my $backend_dir = $self->_config->backend_dir;
1012 my $backend_tmpl = join "/", $backend_dir, $backend;
1013 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
1014 ( $params->{method}//q{} ) . ".inc";
1015 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
1016 ( $params->{method}//q{} ) . ".inc";
1018 $params->{template} = $intra_tmpl;
1019 $params->{opac_template} = $opac_tmpl;
1023 #### Abstract Imports
1027 my $limit_rules = $abstract->getLimits( {
1028 type => 'brw_cat' | 'branch',
1032 Return the ILL limit rules for the supplied combination of type / value.
1034 As the config may have no rules for this particular type / value combination,
1035 or for the default, we must define fall-back values here.
1040 my ( $self, $params ) = @_;
1041 my $limits = $self->_config->getLimitRules($params->{type});
1043 if ( defined $params->{value}
1044 && defined $limits->{$params->{value}} ) {
1045 return $limits->{$params->{value}};
1048 return $limits->{default} || { count => -1, method => 'active' };
1054 my $prefix = $abstract->getPrefix( {
1055 branch => $branch_code
1058 Return the ILL prefix as defined by our $params: either per borrower category,
1059 per branch or the default.
1064 my ( $self, $params ) = @_;
1065 my $brn_prefixes = $self->_config->getPrefixes();
1066 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
1071 my $type = $abstract->get_type();
1073 Return a string representing the material type of this request or undef
1079 my $attr = $self->illrequestattributes->find({ type => 'type'});
1081 return $attr->value;
1084 #### Illrequests Imports
1088 my $ok = $illRequests->check_limits( {
1089 borrower => $borrower,
1090 branchcode => 'branchcode' | undef,
1093 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
1094 see whether we are still able to place ILLs.
1096 LimitRules are derived from koha-conf.xml:
1097 + default limit counts, and counting method
1098 + branch specific limit counts & counting method
1099 + borrower category specific limit counts & counting method
1100 + err on the side of caution: a counting fail will cause fail, even if
1101 the other counts passes.
1106 my ( $self, $params ) = @_;
1107 my $patron = $params->{patron};
1108 my $branchcode = $params->{librarycode} || $patron->branchcode;
1110 # Establish maximum number of allowed requests
1111 my ( $branch_rules, $brw_rules ) = (
1114 value => $branchcode
1118 value => $patron->categorycode,
1121 my ( $branch_limit, $brw_limit )
1122 = ( $branch_rules->{count}, $brw_rules->{count} );
1123 # Establish currently existing requests
1124 my ( $branch_count, $brw_count ) = (
1125 $self->_limit_counter(
1126 $branch_rules->{method}, { branchcode => $branchcode }
1128 $self->_limit_counter(
1129 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
1133 # Compare and return
1134 # A limit of -1 means no limit exists.
1135 # We return blocked if either branch limit or brw limit is reached.
1136 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
1137 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
1144 sub _limit_counter {
1145 my ( $self, $method, $target ) = @_;
1147 # Establish parameters of counts
1149 if ($method && $method eq 'annual') {
1150 $resultset = Koha::Illrequests->search({
1153 \"YEAR(placed) = YEAR(NOW())"
1156 } else { # assume 'active'
1157 # XXX: This status list is ugly. There should be a method in config
1159 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1160 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1164 return $resultset->count;
1167 =head3 requires_moderation
1169 my $status = $illRequest->requires_moderation;
1171 Return the name of the status if moderation by staff is required; or 0
1176 sub requires_moderation {
1178 my $require_moderation = {
1179 'CANCREQ' => 'CANCREQ',
1181 return $require_moderation->{$self->status};
1186 my $biblio = $request->biblio;
1188 For a given request, return the biblio associated with it,
1189 or undef if none exists
1195 my $biblio_rs = $self->_result->biblio;
1196 return unless $biblio_rs;
1197 return Koha::Biblio->_new_from_dbic($biblio_rs);
1202 my $stage_summary = $request->check_out;
1204 Handle the check_out method. The first stage involves gathering the required
1205 data from the user via a form, the second stage creates an item and tries to
1206 issue it to the patron. If successful, it notifies the patron, then it
1207 returns a summary of how things went
1212 my ( $self, $params ) = @_;
1214 # Objects required by the template
1215 my $itemtypes = Koha::ItemTypes->search(
1217 { order_by => ['description'] }
1219 my $libraries = Koha::Libraries->search(
1221 { order_by => ['branchcode'] }
1223 my $biblio = $self->biblio;
1225 # Find all statistical patrons
1226 my $statistical_patrons = Koha::Patrons->search(
1227 { 'category_type' => 'x' },
1228 { join => { 'categorycode' => 'borrowers' } }
1231 if (!$params->{stage} || $params->{stage} eq 'init') {
1232 # Present a form to gather the required data
1234 # We may be viewing this page having previously tried to issue
1235 # the item (in which case, we may already have created an item)
1236 # so we pass the biblio for this request
1238 method => 'check_out',
1241 itemtypes => $itemtypes,
1242 libraries => $libraries,
1243 statistical => $statistical_patrons,
1247 } elsif ($params->{stage} eq 'form') {
1248 # Validate what we've got and return with an error if we fail
1250 if (!$params->{item_type} || length $params->{item_type} == 0) {
1251 $errors->{item_type} = 1;
1253 if ($params->{inhouse} && length $params->{inhouse} > 0) {
1254 my $patron_count = Koha::Patrons->search({
1255 cardnumber => $params->{inhouse}
1257 if ($patron_count != 1) {
1258 $errors->{inhouse} = 1;
1262 # Check we don't have more than one item for this bib,
1263 # if we do, something very odd is going on
1264 # Having 1 is OK, it means we're likely trying to issue
1265 # following a previously failed attempt, the item exists
1267 my @items = $biblio->items->as_list;
1268 my $item_count = scalar @items;
1269 if ($item_count > 1) {
1270 $errors->{itemcount} = 1;
1273 # Failed validation, go back to the form
1276 method => 'check_out',
1280 statistical => $statistical_patrons,
1281 itemtypes => $itemtypes,
1282 libraries => $libraries,
1291 # Create an item if one doesn't already exist,
1292 # if one does, use that
1294 if ($item_count == 0) {
1296 biblionumber => $self->biblio_id,
1297 homebranch => $params->{branchcode},
1298 holdingbranch => $params->{branchcode},
1299 location => $params->{branchcode},
1300 itype => $params->{item_type},
1301 barcode => 'ILL-' . $self->illrequest_id
1304 my $item = Koha::Item->new($item_hash)->store;
1305 $itemnumber = $item->itemnumber;
1308 $itemnumber = $items[0]->itemnumber;
1310 # Check we have an item before going forward
1313 method => 'check_out',
1317 itemtypes => $itemtypes,
1318 libraries => $libraries,
1319 statistical => $statistical_patrons,
1320 errors => { item_creation => 1 }
1327 # Gather what we need
1328 my $target_item = Koha::Items->find( $itemnumber );
1329 # Determine who we're issuing to
1330 my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
1331 Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
1336 scalar $target_item->barcode
1338 if ($params->{duedate} && length $params->{duedate} > 0) {
1339 push @issue_args, dt_from_string($params->{duedate});
1341 # Check if we can check out
1342 my ( $error, $confirm, $alerts, $messages ) =
1343 C4::Circulation::CanBookBeIssued(@issue_args);
1345 # If we got anything back saying we can't check out,
1346 # return it to the template
1348 if ( $error && %{$error} ) { $problems->{error} = $error };
1349 if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
1350 if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
1351 if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
1355 method => 'check_out',
1359 itemtypes => $itemtypes,
1360 libraries => $libraries,
1361 statistical => $statistical_patrons,
1364 check_out_errors => $problems
1369 # We can allegedly check out, so make it so
1370 # For some reason, AddIssue requires an unblessed Patron
1371 $issue_args[0] = $patron->unblessed;
1372 my $issue = C4::Circulation::AddIssue(@issue_args);
1375 # Update the request status
1376 $self->status('CHK')->store;
1378 method => 'check_out',
1379 stage => 'done_check_out',
1388 method => 'check_out',
1392 itemtypes => $itemtypes,
1393 libraries => $libraries,
1394 errors => { item_check_out => 1 }
1402 =head3 generic_confirm
1404 my $stage_summary = $illRequest->generic_confirm;
1406 Handle the generic_confirm extended method. The first stage involves creating
1407 a template email for the end user to edit in the browser. The second stage
1408 attempts to submit the email.
1412 sub generic_confirm {
1413 my ( $self, $params ) = @_;
1414 my $branch = Koha::Libraries->find($params->{current_branchcode})
1415 || die "Invalid current branchcode. Are you logged in as the database user?";
1416 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
1417 # Get the message body from the notice definition
1418 my $letter = $self->get_notice({
1419 notice_code => 'ILL_PARTNER_REQ',
1420 transport => 'email'
1423 my $partners = Koha::Patrons->search({
1424 categorycode => $self->_config->partner_code
1430 method => 'generic_confirm',
1434 subject => $letter->{title},
1435 body => $letter->{content}
1437 partners => $partners,
1441 } elsif ( 'draft' eq $params->{stage} ) {
1442 # Create the to header
1443 my $to = $params->{partners};
1444 if ( defined $to ) {
1445 $to =~ s/^\x00//; # Strip leading NULLs
1447 Koha::Exceptions::Ill::NoTargetEmail->throw(
1448 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
1451 # Take the null delimited string that we receive and create
1452 # an array of associated patron objects
1453 my @to_patrons = map {
1454 Koha::Patrons->find({ borrowernumber => $_ })
1455 } split(/\x00/, $to);
1457 # Create the from, replyto and sender headers
1458 my $from = $branch->from_email_address;
1459 my $replyto = $branch->inbound_ill_address;
1460 Koha::Exceptions::Ill::NoLibraryEmail->throw(
1461 "Your library has no usable email address. Please set it.")
1464 # So we get a notice hashref, then substitute the possibly
1465 # modified title and body from the draft stage
1466 my $letter = $self->get_notice({
1467 notice_code => 'ILL_PARTNER_REQ',
1468 transport => 'email'
1470 $letter->{title} = $params->{subject};
1471 $letter->{content} = $params->{body};
1475 # Keep track of who received this notice
1477 # Iterate our array of recipient patron objects
1478 foreach my $patron(@to_patrons) {
1479 # Create the params we pass to the notice
1482 borrowernumber => $patron->borrowernumber,
1483 message_transport_type => 'email',
1484 to_address => $patron->email,
1485 from_address => $from,
1486 reply_address => $replyto
1488 my $result = C4::Letters::EnqueueLetter($params);
1490 push @queued, $patron->email;
1494 # If all notices were queued successfully,
1496 if (scalar @queued == scalar @to_patrons) {
1497 $self->status("GENREQ")->store;
1498 $self->_backend_capability(
1499 'set_requested_partners',
1502 to => join("; ", @queued)
1509 method => 'generic_confirm',
1518 status => 'email_failed',
1519 message => 'Email queueing failed',
1520 method => 'generic_confirm',
1524 die "Unknown stage, should not have happened."
1528 =head3 send_patron_notice
1530 my $result = $request->send_patron_notice($notice_code);
1532 Send a specified notice regarding this request to a patron
1536 sub send_patron_notice {
1537 my ( $self, $notice_code, $additional_text ) = @_;
1539 # We need a notice code
1540 if (!$notice_code) {
1542 error => 'notice_no_type'
1546 # Map from the notice code to the messaging preference
1547 my %message_name = (
1548 ILL_PICKUP_READY => 'Ill_ready',
1549 ILL_REQUEST_UNAVAIL => 'Ill_unavailable',
1550 ILL_REQUEST_UPDATE => 'Ill_update'
1553 # Get the patron's messaging preferences
1554 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
1555 borrowernumber => $self->borrowernumber,
1556 message_name => $message_name{$notice_code}
1558 my @transports = keys %{ $borrower_preferences->{transports} };
1560 # Notice should come from the library where the request was placed,
1561 # not the patrons home library
1562 my $branch = Koha::Libraries->find($self->branchcode);
1563 my $from_address = $branch->from_email_address;
1564 my $reply_address = $branch->inbound_ill_address;
1566 # Send the notice to the patron via the chosen transport methods
1567 # and record the results
1570 for my $transport (@transports) {
1571 my $letter = $self->get_notice({
1572 notice_code => $notice_code,
1573 transport => $transport,
1574 additional_text => $additional_text
1577 my $result = C4::Letters::EnqueueLetter({
1579 borrowernumber => $self->borrowernumber,
1580 message_transport_type => $transport,
1581 from_address => $from_address,
1582 reply_address => $reply_address
1585 push @success, $transport;
1587 push @fail, $transport;
1590 push @fail, $transport;
1593 if (scalar @success > 0) {
1594 my $logger = Koha::Illrequest::Logger->new;
1595 $logger->log_patron_notice({
1597 notice_code => $notice_code
1602 success => \@success,
1608 =head3 send_staff_notice
1610 my $result = $request->send_staff_notice($notice_code);
1612 Send a specified notice regarding this request to staff
1616 sub send_staff_notice {
1617 my ( $self, $notice_code ) = @_;
1619 # We need a notice code
1620 if (!$notice_code) {
1622 error => 'notice_no_type'
1626 # Get the staff notices that have been assigned for sending in
1628 my $staff_to_send = C4::Context->preference('ILLSendStaffNotices') // q{};
1630 # If it hasn't been enabled in the syspref, we don't want to send it
1631 if ($staff_to_send !~ /\b$notice_code\b/) {
1633 error => 'notice_not_enabled'
1637 my $letter = $self->get_notice({
1638 notice_code => $notice_code,
1639 transport => 'email'
1642 # Try and get an address to which to send staff notices
1643 my $branch = Koha::Libraries->find($self->branchcode);
1644 my $to_address = $branch->inbound_ill_address;
1645 my $from_address = $branch->inbound_ill_address;
1649 borrowernumber => $self->borrowernumber,
1650 message_transport_type => 'email',
1651 from_address => $from_address
1655 $params->{to_address} = $to_address;
1658 error => 'notice_no_create'
1663 C4::Letters::EnqueueLetter($params)
1664 or warn "can't enqueue letter $letter";
1666 success => 'notice_queued'
1670 error => 'notice_no_create'
1677 my $notice = $request->get_notice($params);
1679 Return a compiled notice hashref for the passed notice code
1685 my ( $self, $params ) = @_;
1687 my $title = $self->illrequestattributes->find(
1690 my $author = $self->illrequestattributes->find(
1691 { type => 'author' }
1693 my $metahash = $self->metadata;
1695 foreach my $key (sort { lc $a cmp lc $b } keys %{$metahash}) {
1696 my $value = $metahash->{$key};
1697 push @metaarray, "- $key: $value" if $value;
1699 my $metastring = join("\n", @metaarray);
1700 my $letter = C4::Letters::GetPreparedLetter(
1702 letter_code => $params->{notice_code},
1703 branchcode => $self->branchcode,
1704 message_transport_type => $params->{transport},
1705 lang => $self->patron->lang,
1707 illrequests => $self->illrequest_id,
1708 borrowers => $self->borrowernumber,
1709 biblio => $self->biblio_id,
1710 branches => $self->branchcode,
1713 ill_bib_title => $title ? $title->value : '',
1714 ill_bib_author => $author ? $author->value : '',
1715 ill_full_metadata => $metastring,
1716 additional_text => $params->{additional_text}
1724 =head3 attach_processors
1726 Receive a Koha::Illrequest::SupplierUpdate and attach
1727 any processors we have for it
1731 sub attach_processors {
1732 my ( $self, $update ) = @_;
1734 foreach my $processor(@{$self->{processors}}) {
1736 $processor->{target_source_type} eq $update->{source_type} &&
1737 $processor->{target_source_name} eq $update->{source_name}
1739 $update->attach_processor($processor);
1744 =head3 append_to_note
1746 append_to_note("Some text");
1748 Append some text to the staff note
1752 sub append_to_note {
1753 my ($self, $text) = @_;
1754 my $current = $self->notesstaff;
1755 $text = ($current && length $current > 0) ? "$current\n\n$text" : $text;
1756 $self->notesstaff($text)->store;
1761 my $prefix = $record->id_prefix;
1763 Return the prefix appropriate for the current Illrequest as derived from the
1764 borrower and branch associated with this request's Status, and the config
1771 my $prefix = $self->getPrefix( {
1772 branch => $self->branchcode,
1774 $prefix .= "-" if ( $prefix );
1780 my $params = $illRequest->_censor($params);
1782 Return $params, modified to reflect our censorship requirements.
1787 my ( $self, $params ) = @_;
1788 my $censorship = $self->_config->censorship;
1789 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1790 if ( $params->{opac} );
1791 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1800 Overloaded I<store> method that, in addition to performing the 'store',
1801 possibly records the fact that something happened
1806 my ( $self, $attrs ) = @_;
1808 my %updated_columns = $self->_result->get_dirty_columns;
1811 if( $self->in_storage and defined $updated_columns{'borrowernumber'} and
1812 Koha::Patrons->find( $updated_columns{'borrowernumber'} ) )
1814 # borrowernumber has changed
1815 my $old_illreq = $self->get_from_storage;
1816 @holds = Koha::Holds->search( {
1817 borrowernumber => $old_illreq->borrowernumber,
1818 biblionumber => $self->biblio_id,
1819 } )->as_list if $old_illreq;
1822 my $ret = $self->SUPER::store;
1824 if ( scalar @holds ) {
1825 # move holds to the changed borrowernumber
1826 foreach my $hold ( @holds ) {
1827 $hold->borrowernumber( $updated_columns{'borrowernumber'} )->store;
1831 $attrs->{log_origin} = 'core';
1833 if ($ret && defined $attrs) {
1834 my $logger = Koha::Illrequest::Logger->new;
1835 $logger->log_maybe({
1844 =head3 requested_partners
1846 my $partners_string = $illRequest->requested_partners;
1848 Return the string representing the email addresses of the partners to
1849 whom a request has been sent
1853 sub requested_partners {
1855 return $self->_backend_capability(
1856 'get_requested_partners',
1857 { request => $self }
1863 $json = $illrequest->TO_JSON
1865 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1866 into the unblessed representation of the object.
1868 TODO: This method does nothing and is not called anywhere. However, bug 74325
1869 touches it, so keeping this for now until both this and bug 74325 are merged,
1870 at which point we can sort it out and remove it completely
1875 my ( $self, $embed ) = @_;
1877 my $object = $self->SUPER::TO_JSON();
1882 =head2 Internal methods
1884 =head3 to_api_mapping
1888 sub to_api_mapping {
1890 illrequest_id => 'ill_request_id',
1891 borrowernumber => 'patron_id',
1892 branchcode => 'library_id',
1893 status_alias => 'status_av',
1894 placed => 'requested_date',
1895 replied => 'replied_date',
1896 updated => 'timestamp',
1897 completed => 'completed_date',
1898 accessurl => 'access_url',
1899 price_paid => 'paid_price',
1900 notesopac => 'opac_notes',
1901 notesstaff => 'staff_notes',
1902 orderid => 'ill_backend_request_id',
1903 backend => 'ill_backend_id',
1909 my $strings = $self->string_map({ [ public => 0|1 ] });
1911 Returns a map of column name to string representations. Extra information
1912 is returned depending on the column characteristics as shown below.
1914 Accepts a param hashref where the I<public> key denotes whether we want the public
1915 or staff client strings.
1921 backend => 'backendName',
1922 str => 'Status description',
1923 type => 'ill_status',
1926 category => 'ILL_STATUS_ALIAS,
1927 str => $value, # the AV description, depending on $params->{public}
1935 my ( $self, $params ) = @_;
1937 my $cache = Koha::Cache::Memory::Lite->get_instance();
1938 my $cache_key = 'ill:status_graph:' . $self->backend;
1940 my $status_graph_union = $cache->get($cache_key);
1941 unless ($status_graph_union) {
1942 $status_graph_union = $self->capabilities;
1943 $cache->set( $cache_key, $status_graph_union );
1947 ( exists $status_graph_union->{ $self->status } && defined $status_graph_union->{ $self->status }->{name} )
1948 ? $status_graph_union->{ $self->status }->{name}
1952 ( exists $status_graph_union->{ $self->status } && defined $status_graph_union->{ $self->status }->{id} )
1953 ? $status_graph_union->{ $self->status }->{id}
1958 backend => $self->backend, # the backend identifier
1959 str => $status_string, # the status description, taken from the status graph
1960 code => $status_code, # the status id, taken from the status graph
1961 type => 'ill_status', # fixed type
1965 my $status_alias = $self->statusalias;
1966 if ($status_alias) {
1967 $strings->{"status_alias"} = {
1968 category => 'ILL_STATUS_ALIAS',
1969 str => $params->{public} ? $status_alias->lib_opac : $status_alias->lib,
1970 code => $status_alias->authorised_value,
1983 return 'Illrequest';
1988 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1989 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>