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;
36 use base qw(Koha::Object);
40 Koha::Illrequest - Koha Illrequest Object class
44 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
45 of related Illrequestattributes.
47 The former encapsulates the basic necessary information that any ILL requires
48 to be usable in Koha. The latter is a set of additional properties used by
51 The former subsumes the legacy "Status" object. The latter remains
52 encapsulated in the "Record" object.
56 - Anything invoking the ->status method; annotated with:
57 + # Old use of ->status !
61 =head2 Backend API Response Principles
63 All methods should return a hashref in the following format:
69 This should be set to 1 if an error was encountered.
73 The status should be a string from the list of statuses detailed below.
77 The message is a free text field that can be passed on to the end user.
81 The value returned by the method.
85 =head2 Interface Status Messages
89 =item * branch_address_incomplete
91 An interface request has determined branch address details are incomplete.
93 =item * cancel_success
95 The interface's cancel_request method was successful in cancelling the
96 Illrequest using the API.
100 The interface's cancel_request method failed to cancel the Illrequest using
105 The interface's request method returned saying that the desired item is not
106 available for request.
112 =head3 illrequestattributes
116 sub illrequestattributes {
118 return Koha::Illrequestattributes->_new_from_dbic(
119 scalar $self->_result->illrequestattributes
129 return Koha::Illcomments->_new_from_dbic(
130 scalar $self->_result->illcomments
140 return Koha::Patron->_new_from_dbic(
141 scalar $self->_result->borrowernumber
147 Require "Base.pm" from the relevant ILL backend.
152 my ( $self, $backend_id ) = @_;
154 my @raw = qw/Koha Illbackends/; # Base Path
156 my $backend_name = $backend_id || $self->backend;
158 unless ( defined $backend_name && $backend_name ne '' ) {
159 Koha::Exceptions::Ill::InvalidBackendId->throw(
160 "An invalid backend ID was requested ('')");
163 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
164 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
166 $self->{_my_backend} = $backend_class->new({ config => $self->_config });
173 my $backend = $abstract->_backend($new_backend);
174 my $backend = $abstract->_backend;
176 Getter/Setter for our API object.
181 my ( $self, $backend ) = @_;
182 $self->{_my_backend} = $backend if ( $backend );
183 # Dynamically load our backend object, as late as possible.
184 $self->load_backend unless ( $self->{_my_backend} );
185 return $self->{_my_backend};
188 =head3 _backend_capability
190 my $backend_capability_result = $self->_backend_capability($name, $args);
192 This is a helper method to invoke optional capabilities in the backend. If
193 the capability named by $name is not supported, return 0, else invoke it,
194 passing $args along with the invocation, and return its return value.
196 NOTE: this module suffers from a confusion in termninology:
198 in _backend_capability, the notion of capability refers to an optional feature
199 that is implemented in core, but might not be supported by a given backend.
201 in capabilities & custom_capability, capability refers to entries in the
202 status_graph (after union between backend and core).
204 The easiest way to fix this would be to fix the terminology in
205 capabilities & custom_capability and their callers.
209 sub _backend_capability {
210 my ( $self, $name, $args ) = @_;
213 $capability = $self->_backend->capabilities($name);
218 return &{$capability}($args);
226 my $config = $abstract->_config($config);
227 my $config = $abstract->_config;
229 Getter/Setter for our config object.
234 my ( $self, $config ) = @_;
235 $self->{_my_config} = $config if ( $config );
236 # Load our config object, as late as possible.
237 unless ( $self->{_my_config} ) {
238 $self->{_my_config} = Koha::Illrequest::Config->new;
240 return $self->{_my_config};
249 return $self->_backend->metadata($self);
252 =head3 _core_status_graph
254 my $core_status_graph = $illrequest->_core_status_graph;
256 Returns ILL module's default status graph. A status graph defines the list of
257 available actions at any stage in the ILL workflow. This is for instance used
258 by the perl script & template to generate the correct buttons to display to
259 the end user at any given point.
263 sub _core_status_graph {
267 prev_actions => [ ], # Actions containing buttons
268 # leading to this status
269 id => 'NEW', # ID of this status
270 name => 'New request', # UI name of this status
271 ui_method_name => 'New request', # UI name of method leading
273 method => 'create', # method to this status
274 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
275 # requests with this status
276 ui_method_icon => 'fa-plus', # UI Style class
279 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
282 ui_method_name => 'Confirm request',
284 next_actions => [ 'REQREV', 'COMP' ],
285 ui_method_icon => 'fa-check',
288 prev_actions => [ 'NEW', 'REQREV' ],
290 name => 'Requested from partners',
291 ui_method_name => 'Place request with partners',
292 method => 'generic_confirm',
293 next_actions => [ 'COMP' ],
294 ui_method_icon => 'fa-send-o',
297 prev_actions => [ 'REQ' ],
299 name => 'Request reverted',
300 ui_method_name => 'Revert Request',
302 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
303 ui_method_icon => 'fa-times',
308 name => 'Queued request',
311 next_actions => [ 'REQ', 'KILL' ],
315 prev_actions => [ 'NEW' ],
317 name => 'Cancellation requested',
320 next_actions => [ 'KILL', 'REQ' ],
324 prev_actions => [ 'REQ' ],
327 ui_method_name => 'Mark completed',
328 method => 'mark_completed',
330 ui_method_icon => 'fa-check',
333 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
336 ui_method_name => 'Delete request',
339 ui_method_icon => 'fa-trash',
344 =head3 _core_status_graph
346 my $status_graph = $illrequest->_core_status_graph($origin, $new_graph);
348 Return a new status_graph, the result of merging $origin & new_graph. This is
349 operation is a union over the sets defied by the two graphs.
351 Each entry in $new_graph is added to $origin. We do not provide a syntax for
352 'subtraction' of entries from $origin.
354 Whilst it is not intended that this works, you can override entries in $origin
355 with entries with the same key in $new_graph. This can lead to problematic
356 behaviour when $new_graph adds an entry, which modifies a dependent entry in
357 $origin, only for the entry in $origin to be replaced later with a new entry
360 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
361 i.e. each of the graphs need to be correct at the outset of the operation.
365 sub _status_graph_union {
366 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
367 # Create new status graph with:
368 # - all core_status_graph
369 # - for-each each backend_status_graph
370 # + add to new status graph
371 # + for each core prev_action:
372 # * locate core_status
373 # * update next_actions with additional next action.
374 # + for each core next_action:
375 # * locate core_status
376 # * update prev_actions with additional prev action
378 my @core_status_ids = keys %{$core_status_graph};
379 my $status_graph = clone($core_status_graph);
381 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
382 my $backend_status = $backend_status_graph->{$backend_status_key};
383 # Add to new status graph
384 $status_graph->{$backend_status_key} = $backend_status;
385 # Update all core methods' next_actions.
386 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
387 if ( grep $prev_action, @core_status_ids ) {
389 @{$status_graph->{$prev_action}->{next_actions}};
390 push @next_actions, $backend_status_key;
391 $status_graph->{$prev_action}->{next_actions}
395 # Update all core methods' prev_actions
396 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
397 if ( grep $next_action, @core_status_ids ) {
399 @{$status_graph->{$next_action}->{prev_actions}};
400 push @prev_actions, $backend_status_key;
401 $status_graph->{$next_action}->{prev_actions}
407 return $status_graph;
414 my $capabilities = $illrequest->capabilities;
416 Return a hashref mapping methods to operation names supported by the queried
419 Example return value:
421 { create => "Create Request", confirm => "Progress Request" }
423 NOTE: this module suffers from a confusion in termninology:
425 in _backend_capability, the notion of capability refers to an optional feature
426 that is implemented in core, but might not be supported by a given backend.
428 in capabilities & custom_capability, capability refers to entries in the
429 status_graph (after union between backend and core).
431 The easiest way to fix this would be to fix the terminology in
432 capabilities & custom_capability and their callers.
437 my ( $self, $status ) = @_;
438 # Generate up to date status_graph
439 my $status_graph = $self->_status_graph_union(
440 $self->_core_status_graph,
441 $self->_backend->status_graph({
446 # Extract available actions from graph.
447 return $status_graph->{$status} if $status;
448 # Or return entire graph.
449 return $status_graph;
452 =head3 custom_capability
454 Return the result of invoking $CANDIDATE on this request's backend with
455 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
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.
470 sub custom_capability {
471 my ( $self, $candidate, $params ) = @_;
472 foreach my $capability ( values %{$self->capabilities} ) {
473 if ( $candidate eq $capability->{method} ) {
475 $self->_backend->$candidate({
479 return $self->expandTemplate($response);
485 =head3 available_backends
487 Return a list of available backends.
491 sub available_backends {
493 my $backends = $self->_config->available_backends;
497 =head3 available_actions
499 Return a list of available actions.
503 sub available_actions {
505 my $current_action = $self->capabilities($self->status);
506 my @available_actions = map { $self->capabilities($_) }
507 @{$current_action->{next_actions}};
508 return \@available_actions;
511 =head3 mark_completed
513 Mark a request as completed (status = COMP).
519 $self->status('COMP')->store;
524 method => 'mark_completed',
530 =head2 backend_migrate
532 Migrate a request from one backend to another.
536 sub backend_migrate {
537 my ( $self, $params ) = @_;
539 my $response = $self->_backend->migrate({
543 return $self->expandTemplate($response);
546 =head2 backend_confirm
548 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
554 =item * accessurl, cost (if available).
560 sub backend_confirm {
561 my ( $self, $params ) = @_;
563 my $response = $self->_backend->confirm({
567 return $self->expandTemplate($response);
570 =head3 backend_update_status
574 sub backend_update_status {
575 my ( $self, $params ) = @_;
576 return $self->expandTemplate($self->_backend->update_status($params));
579 =head3 backend_cancel
581 my $ILLResponse = $illRequest->backend_cancel;
583 The standard interface method allowing for request cancellation.
588 my ( $self, $params ) = @_;
590 my $result = $self->_backend->cancel({
595 return $self->expandTemplate($result);
600 my $renew_response = $illRequest->backend_renew;
602 The standard interface method allowing for request renewal queries.
608 return $self->expandTemplate(
609 $self->_backend->renew({
615 =head3 backend_create
617 my $create_response = $abstractILL->backend_create($params);
619 Return an array of Record objects created by querying our backend with
622 In the context of the other ILL methods, this is a special method: we only
623 pass it $params, as it does not yet have any other data associated with it.
628 my ( $self, $params ) = @_;
630 # Establish whether we need to do a generic copyright clearance.
631 if ($params->{opac}) {
632 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
633 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
639 stage => 'copyrightclearance',
641 backend => $self->_backend->name
644 } elsif ( defined $params->{stage}
645 && $params->{stage} eq 'copyrightclearance' ) {
646 $params->{stage} = 'init';
649 # First perform API action, then...
654 my $result = $self->_backend->create($args);
656 # ... simple case: we're not at 'commit' stage.
657 my $stage = $result->{stage};
658 return $self->expandTemplate($result)
659 unless ( 'commit' eq $stage );
661 # ... complex case: commit!
663 # Do we still have space for an ILL or should we queue?
664 my $permitted = $self->check_limits(
665 { patron => $self->patron }, { librarycode => $self->branchcode }
668 # Now augment our committed request.
670 $result->{permitted} = $permitted; # Queue request?
674 # ...Updating status!
675 $self->status('QUEUED')->store unless ( $permitted );
677 return $self->expandTemplate($result);
680 =head3 expandTemplate
682 my $params = $abstract->expandTemplate($params);
684 Return a version of $PARAMS augmented with our required template path.
689 my ( $self, $params ) = @_;
690 my $backend = $self->_backend->name;
691 # Generate path to file to load
692 my $backend_dir = $self->_config->backend_dir;
693 my $backend_tmpl = join "/", $backend_dir, $backend;
694 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
695 $params->{method} . ".inc";
696 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
697 $params->{method} . ".inc";
699 $params->{template} = $intra_tmpl;
700 $params->{opac_template} = $opac_tmpl;
704 #### Abstract Imports
708 my $limit_rules = $abstract->getLimits( {
709 type => 'brw_cat' | 'branch',
713 Return the ILL limit rules for the supplied combination of type / value.
715 As the config may have no rules for this particular type / value combination,
716 or for the default, we must define fall-back values here.
721 my ( $self, $params ) = @_;
722 my $limits = $self->_config->getLimitRules($params->{type});
724 if ( defined $params->{value}
725 && defined $limits->{$params->{value}} ) {
726 return $limits->{$params->{value}};
729 return $limits->{default} || { count => -1, method => 'active' };
735 my $prefix = $abstract->getPrefix( {
736 branch => $branch_code
739 Return the ILL prefix as defined by our $params: either per borrower category,
740 per branch or the default.
745 my ( $self, $params ) = @_;
746 my $brn_prefixes = $self->_config->getPrefixes();
747 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
752 my $type = $abstract->get_type();
754 Return a string representing the material type of this request or undef
760 my $attr = $self->illrequestattributes->find({ type => 'type'});
765 #### Illrequests Imports
769 my $ok = $illRequests->check_limits( {
770 borrower => $borrower,
771 branchcode => 'branchcode' | undef,
774 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
775 see whether we are still able to place ILLs.
777 LimitRules are derived from koha-conf.xml:
778 + default limit counts, and counting method
779 + branch specific limit counts & counting method
780 + borrower category specific limit counts & counting method
781 + err on the side of caution: a counting fail will cause fail, even if
782 the other counts passes.
787 my ( $self, $params ) = @_;
788 my $patron = $params->{patron};
789 my $branchcode = $params->{librarycode} || $patron->branchcode;
791 # Establish maximum number of allowed requests
792 my ( $branch_rules, $brw_rules ) = (
799 value => $patron->categorycode,
802 my ( $branch_limit, $brw_limit )
803 = ( $branch_rules->{count}, $brw_rules->{count} );
804 # Establish currently existing requests
805 my ( $branch_count, $brw_count ) = (
806 $self->_limit_counter(
807 $branch_rules->{method}, { branchcode => $branchcode }
809 $self->_limit_counter(
810 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
815 # A limit of -1 means no limit exists.
816 # We return blocked if either branch limit or brw limit is reached.
817 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
818 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
826 my ( $self, $method, $target ) = @_;
828 # Establish parameters of counts
830 if ($method && $method eq 'annual') {
831 $resultset = Koha::Illrequests->search({
834 \"YEAR(placed) = YEAR(NOW())"
837 } else { # assume 'active'
838 # XXX: This status list is ugly. There should be a method in config
840 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
841 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
845 return $resultset->count;
848 =head3 requires_moderation
850 my $status = $illRequest->requires_moderation;
852 Return the name of the status if moderation by staff is required; or 0
857 sub requires_moderation {
859 my $require_moderation = {
860 'CANCREQ' => 'CANCREQ',
862 return $require_moderation->{$self->status};
865 =head3 generic_confirm
867 my $stage_summary = $illRequest->generic_confirm;
869 Handle the generic_confirm extended method. The first stage involves creating
870 a template email for the end user to edit in the browser. The second stage
871 attempts to submit the email.
875 sub generic_confirm {
876 my ( $self, $params ) = @_;
877 my $branch = Koha::Libraries->find($params->{current_branchcode})
878 || die "Invalid current branchcode. Are you logged in as the database user?";
879 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
880 my $draft->{subject} = "ILL Request";
881 $draft->{body} = <<EOF;
884 We would like to request an interlibrary loan for a title matching the
885 following description:
889 my $details = $self->metadata;
890 while (my ($title, $value) = each %{$details}) {
891 $draft->{body} .= " - " . $title . ": " . $value . "\n"
894 $draft->{body} .= <<EOF;
896 Please let us know if you are able to supply this to us.
902 my @address = map { $branch->$_ }
903 qw/ branchname branchaddress1 branchaddress2 branchaddress3
904 branchzip branchcity branchstate branchcountry branchphone
907 foreach my $line ( @address ) {
908 $address .= $line . "\n" if $line;
911 $draft->{body} .= $address;
913 my $partners = Koha::Patrons->search({
914 categorycode => $self->_config->partner_code
920 method => 'generic_confirm',
924 partners => $partners,
928 } elsif ( 'draft' eq $params->{stage} ) {
929 # Create the to header
930 my $to = $params->{partners};
932 $to =~ s/^\x00//; # Strip leading NULLs
933 $to =~ s/\x00/; /; # Replace others with '; '
935 Koha::Exceptions::Ill::NoTargetEmail->throw(
936 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
938 # Create the from, replyto and sender headers
939 my $from = $branch->branchemail;
940 my $replyto = $branch->branchreplyto || $from;
941 Koha::Exceptions::Ill::NoLibraryEmail->throw(
942 "Your library has no usable email address. Please set it.")
946 my $message = Koha::Email->new;
947 my %mail = $message->create_message_headers(
952 subject => Encode::encode( "utf8", $params->{subject} ),
953 message => Encode::encode( "utf8", $params->{body} ),
954 contenttype => 'text/plain',
958 my $result = sendmail(%mail);
960 $self->status("GENREQ")->store;
965 method => 'generic_confirm',
972 status => 'email_failed',
973 message => $Mail::Sendmail::error,
974 method => 'generic_confirm',
979 die "Unknown stage, should not have happened."
985 my $prefix = $record->id_prefix;
987 Return the prefix appropriate for the current Illrequest as derived from the
988 borrower and branch associated with this request's Status, and the config
995 my $prefix = $self->getPrefix( {
996 branch => $self->branchcode,
998 $prefix .= "-" if ( $prefix );
1004 my $params = $illRequest->_censor($params);
1006 Return $params, modified to reflect our censorship requirements.
1011 my ( $self, $params ) = @_;
1012 my $censorship = $self->_config->censorship;
1013 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1014 if ( $params->{opac} );
1015 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1022 $json = $illrequest->TO_JSON
1024 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1025 into the unblessed representation of the object.
1027 TODO: This method does nothing and is not called anywhere. However, bug 74325
1028 touches it, so keeping this for now until both this and bug 74325 are merged,
1029 at which point we can sort it out and remove it completely
1034 my ( $self, $embed ) = @_;
1036 my $object = $self->SUPER::TO_JSON();
1041 =head2 Internal methods
1048 return 'Illrequest';
1053 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>