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.
119 return $self->status_alias ?
120 Koha::AuthorisedValue->_new_from_dbic(
121 scalar $self->_result->status_alias
126 =head3 illrequestattributes
130 sub illrequestattributes {
132 return Koha::Illrequestattributes->_new_from_dbic(
133 scalar $self->_result->illrequestattributes
143 return Koha::Illcomments->_new_from_dbic(
144 scalar $self->_result->illcomments
154 return Koha::Patron->_new_from_dbic(
155 scalar $self->_result->borrowernumber
161 Overloaded getter/setter for request status,
162 also nullifies status_alias
167 my ( $self, $newval) = @_;
169 $self->status_alias(undef);
170 return $self->SUPER::status($newval);
172 return $self->SUPER::status;
177 Require "Base.pm" from the relevant ILL backend.
182 my ( $self, $backend_id ) = @_;
184 my @raw = qw/Koha Illbackends/; # Base Path
186 my $backend_name = $backend_id || $self->backend;
188 unless ( defined $backend_name && $backend_name ne '' ) {
189 Koha::Exceptions::Ill::InvalidBackendId->throw(
190 "An invalid backend ID was requested ('')");
193 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
194 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
196 $self->{_my_backend} = $backend_class->new({ config => $self->_config });
203 my $backend = $abstract->_backend($new_backend);
204 my $backend = $abstract->_backend;
206 Getter/Setter for our API object.
211 my ( $self, $backend ) = @_;
212 $self->{_my_backend} = $backend if ( $backend );
213 # Dynamically load our backend object, as late as possible.
214 $self->load_backend unless ( $self->{_my_backend} );
215 return $self->{_my_backend};
218 =head3 _backend_capability
220 my $backend_capability_result = $self->_backend_capability($name, $args);
222 This is a helper method to invoke optional capabilities in the backend. If
223 the capability named by $name is not supported, return 0, else invoke it,
224 passing $args along with the invocation, and return its return value.
226 NOTE: this module suffers from a confusion in termninology:
228 in _backend_capability, the notion of capability refers to an optional feature
229 that is implemented in core, but might not be supported by a given backend.
231 in capabilities & custom_capability, capability refers to entries in the
232 status_graph (after union between backend and core).
234 The easiest way to fix this would be to fix the terminology in
235 capabilities & custom_capability and their callers.
239 sub _backend_capability {
240 my ( $self, $name, $args ) = @_;
243 $capability = $self->_backend->capabilities($name);
248 return &{$capability}($args);
256 my $config = $abstract->_config($config);
257 my $config = $abstract->_config;
259 Getter/Setter for our config object.
264 my ( $self, $config ) = @_;
265 $self->{_my_config} = $config if ( $config );
266 # Load our config object, as late as possible.
267 unless ( $self->{_my_config} ) {
268 $self->{_my_config} = Koha::Illrequest::Config->new;
270 return $self->{_my_config};
279 return $self->_backend->metadata($self);
282 =head3 _core_status_graph
284 my $core_status_graph = $illrequest->_core_status_graph;
286 Returns ILL module's default status graph. A status graph defines the list of
287 available actions at any stage in the ILL workflow. This is for instance used
288 by the perl script & template to generate the correct buttons to display to
289 the end user at any given point.
293 sub _core_status_graph {
297 prev_actions => [ ], # Actions containing buttons
298 # leading to this status
299 id => 'NEW', # ID of this status
300 name => 'New request', # UI name of this status
301 ui_method_name => 'New request', # UI name of method leading
303 method => 'create', # method to this status
304 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
305 # requests with this status
306 ui_method_icon => 'fa-plus', # UI Style class
309 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
312 ui_method_name => 'Confirm request',
314 next_actions => [ 'REQREV', 'COMP' ],
315 ui_method_icon => 'fa-check',
318 prev_actions => [ 'NEW', 'REQREV' ],
320 name => 'Requested from partners',
321 ui_method_name => 'Place request with partners',
322 method => 'generic_confirm',
323 next_actions => [ 'COMP' ],
324 ui_method_icon => 'fa-send-o',
327 prev_actions => [ 'REQ' ],
329 name => 'Request reverted',
330 ui_method_name => 'Revert Request',
332 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
333 ui_method_icon => 'fa-times',
338 name => 'Queued request',
341 next_actions => [ 'REQ', 'KILL' ],
345 prev_actions => [ 'NEW' ],
347 name => 'Cancellation requested',
350 next_actions => [ 'KILL', 'REQ' ],
354 prev_actions => [ 'REQ' ],
357 ui_method_name => 'Mark completed',
358 method => 'mark_completed',
360 ui_method_icon => 'fa-check',
363 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
366 ui_method_name => 'Delete request',
369 ui_method_icon => 'fa-trash',
374 =head3 _core_status_graph
376 my $status_graph = $illrequest->_core_status_graph($origin, $new_graph);
378 Return a new status_graph, the result of merging $origin & new_graph. This is
379 operation is a union over the sets defied by the two graphs.
381 Each entry in $new_graph is added to $origin. We do not provide a syntax for
382 'subtraction' of entries from $origin.
384 Whilst it is not intended that this works, you can override entries in $origin
385 with entries with the same key in $new_graph. This can lead to problematic
386 behaviour when $new_graph adds an entry, which modifies a dependent entry in
387 $origin, only for the entry in $origin to be replaced later with a new entry
390 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
391 i.e. each of the graphs need to be correct at the outset of the operation.
395 sub _status_graph_union {
396 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
397 # Create new status graph with:
398 # - all core_status_graph
399 # - for-each each backend_status_graph
400 # + add to new status graph
401 # + for each core prev_action:
402 # * locate core_status
403 # * update next_actions with additional next action.
404 # + for each core next_action:
405 # * locate core_status
406 # * update prev_actions with additional prev action
408 my @core_status_ids = keys %{$core_status_graph};
409 my $status_graph = clone($core_status_graph);
411 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
412 my $backend_status = $backend_status_graph->{$backend_status_key};
413 # Add to new status graph
414 $status_graph->{$backend_status_key} = $backend_status;
415 # Update all core methods' next_actions.
416 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
417 if ( grep $prev_action, @core_status_ids ) {
419 @{$status_graph->{$prev_action}->{next_actions}};
420 push @next_actions, $backend_status_key;
421 $status_graph->{$prev_action}->{next_actions}
425 # Update all core methods' prev_actions
426 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
427 if ( grep $next_action, @core_status_ids ) {
429 @{$status_graph->{$next_action}->{prev_actions}};
430 push @prev_actions, $backend_status_key;
431 $status_graph->{$next_action}->{prev_actions}
437 return $status_graph;
444 my $capabilities = $illrequest->capabilities;
446 Return a hashref mapping methods to operation names supported by the queried
449 Example return value:
451 { create => "Create Request", confirm => "Progress Request" }
453 NOTE: this module suffers from a confusion in termninology:
455 in _backend_capability, the notion of capability refers to an optional feature
456 that is implemented in core, but might not be supported by a given backend.
458 in capabilities & custom_capability, capability refers to entries in the
459 status_graph (after union between backend and core).
461 The easiest way to fix this would be to fix the terminology in
462 capabilities & custom_capability and their callers.
467 my ( $self, $status ) = @_;
468 # Generate up to date status_graph
469 my $status_graph = $self->_status_graph_union(
470 $self->_core_status_graph,
471 $self->_backend->status_graph({
476 # Extract available actions from graph.
477 return $status_graph->{$status} if $status;
478 # Or return entire graph.
479 return $status_graph;
482 =head3 custom_capability
484 Return the result of invoking $CANDIDATE on this request's backend with
485 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
487 NOTE: this module suffers from a confusion in termninology:
489 in _backend_capability, the notion of capability refers to an optional feature
490 that is implemented in core, but might not be supported by a given backend.
492 in capabilities & custom_capability, capability refers to entries in the
493 status_graph (after union between backend and core).
495 The easiest way to fix this would be to fix the terminology in
496 capabilities & custom_capability and their callers.
500 sub custom_capability {
501 my ( $self, $candidate, $params ) = @_;
502 foreach my $capability ( values %{$self->capabilities} ) {
503 if ( $candidate eq $capability->{method} ) {
505 $self->_backend->$candidate({
509 return $self->expandTemplate($response);
515 =head3 available_backends
517 Return a list of available backends.
521 sub available_backends {
523 my $backends = $self->_config->available_backends;
527 =head3 available_actions
529 Return a list of available actions.
533 sub available_actions {
535 my $current_action = $self->capabilities($self->status);
536 my @available_actions = map { $self->capabilities($_) }
537 @{$current_action->{next_actions}};
538 return \@available_actions;
541 =head3 mark_completed
543 Mark a request as completed (status = COMP).
549 $self->status('COMP')->store;
554 method => 'mark_completed',
560 =head2 backend_migrate
562 Migrate a request from one backend to another.
566 sub backend_migrate {
567 my ( $self, $params ) = @_;
569 my $response = $self->_backend_capability('migrate',{
573 return $self->expandTemplate($response) if $response;
577 =head2 backend_confirm
579 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
585 =item * accessurl, cost (if available).
591 sub backend_confirm {
592 my ( $self, $params ) = @_;
594 my $response = $self->_backend->confirm({
598 return $self->expandTemplate($response);
601 =head3 backend_update_status
605 sub backend_update_status {
606 my ( $self, $params ) = @_;
607 return $self->expandTemplate($self->_backend->update_status($params));
610 =head3 backend_cancel
612 my $ILLResponse = $illRequest->backend_cancel;
614 The standard interface method allowing for request cancellation.
619 my ( $self, $params ) = @_;
621 my $result = $self->_backend->cancel({
626 return $self->expandTemplate($result);
631 my $renew_response = $illRequest->backend_renew;
633 The standard interface method allowing for request renewal queries.
639 return $self->expandTemplate(
640 $self->_backend->renew({
646 =head3 backend_create
648 my $create_response = $abstractILL->backend_create($params);
650 Return an array of Record objects created by querying our backend with
653 In the context of the other ILL methods, this is a special method: we only
654 pass it $params, as it does not yet have any other data associated with it.
659 my ( $self, $params ) = @_;
661 # Establish whether we need to do a generic copyright clearance.
662 if ($params->{opac}) {
663 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
664 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
670 stage => 'copyrightclearance',
672 backend => $self->_backend->name
675 } elsif ( defined $params->{stage}
676 && $params->{stage} eq 'copyrightclearance' ) {
677 $params->{stage} = 'init';
680 # First perform API action, then...
685 my $result = $self->_backend->create($args);
687 # ... simple case: we're not at 'commit' stage.
688 my $stage = $result->{stage};
689 return $self->expandTemplate($result)
690 unless ( 'commit' eq $stage );
692 # ... complex case: commit!
694 # Do we still have space for an ILL or should we queue?
695 my $permitted = $self->check_limits(
696 { patron => $self->patron }, { librarycode => $self->branchcode }
699 # Now augment our committed request.
701 $result->{permitted} = $permitted; # Queue request?
705 # ...Updating status!
706 $self->status('QUEUED')->store unless ( $permitted );
708 return $self->expandTemplate($result);
711 =head3 expandTemplate
713 my $params = $abstract->expandTemplate($params);
715 Return a version of $PARAMS augmented with our required template path.
720 my ( $self, $params ) = @_;
721 my $backend = $self->_backend->name;
722 # Generate path to file to load
723 my $backend_dir = $self->_config->backend_dir;
724 my $backend_tmpl = join "/", $backend_dir, $backend;
725 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
726 $params->{method} . ".inc";
727 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
728 $params->{method} . ".inc";
730 $params->{template} = $intra_tmpl;
731 $params->{opac_template} = $opac_tmpl;
735 #### Abstract Imports
739 my $limit_rules = $abstract->getLimits( {
740 type => 'brw_cat' | 'branch',
744 Return the ILL limit rules for the supplied combination of type / value.
746 As the config may have no rules for this particular type / value combination,
747 or for the default, we must define fall-back values here.
752 my ( $self, $params ) = @_;
753 my $limits = $self->_config->getLimitRules($params->{type});
755 if ( defined $params->{value}
756 && defined $limits->{$params->{value}} ) {
757 return $limits->{$params->{value}};
760 return $limits->{default} || { count => -1, method => 'active' };
766 my $prefix = $abstract->getPrefix( {
767 branch => $branch_code
770 Return the ILL prefix as defined by our $params: either per borrower category,
771 per branch or the default.
776 my ( $self, $params ) = @_;
777 my $brn_prefixes = $self->_config->getPrefixes();
778 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
783 my $type = $abstract->get_type();
785 Return a string representing the material type of this request or undef
791 my $attr = $self->illrequestattributes->find({ type => 'type'});
796 #### Illrequests Imports
800 my $ok = $illRequests->check_limits( {
801 borrower => $borrower,
802 branchcode => 'branchcode' | undef,
805 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
806 see whether we are still able to place ILLs.
808 LimitRules are derived from koha-conf.xml:
809 + default limit counts, and counting method
810 + branch specific limit counts & counting method
811 + borrower category specific limit counts & counting method
812 + err on the side of caution: a counting fail will cause fail, even if
813 the other counts passes.
818 my ( $self, $params ) = @_;
819 my $patron = $params->{patron};
820 my $branchcode = $params->{librarycode} || $patron->branchcode;
822 # Establish maximum number of allowed requests
823 my ( $branch_rules, $brw_rules ) = (
830 value => $patron->categorycode,
833 my ( $branch_limit, $brw_limit )
834 = ( $branch_rules->{count}, $brw_rules->{count} );
835 # Establish currently existing requests
836 my ( $branch_count, $brw_count ) = (
837 $self->_limit_counter(
838 $branch_rules->{method}, { branchcode => $branchcode }
840 $self->_limit_counter(
841 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
846 # A limit of -1 means no limit exists.
847 # We return blocked if either branch limit or brw limit is reached.
848 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
849 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
857 my ( $self, $method, $target ) = @_;
859 # Establish parameters of counts
861 if ($method && $method eq 'annual') {
862 $resultset = Koha::Illrequests->search({
865 \"YEAR(placed) = YEAR(NOW())"
868 } else { # assume 'active'
869 # XXX: This status list is ugly. There should be a method in config
871 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
872 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
876 return $resultset->count;
879 =head3 requires_moderation
881 my $status = $illRequest->requires_moderation;
883 Return the name of the status if moderation by staff is required; or 0
888 sub requires_moderation {
890 my $require_moderation = {
891 'CANCREQ' => 'CANCREQ',
893 return $require_moderation->{$self->status};
896 =head3 generic_confirm
898 my $stage_summary = $illRequest->generic_confirm;
900 Handle the generic_confirm extended method. The first stage involves creating
901 a template email for the end user to edit in the browser. The second stage
902 attempts to submit the email.
906 sub generic_confirm {
907 my ( $self, $params ) = @_;
908 my $branch = Koha::Libraries->find($params->{current_branchcode})
909 || die "Invalid current branchcode. Are you logged in as the database user?";
910 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
911 my $draft->{subject} = "ILL Request";
912 $draft->{body} = <<EOF;
915 We would like to request an interlibrary loan for a title matching the
916 following description:
920 my $details = $self->metadata;
921 while (my ($title, $value) = each %{$details}) {
922 $draft->{body} .= " - " . $title . ": " . $value . "\n"
925 $draft->{body} .= <<EOF;
927 Please let us know if you are able to supply this to us.
933 my @address = map { $branch->$_ }
934 qw/ branchname branchaddress1 branchaddress2 branchaddress3
935 branchzip branchcity branchstate branchcountry branchphone
938 foreach my $line ( @address ) {
939 $address .= $line . "\n" if $line;
942 $draft->{body} .= $address;
944 my $partners = Koha::Patrons->search({
945 categorycode => $self->_config->partner_code
951 method => 'generic_confirm',
955 partners => $partners,
959 } elsif ( 'draft' eq $params->{stage} ) {
960 # Create the to header
961 my $to = $params->{partners};
963 $to =~ s/^\x00//; # Strip leading NULLs
964 $to =~ s/\x00/; /; # Replace others with '; '
966 Koha::Exceptions::Ill::NoTargetEmail->throw(
967 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
969 # Create the from, replyto and sender headers
970 my $from = $branch->branchemail;
971 my $replyto = $branch->branchreplyto || $from;
972 Koha::Exceptions::Ill::NoLibraryEmail->throw(
973 "Your library has no usable email address. Please set it.")
977 my $message = Koha::Email->new;
978 my %mail = $message->create_message_headers(
983 subject => Encode::encode( "utf8", $params->{subject} ),
984 message => Encode::encode( "utf8", $params->{body} ),
985 contenttype => 'text/plain',
989 my $result = sendmail(%mail);
991 $self->status("GENREQ")->store;
996 method => 'generic_confirm',
1003 status => 'email_failed',
1004 message => $Mail::Sendmail::error,
1005 method => 'generic_confirm',
1010 die "Unknown stage, should not have happened."
1016 my $prefix = $record->id_prefix;
1018 Return the prefix appropriate for the current Illrequest as derived from the
1019 borrower and branch associated with this request's Status, and the config
1026 my $prefix = $self->getPrefix( {
1027 branch => $self->branchcode,
1029 $prefix .= "-" if ( $prefix );
1035 my $params = $illRequest->_censor($params);
1037 Return $params, modified to reflect our censorship requirements.
1042 my ( $self, $params ) = @_;
1043 my $censorship = $self->_config->censorship;
1044 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1045 if ( $params->{opac} );
1046 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1053 $json = $illrequest->TO_JSON
1055 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1056 into the unblessed representation of the object.
1058 TODO: This method does nothing and is not called anywhere. However, bug 74325
1059 touches it, so keeping this for now until both this and bug 74325 are merged,
1060 at which point we can sort it out and remove it completely
1065 my ( $self, $embed ) = @_;
1067 my $object = $self->SUPER::TO_JSON();
1072 =head2 Internal methods
1079 return 'Illrequest';
1084 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>