1 package Koha
::Illrequest
;
3 # Copyright PTFS Europe 2016
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 3 of the License, or (at your option) any later
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 # You should have received a copy of the GNU General Public License along with
18 # Koha; if not, write to the Free Software Foundation, Inc., 51 Franklin
19 # Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 use File
::Basename
qw( basename );
25 use Encode
qw( encode );
31 use Koha
::Exceptions
::Ill
;
32 use Koha
::Illcomments
;
33 use Koha
::Illrequestattributes
;
34 use Koha
::AuthorisedValue
;
37 use base
qw(Koha::Object);
41 Koha::Illrequest - Koha Illrequest Object class
45 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
46 of related Illrequestattributes.
48 The former encapsulates the basic necessary information that any ILL requires
49 to be usable in Koha. The latter is a set of additional properties used by
52 The former subsumes the legacy "Status" object. The latter remains
53 encapsulated in the "Record" object.
57 - Anything invoking the ->status method; annotated with:
58 + # Old use of ->status !
62 =head2 Backend API Response Principles
64 All methods should return a hashref in the following format:
70 This should be set to 1 if an error was encountered.
74 The status should be a string from the list of statuses detailed below.
78 The message is a free text field that can be passed on to the end user.
82 The value returned by the method.
86 =head2 Interface Status Messages
90 =item * branch_address_incomplete
92 An interface request has determined branch address details are incomplete.
94 =item * cancel_success
96 The interface's cancel_request method was successful in cancelling the
97 Illrequest using the API.
101 The interface's cancel_request method failed to cancel the Illrequest using
106 The interface's request method returned saying that the desired item is not
107 available for request.
115 my $statusalias = $request->statusalias;
117 Return a request's status alias, if one is defined, otherwise
118 return implicit undef
124 return unless $self->status_alias;
125 return Koha
::AuthorisedValue
->_new_from_dbic(
126 scalar $self->_result->status_alias
130 =head3 illrequestattributes
134 sub illrequestattributes
{
136 return Koha
::Illrequestattributes
->_new_from_dbic(
137 scalar $self->_result->illrequestattributes
147 return Koha
::Illcomments
->_new_from_dbic(
148 scalar $self->_result->illcomments
158 return Koha
::Patron
->_new_from_dbic(
159 scalar $self->_result->borrowernumber
165 Overloaded getter/setter for request status,
166 also nullifies status_alias
171 my ( $self, $newval) = @_;
173 $self->status_alias(undef);
174 return $self->SUPER::status
($newval);
176 return $self->SUPER::status
;
181 Require "Base.pm" from the relevant ILL backend.
186 my ( $self, $backend_id ) = @_;
188 my @raw = qw
/Koha Illbackends/; # Base Path
190 my $backend_name = $backend_id || $self->backend;
192 unless ( defined $backend_name && $backend_name ne '' ) {
193 Koha
::Exceptions
::Ill
::InvalidBackendId
->throw(
194 "An invalid backend ID was requested ('')");
197 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
198 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
200 $self->{_my_backend
} = $backend_class->new({ config
=> $self->_config });
207 my $backend = $abstract->_backend($new_backend);
208 my $backend = $abstract->_backend;
210 Getter/Setter for our API object.
215 my ( $self, $backend ) = @_;
216 $self->{_my_backend
} = $backend if ( $backend );
217 # Dynamically load our backend object, as late as possible.
218 $self->load_backend unless ( $self->{_my_backend
} );
219 return $self->{_my_backend
};
222 =head3 _backend_capability
224 my $backend_capability_result = $self->_backend_capability($name, $args);
226 This is a helper method to invoke optional capabilities in the backend. If
227 the capability named by $name is not supported, return 0, else invoke it,
228 passing $args along with the invocation, and return its return value.
230 NOTE: this module suffers from a confusion in termninology:
232 in _backend_capability, the notion of capability refers to an optional feature
233 that is implemented in core, but might not be supported by a given backend.
235 in capabilities & custom_capability, capability refers to entries in the
236 status_graph (after union between backend and core).
238 The easiest way to fix this would be to fix the terminology in
239 capabilities & custom_capability and their callers.
243 sub _backend_capability
{
244 my ( $self, $name, $args ) = @_;
247 $capability = $self->_backend->capabilities($name);
252 return &{$capability}($args);
260 my $config = $abstract->_config($config);
261 my $config = $abstract->_config;
263 Getter/Setter for our config object.
268 my ( $self, $config ) = @_;
269 $self->{_my_config
} = $config if ( $config );
270 # Load our config object, as late as possible.
271 unless ( $self->{_my_config
} ) {
272 $self->{_my_config
} = Koha
::Illrequest
::Config
->new;
274 return $self->{_my_config
};
283 return $self->_backend->metadata($self);
286 =head3 _core_status_graph
288 my $core_status_graph = $illrequest->_core_status_graph;
290 Returns ILL module's default status graph. A status graph defines the list of
291 available actions at any stage in the ILL workflow. This is for instance used
292 by the perl script & template to generate the correct buttons to display to
293 the end user at any given point.
297 sub _core_status_graph
{
301 prev_actions
=> [ ], # Actions containing buttons
302 # leading to this status
303 id
=> 'NEW', # ID of this status
304 name
=> 'New request', # UI name of this status
305 ui_method_name
=> 'New request', # UI name of method leading
307 method
=> 'create', # method to this status
308 next_actions
=> [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
309 # requests with this status
310 ui_method_icon
=> 'fa-plus', # UI Style class
313 prev_actions
=> [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
316 ui_method_name
=> 'Confirm request',
318 next_actions
=> [ 'REQREV', 'COMP' ],
319 ui_method_icon
=> 'fa-check',
322 prev_actions
=> [ 'NEW', 'REQREV' ],
324 name
=> 'Requested from partners',
325 ui_method_name
=> 'Place request with partners',
326 method
=> 'generic_confirm',
327 next_actions
=> [ 'COMP' ],
328 ui_method_icon
=> 'fa-send-o',
331 prev_actions
=> [ 'REQ' ],
333 name
=> 'Request reverted',
334 ui_method_name
=> 'Revert Request',
336 next_actions
=> [ 'REQ', 'GENREQ', 'KILL' ],
337 ui_method_icon
=> 'fa-times',
342 name
=> 'Queued request',
345 next_actions
=> [ 'REQ', 'KILL' ],
349 prev_actions
=> [ 'NEW' ],
351 name
=> 'Cancellation requested',
354 next_actions
=> [ 'KILL', 'REQ' ],
358 prev_actions
=> [ 'REQ' ],
361 ui_method_name
=> 'Mark completed',
362 method
=> 'mark_completed',
364 ui_method_icon
=> 'fa-check',
367 prev_actions
=> [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
370 ui_method_name
=> 'Delete request',
373 ui_method_icon
=> 'fa-trash',
378 =head3 _core_status_graph
380 my $status_graph = $illrequest->_core_status_graph($origin, $new_graph);
382 Return a new status_graph, the result of merging $origin & new_graph. This is
383 operation is a union over the sets defied by the two graphs.
385 Each entry in $new_graph is added to $origin. We do not provide a syntax for
386 'subtraction' of entries from $origin.
388 Whilst it is not intended that this works, you can override entries in $origin
389 with entries with the same key in $new_graph. This can lead to problematic
390 behaviour when $new_graph adds an entry, which modifies a dependent entry in
391 $origin, only for the entry in $origin to be replaced later with a new entry
394 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
395 i.e. each of the graphs need to be correct at the outset of the operation.
399 sub _status_graph_union
{
400 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
401 # Create new status graph with:
402 # - all core_status_graph
403 # - for-each each backend_status_graph
404 # + add to new status graph
405 # + for each core prev_action:
406 # * locate core_status
407 # * update next_actions with additional next action.
408 # + for each core next_action:
409 # * locate core_status
410 # * update prev_actions with additional prev action
412 my @core_status_ids = keys %{$core_status_graph};
413 my $status_graph = clone
($core_status_graph);
415 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
416 my $backend_status = $backend_status_graph->{$backend_status_key};
417 # Add to new status graph
418 $status_graph->{$backend_status_key} = $backend_status;
419 # Update all core methods' next_actions.
420 foreach my $prev_action ( @
{$backend_status->{prev_actions
}} ) {
421 if ( grep $prev_action, @core_status_ids ) {
423 @
{$status_graph->{$prev_action}->{next_actions
}};
424 push @next_actions, $backend_status_key;
425 $status_graph->{$prev_action}->{next_actions
}
429 # Update all core methods' prev_actions
430 foreach my $next_action ( @
{$backend_status->{next_actions
}} ) {
431 if ( grep $next_action, @core_status_ids ) {
433 @
{$status_graph->{$next_action}->{prev_actions
}};
434 push @prev_actions, $backend_status_key;
435 $status_graph->{$next_action}->{prev_actions
}
441 return $status_graph;
448 my $capabilities = $illrequest->capabilities;
450 Return a hashref mapping methods to operation names supported by the queried
453 Example return value:
455 { create => "Create Request", confirm => "Progress Request" }
457 NOTE: this module suffers from a confusion in termninology:
459 in _backend_capability, the notion of capability refers to an optional feature
460 that is implemented in core, but might not be supported by a given backend.
462 in capabilities & custom_capability, capability refers to entries in the
463 status_graph (after union between backend and core).
465 The easiest way to fix this would be to fix the terminology in
466 capabilities & custom_capability and their callers.
471 my ( $self, $status ) = @_;
472 # Generate up to date status_graph
473 my $status_graph = $self->_status_graph_union(
474 $self->_core_status_graph,
475 $self->_backend->status_graph({
480 # Extract available actions from graph.
481 return $status_graph->{$status} if $status;
482 # Or return entire graph.
483 return $status_graph;
486 =head3 custom_capability
488 Return the result of invoking $CANDIDATE on this request's backend with
489 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
491 NOTE: this module suffers from a confusion in termninology:
493 in _backend_capability, the notion of capability refers to an optional feature
494 that is implemented in core, but might not be supported by a given backend.
496 in capabilities & custom_capability, capability refers to entries in the
497 status_graph (after union between backend and core).
499 The easiest way to fix this would be to fix the terminology in
500 capabilities & custom_capability and their callers.
504 sub custom_capability
{
505 my ( $self, $candidate, $params ) = @_;
506 foreach my $capability ( values %{$self->capabilities} ) {
507 if ( $candidate eq $capability->{method
} ) {
509 $self->_backend->$candidate({
513 return $self->expandTemplate($response);
519 =head3 available_backends
521 Return a list of available backends.
525 sub available_backends
{
527 my $backends = $self->_config->available_backends;
531 =head3 available_actions
533 Return a list of available actions.
537 sub available_actions
{
539 my $current_action = $self->capabilities($self->status);
540 my @available_actions = map { $self->capabilities($_) }
541 @
{$current_action->{next_actions
}};
542 return \
@available_actions;
545 =head3 mark_completed
547 Mark a request as completed (status = COMP).
553 $self->status('COMP')->store;
558 method
=> 'mark_completed',
564 =head2 backend_migrate
566 Migrate a request from one backend to another.
570 sub backend_migrate
{
571 my ( $self, $params ) = @_;
573 my $response = $self->_backend_capability('migrate',{
577 return $self->expandTemplate($response) if $response;
581 =head2 backend_confirm
583 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
589 =item * accessurl, cost (if available).
595 sub backend_confirm
{
596 my ( $self, $params ) = @_;
598 my $response = $self->_backend->confirm({
602 return $self->expandTemplate($response);
605 =head3 backend_update_status
609 sub backend_update_status
{
610 my ( $self, $params ) = @_;
611 return $self->expandTemplate($self->_backend->update_status($params));
614 =head3 backend_cancel
616 my $ILLResponse = $illRequest->backend_cancel;
618 The standard interface method allowing for request cancellation.
623 my ( $self, $params ) = @_;
625 my $result = $self->_backend->cancel({
630 return $self->expandTemplate($result);
635 my $renew_response = $illRequest->backend_renew;
637 The standard interface method allowing for request renewal queries.
643 return $self->expandTemplate(
644 $self->_backend->renew({
650 =head3 backend_create
652 my $create_response = $abstractILL->backend_create($params);
654 Return an array of Record objects created by querying our backend with
657 In the context of the other ILL methods, this is a special method: we only
658 pass it $params, as it does not yet have any other data associated with it.
663 my ( $self, $params ) = @_;
665 # Establish whether we need to do a generic copyright clearance.
666 if ($params->{opac
}) {
667 if ( ( !$params->{stage
} || $params->{stage
} eq 'init' )
668 && C4
::Context
->preference("ILLModuleCopyrightClearance") ) {
674 stage
=> 'copyrightclearance',
676 backend
=> $self->_backend->name
679 } elsif ( defined $params->{stage
}
680 && $params->{stage
} eq 'copyrightclearance' ) {
681 $params->{stage
} = 'init';
684 # First perform API action, then...
689 my $result = $self->_backend->create($args);
691 # ... simple case: we're not at 'commit' stage.
692 my $stage = $result->{stage
};
693 return $self->expandTemplate($result)
694 unless ( 'commit' eq $stage );
696 # ... complex case: commit!
698 # Do we still have space for an ILL or should we queue?
699 my $permitted = $self->check_limits(
700 { patron
=> $self->patron }, { librarycode
=> $self->branchcode }
703 # Now augment our committed request.
705 $result->{permitted
} = $permitted; # Queue request?
709 # ...Updating status!
710 $self->status('QUEUED')->store unless ( $permitted );
712 return $self->expandTemplate($result);
715 =head3 expandTemplate
717 my $params = $abstract->expandTemplate($params);
719 Return a version of $PARAMS augmented with our required template path.
724 my ( $self, $params ) = @_;
725 my $backend = $self->_backend->name;
726 # Generate path to file to load
727 my $backend_dir = $self->_config->backend_dir;
728 my $backend_tmpl = join "/", $backend_dir, $backend;
729 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
730 $params->{method
} . ".inc";
731 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
732 $params->{method
} . ".inc";
734 $params->{template
} = $intra_tmpl;
735 $params->{opac_template
} = $opac_tmpl;
739 #### Abstract Imports
743 my $limit_rules = $abstract->getLimits( {
744 type => 'brw_cat' | 'branch',
748 Return the ILL limit rules for the supplied combination of type / value.
750 As the config may have no rules for this particular type / value combination,
751 or for the default, we must define fall-back values here.
756 my ( $self, $params ) = @_;
757 my $limits = $self->_config->getLimitRules($params->{type
});
759 if ( defined $params->{value
}
760 && defined $limits->{$params->{value
}} ) {
761 return $limits->{$params->{value
}};
764 return $limits->{default} || { count
=> -1, method
=> 'active' };
770 my $prefix = $abstract->getPrefix( {
771 branch => $branch_code
774 Return the ILL prefix as defined by our $params: either per borrower category,
775 per branch or the default.
780 my ( $self, $params ) = @_;
781 my $brn_prefixes = $self->_config->getPrefixes();
782 return $brn_prefixes->{$params->{branch
}} || ""; # "the empty prefix"
787 my $type = $abstract->get_type();
789 Return a string representing the material type of this request or undef
795 my $attr = $self->illrequestattributes->find({ type
=> 'type'});
800 #### Illrequests Imports
804 my $ok = $illRequests->check_limits( {
805 borrower => $borrower,
806 branchcode => 'branchcode' | undef,
809 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
810 see whether we are still able to place ILLs.
812 LimitRules are derived from koha-conf.xml:
813 + default limit counts, and counting method
814 + branch specific limit counts & counting method
815 + borrower category specific limit counts & counting method
816 + err on the side of caution: a counting fail will cause fail, even if
817 the other counts passes.
822 my ( $self, $params ) = @_;
823 my $patron = $params->{patron
};
824 my $branchcode = $params->{librarycode
} || $patron->branchcode;
826 # Establish maximum number of allowed requests
827 my ( $branch_rules, $brw_rules ) = (
834 value
=> $patron->categorycode,
837 my ( $branch_limit, $brw_limit )
838 = ( $branch_rules->{count
}, $brw_rules->{count
} );
839 # Establish currently existing requests
840 my ( $branch_count, $brw_count ) = (
841 $self->_limit_counter(
842 $branch_rules->{method
}, { branchcode
=> $branchcode }
844 $self->_limit_counter(
845 $brw_rules->{method
}, { borrowernumber
=> $patron->borrowernumber }
850 # A limit of -1 means no limit exists.
851 # We return blocked if either branch limit or brw limit is reached.
852 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
853 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
861 my ( $self, $method, $target ) = @_;
863 # Establish parameters of counts
865 if ($method && $method eq 'annual') {
866 $resultset = Koha
::Illrequests
->search({
869 \"YEAR
(placed
) = YEAR
(NOW
())"
872 } else { # assume 'active'
873 # XXX: This status list is ugly. There should be a method in config
875 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
876 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
880 return $resultset->count;
883 =head3 requires_moderation
885 my $status = $illRequest->requires_moderation;
887 Return the name of the status if moderation by staff is required; or 0
892 sub requires_moderation {
894 my $require_moderation = {
895 'CANCREQ' => 'CANCREQ',
897 return $require_moderation->{$self->status};
900 =head3 generic_confirm
902 my $stage_summary = $illRequest->generic_confirm;
904 Handle the generic_confirm extended method. The first stage involves creating
905 a template email for the end user to edit in the browser. The second stage
906 attempts to submit the email.
910 sub generic_confirm {
911 my ( $self, $params ) = @_;
912 my $branch = Koha::Libraries->find($params->{current_branchcode})
913 || die "Invalid current branchcode
. Are you logged
in as the database user?
";
914 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
915 my $draft->{subject} = "ILL Request
";
916 $draft->{body} = <<EOF;
919 We would like to request an interlibrary loan for a title matching the
920 following description:
924 my $details = $self->metadata;
925 while (my ($title, $value) = each %{$details}) {
926 $draft->{body
} .= " - " . $title . ": " . $value . "\n"
929 $draft->{body
} .= <<EOF;
931 Please let us know if you are able to supply this to us.
937 my @address = map { $branch->$_ }
938 qw
/ branchname branchaddress1 branchaddress2 branchaddress3
939 branchzip branchcity branchstate branchcountry branchphone
942 foreach my $line ( @address ) {
943 $address .= $line . "\n" if $line;
946 $draft->{body
} .= $address;
948 my $partners = Koha
::Patrons
->search({
949 categorycode
=> $self->_config->partner_code
955 method
=> 'generic_confirm',
959 partners
=> $partners,
963 } elsif ( 'draft' eq $params->{stage
} ) {
964 # Create the to header
965 my $to = $params->{partners
};
967 $to =~ s/^\x00//; # Strip leading NULLs
968 $to =~ s/\x00/; /; # Replace others with '; '
970 Koha
::Exceptions
::Ill
::NoTargetEmail
->throw(
971 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
973 # Create the from, replyto and sender headers
974 my $from = $branch->branchemail;
975 my $replyto = $branch->branchreplyto || $from;
976 Koha
::Exceptions
::Ill
::NoLibraryEmail
->throw(
977 "Your library has no usable email address. Please set it.")
981 my $message = Koha
::Email
->new;
982 my %mail = $message->create_message_headers(
987 subject
=> Encode
::encode
( "utf8", $params->{subject
} ),
988 message
=> Encode
::encode
( "utf8", $params->{body
} ),
989 contenttype
=> 'text/plain',
993 my $result = sendmail
(%mail);
995 $self->status("GENREQ")->store;
1000 method
=> 'generic_confirm',
1007 status
=> 'email_failed',
1008 message
=> $Mail::Sendmail
::error
,
1009 method
=> 'generic_confirm',
1014 die "Unknown stage, should not have happened."
1020 my $prefix = $record->id_prefix;
1022 Return the prefix appropriate for the current Illrequest as derived from the
1023 borrower and branch associated with this request's Status, and the config
1030 my $prefix = $self->getPrefix( {
1031 branch
=> $self->branchcode,
1033 $prefix .= "-" if ( $prefix );
1039 my $params = $illRequest->_censor($params);
1041 Return $params, modified to reflect our censorship requirements.
1046 my ( $self, $params ) = @_;
1047 my $censorship = $self->_config->censorship;
1048 $params->{censor_notes_staff
} = $censorship->{censor_notes_staff
}
1049 if ( $params->{opac
} );
1050 $params->{display_reply_date
} = ( $censorship->{censor_reply_date
} ) ?
0 : 1;
1057 $json = $illrequest->TO_JSON
1059 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1060 into the unblessed representation of the object.
1062 TODO: This method does nothing and is not called anywhere. However, bug 74325
1063 touches it, so keeping this for now until both this and bug 74325 are merged,
1064 at which point we can sort it out and remove it completely
1069 my ( $self, $embed ) = @_;
1071 my $object = $self->SUPER::TO_JSON
();
1076 =head2 Internal methods
1083 return 'Illrequest';
1088 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>