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/;
28 use Koha::Illrequestattributes;
33 use base qw(Koha::Object);
37 Koha::Illrequest - Koha Illrequest Object class
41 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
42 of related Illrequestattributes.
44 The former encapsulates the basic necessary information that any ILL requires
45 to be usable in Koha. The latter is a set of additional properties used by
48 The former subsumes the legacy "Status" object. The latter remains
49 encapsulated in the "Record" object.
53 - Anything invoking the ->status method; annotated with:
54 + # Old use of ->status !
58 =head2 Backend API Response Principles
60 All methods should return a hashref in the following format:
64 This should be set to 1 if an error was encountered.
68 The status should be a string from the list of statuses detailed below.
72 The message is a free text field that can be passed on to the end user.
76 The value returned by the method.
80 =head2 Interface Status Messages
84 =item * branch_address_incomplete
86 An interface request has determined branch address details are incomplete.
88 =item * cancel_success
90 The interface's cancel_request method was successful in cancelling the
91 Illrequest using the API.
95 The interface's cancel_request method failed to cancel the Illrequest using
100 The interface's request method returned saying that the desired item is not
101 available for request.
115 sub illrequestattributes {
117 return Koha::Illrequestattributes->_new_from_dbic(
118 scalar $self->_result->illrequestattributes
124 return Koha::Patron->_new_from_dbic(
125 scalar $self->_result->borrowernumber
130 my ( $self, $backend_id ) = @_;
132 my @raw = qw/Koha Illbackends/; # Base Path
134 my $backend_name = $backend_id || $self->backend;
135 $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
136 $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
138 $self->{_my_backend} = $backend_class->new({ config => $self->_config });
144 my $backend = $abstract->_backend($new_backend);
145 my $backend = $abstract->_backend;
147 Getter/Setter for our API object.
152 my ( $self, $backend ) = @_;
153 $self->{_my_backend} = $backend if ( $backend );
154 # Dynamically load our backend object, as late as possible.
155 $self->load_backend unless ( $self->{_my_backend} );
156 return $self->{_my_backend};
159 =head3 _backend_capability
161 my $backend_capability_result = $self->_backend_capability($name, $args);
163 This is a helper method to invoke optional capabilities in the backend. If
164 the capability named by $name is not supported, return 0, else invoke it,
165 passing $args along with the invocation, and return its return value.
167 NOTE: this module suffers from a confusion in termninology:
169 in _backend_capability, the notion of capability refers to an optional feature
170 that is implemented in core, but might not be supported by a given backend.
172 in capabilities & custom_capability, capability refers to entries in the
173 status_graph (after union between backend and core).
175 The easiest way to fix this would be to fix the terminology in
176 capabilities & custom_capability and their callers.
180 sub _backend_capability {
181 my ( $self, $name, $args ) = @_;
184 $capability = $self->_backend->capabilities($name);
189 return &{$capability}($args);
197 my $config = $abstract->_config($config);
198 my $config = $abstract->_config;
200 Getter/Setter for our config object.
205 my ( $self, $config ) = @_;
206 $self->{_my_config} = $config if ( $config );
207 # Load our config object, as late as possible.
208 unless ( $self->{_my_config} ) {
209 $self->{_my_config} = Koha::Illrequest::Config->new;
211 return $self->{_my_config};
220 return $self->_backend->metadata($self);
223 =head3 _core_status_graph
225 my $core_status_graph = $illrequest->_core_status_graph;
227 Returns ILL module's default status graph. A status graph defines the list of
228 available actions at any stage in the ILL workflow. This is for instance used
229 by the perl script & template to generate the correct buttons to display to
230 the end user at any given point.
234 sub _core_status_graph {
238 prev_actions => [ ], # Actions containing buttons
239 # leading to this status
240 id => 'NEW', # ID of this status
241 name => 'New request', # UI name of this status
242 ui_method_name => 'New request', # UI name of method leading
244 method => 'create', # method to this status
245 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
246 # requests with this status
247 ui_method_icon => 'fa-plus', # UI Style class
250 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
253 ui_method_name => 'Confirm request',
255 next_actions => [ 'REQREV', 'COMP' ],
256 ui_method_icon => 'fa-check',
259 prev_actions => [ 'NEW', 'REQREV' ],
261 name => 'Requested from partners',
262 ui_method_name => 'Place request with partners',
263 method => 'generic_confirm',
264 next_actions => [ 'COMP' ],
265 ui_method_icon => 'fa-send-o',
268 prev_actions => [ 'REQ' ],
270 name => 'Request reverted',
271 ui_method_name => 'Revert Request',
273 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
274 ui_method_icon => 'fa-times',
279 name => 'Queued request',
282 next_actions => [ 'REQ', 'KILL' ],
286 prev_actions => [ 'NEW' ],
288 name => 'Cancellation requested',
291 next_actions => [ 'KILL', 'REQ' ],
295 prev_actions => [ 'REQ' ],
298 ui_method_name => 'Mark completed',
299 method => 'mark_completed',
301 ui_method_icon => 'fa-check',
304 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
307 ui_method_name => 'Delete request',
310 ui_method_icon => 'fa-trash',
315 =head3 _core_status_graph
317 my $status_graph = $illrequest->_core_status_graph($origin, $new_graph);
319 Return a new status_graph, the result of merging $origin & new_graph. This is
320 operation is a union over the sets defied by the two graphs.
322 Each entry in $new_graph is added to $origin. We do not provide a syntax for
323 'subtraction' of entries from $origin.
325 Whilst it is not intended that this works, you can override entries in $origin
326 with entries with the same key in $new_graph. This can lead to problematic
327 behaviour when $new_graph adds an entry, which modifies a dependent entry in
328 $origin, only for the entry in $origin to be replaced later with a new entry
331 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
332 i.e. each of the graphs need to be correct at the outset of the operation.
336 sub _status_graph_union {
337 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
338 # Create new status graph with:
339 # - all core_status_graph
340 # - for-each each backend_status_graph
341 # + add to new status graph
342 # + for each core prev_action:
343 # * locate core_status
344 # * update next_actions with additional next action.
345 # + for each core next_action:
346 # * locate core_status
347 # * update prev_actions with additional prev action
349 my @core_status_ids = keys %{$core_status_graph};
350 my $status_graph = clone($core_status_graph);
352 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
353 $backend_status = $backend_status_graph->{$backend_status_key};
354 # Add to new status graph
355 $status_graph->{$backend_status_key} = $backend_status;
356 # Update all core methods' next_actions.
357 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
358 if ( grep $prev_action, @core_status_ids ) {
360 @{$status_graph->{$prev_action}->{next_actions}};
361 push @next_actions, $backend_status_key;
362 $status_graph->{$prev_action}->{next_actions}
366 # Update all core methods' prev_actions
367 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
368 if ( grep $next_action, @core_status_ids ) {
370 @{$status_graph->{$next_action}->{prev_actions}};
371 push @prev_actions, $backend_status_key;
372 $status_graph->{$next_action}->{prev_actions}
378 return $status_graph;
385 my $capabilities = $illrequest->capabilities;
387 Return a hashref mapping methods to operation names supported by the queried
390 Example return value:
392 { create => "Create Request", confirm => "Progress Request" }
394 NOTE: this module suffers from a confusion in termninology:
396 in _backend_capability, the notion of capability refers to an optional feature
397 that is implemented in core, but might not be supported by a given backend.
399 in capabilities & custom_capability, capability refers to entries in the
400 status_graph (after union between backend and core).
402 The easiest way to fix this would be to fix the terminology in
403 capabilities & custom_capability and their callers.
408 my ( $self, $status ) = @_;
409 # Generate up to date status_graph
410 my $status_graph = $self->_status_graph_union(
411 $self->_core_status_graph,
412 $self->_backend->status_graph({
417 # Extract available actions from graph.
418 return $status_graph->{$status} if $status;
419 # Or return entire graph.
420 return $status_graph;
423 =head3 custom_capability
425 Return the result of invoking $CANDIDATE on this request's backend with
426 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
428 NOTE: this module suffers from a confusion in termninology:
430 in _backend_capability, the notion of capability refers to an optional feature
431 that is implemented in core, but might not be supported by a given backend.
433 in capabilities & custom_capability, capability refers to entries in the
434 status_graph (after union between backend and core).
436 The easiest way to fix this would be to fix the terminology in
437 capabilities & custom_capability and their callers.
441 sub custom_capability {
442 my ( $self, $candidate, $params ) = @_;
443 foreach my $capability ( values %{$self->capabilities} ) {
444 if ( $candidate eq $capability->{method} ) {
446 $self->_backend->$candidate({
450 return $self->expandTemplate($response);
456 sub available_backends {
458 my $backend_dir = $self->_config->backend_dir;
460 @backends = <$backend_dir/*> if ( $backend_dir );
461 @backends = map { basename($_) } @backends;
465 sub available_actions {
467 my $current_action = $self->capabilities($self->status);
468 my @available_actions = map { $self->capabilities($_) }
469 @{$current_action->{next_actions}};
470 return \@available_actions;
475 $self->status('COMP')->store;
480 method => 'mark_completed',
486 sub backend_confirm {
487 my ( $self, $params ) = @_;
489 # The backend handles setting of mandatory fields in the commit stage:
491 # - accessurl, cost (if available).
492 my $response = $self->_backend->confirm({
496 return $self->expandTemplate($response);
499 sub backend_update_status {
500 my ( $self, $params ) = @_;
501 return $self->expandTemplate($self->_backend->update_status($params));
504 =head3 backend_cancel
506 my $ILLResponse = $illRequest->backend_cancel;
508 The standard interface method allowing for request cancellation.
513 my ( $self, $params ) = @_;
515 my $result = $self->_backend->cancel({
520 return $self->expandTemplate($result);
525 my $renew_response = $illRequest->backend_renew;
527 The standard interface method allowing for request renewal queries.
533 return $self->expandTemplate(
534 $self->_backend->renew({
540 =head3 backend_create
542 my $create_response = $abstractILL->backend_create($params);
544 Return an array of Record objects created by querying our backend with
547 In the context of the other ILL methods, this is a special method: we only
548 pass it $params, as it does not yet have any other data associated with it.
553 my ( $self, $params ) = @_;
555 # Establish whether we need to do a generic copyright clearance.
556 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
557 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
563 stage => 'copyrightclearance',
565 backend => $self->_backend->name
568 } elsif ( $params->{stage} eq 'copyrightclearance' ) {
569 $params->{stage} = 'init';
572 # First perform API action, then...
577 my $result = $self->_backend->create($args);
579 # ... simple case: we're not at 'commit' stage.
580 my $stage = $result->{stage};
581 return $self->expandTemplate($result)
582 unless ( 'commit' eq $stage );
584 # ... complex case: commit!
586 # Do we still have space for an ILL or should we queue?
587 my $permitted = $self->check_limits(
588 { patron => $self->patron }, { librarycode => $self->branchcode }
591 # Now augment our committed request.
593 $result->{permitted} = $permitted; # Queue request?
597 # ...Updating status!
598 $self->status('QUEUED')->store unless ( $permitted );
600 return $self->expandTemplate($result);
603 =head3 expandTemplate
605 my $params = $abstract->expandTemplate($params);
607 Return a version of $PARAMS augmented with our required template path.
612 my ( $self, $params ) = @_;
613 my $backend = $self->_backend->name;
614 # Generate path to file to load
615 my $backend_dir = $self->_config->backend_dir;
616 my $backend_tmpl = join "/", $backend_dir, $backend;
617 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
618 $params->{method} . ".inc";
619 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
620 $params->{method} . ".inc";
622 $params->{template} = $intra_tmpl;
623 $params->{opac_template} = $opac_tmpl;
627 #### Abstract Imports
631 my $limit_rules = $abstract->getLimits( {
632 type => 'brw_cat' | 'branch',
636 Return the ILL limit rules for the supplied combination of type / value.
638 As the config may have no rules for this particular type / value combination,
639 or for the default, we must define fall-back values here.
644 my ( $self, $params ) = @_;
645 my $limits = $self->_config->getLimitRules($params->{type});
647 return $limits->{$params->{value}}
648 || $limits->{default}
649 || { count => -1, method => 'active' };
654 my $prefix = $abstract->getPrefix( {
656 branch => $branch_code,
659 Return the ILL prefix as defined by our $params: either per borrower category,
660 per branch or the default.
665 my ( $self, $params ) = @_;
666 my $brn_prefixes = $self->_config->getPrefixes('branch');
667 my $brw_prefixes = $self->_config->getPrefixes('brw_cat');
669 return $brw_prefixes->{$params->{brw_cat}}
670 || $brn_prefixes->{$params->{branch}}
671 || $brw_prefixes->{default}
672 || ""; # "the empty prefix"
675 #### Illrequests Imports
679 my $ok = $illRequests->check_limits( {
680 borrower => $borrower,
681 branchcode => 'branchcode' | undef,
684 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
685 see whether we are still able to place ILLs.
687 LimitRules are derived from koha-conf.xml:
688 + default limit counts, and counting method
689 + branch specific limit counts & counting method
690 + borrower category specific limit counts & counting method
691 + err on the side of caution: a counting fail will cause fail, even if
692 the other counts passes.
697 my ( $self, $params ) = @_;
698 my $patron = $params->{patron};
699 my $branchcode = $params->{librarycode} || $patron->branchcode;
701 # Establish maximum number of allowed requests
702 my ( $branch_rules, $brw_rules ) = (
709 value => $patron->categorycode,
712 my ( $branch_limit, $brw_limit )
713 = ( $branch_rules->{count}, $brw_rules->{count} );
714 # Establish currently existing requests
715 my ( $branch_count, $brw_count ) = (
716 $self->_limit_counter(
717 $branch_rules->{method}, { branchcode => $branchcode }
719 $self->_limit_counter(
720 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
725 # A limit of -1 means no limit exists.
726 # We return blocked if either branch limit or brw limit is reached.
727 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
728 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
736 my ( $self, $method, $target ) = @_;
738 # Establish parameters of counts
740 if ($method && $method eq 'annual') {
741 $resultset = Koha::Illrequests->search({
744 \"YEAR(placed) = YEAR(NOW())"
747 } else { # assume 'active'
748 # XXX: This status list is ugly. There should be a method in config
750 $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
751 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
755 return $resultset->count;
758 =head3 requires_moderation
760 my $status = $illRequest->requires_moderation;
762 Return the name of the status if moderation by staff is required; or 0
767 sub requires_moderation {
769 my $require_moderation = {
770 'CANCREQ' => 'CANCREQ',
772 return $require_moderation->{$self->status};
775 =head3 generic_confirm
777 my $stage_summary = $illRequest->generic_confirm;
779 Handle the generic_confirm extended method. The first stage involves creating
780 a template email for the end user to edit in the browser. The second stage
781 attempts to submit the email.
785 sub generic_confirm {
786 my ( $self, $params ) = @_;
787 my $branch = Koha::Libraries->find($params->{current_branchcode})
788 || die "Invalid current branchcode. Are you logged in as the database user?";
789 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
790 my $draft->{subject} = "ILL Request";
791 $draft->{body} = <<EOF;
794 We would like to request an interlibrary loan for a title matching the
795 following description:
799 my $details = $self->metadata;
800 while (my ($title, $value) = each %{$details}) {
801 $draft->{body} .= " - " . $title . ": " . $value . "\n"
804 $draft->{body} .= <<EOF;
806 Please let us know if you are able to supply this to us.
812 my @address = map { $branch->$_ }
813 qw/ branchname branchaddress1 branchaddress2 branchaddress3
814 branchzip branchcity branchstate branchcountry branchphone
817 foreach my $line ( @address ) {
818 $address .= $line . "\n" if $line;
821 $draft->{body} .= $address;
823 my $partners = Koha::Patrons->search({
824 categorycode => $self->_config->partner_code
830 method => 'generic_confirm',
834 partners => $partners,
838 } elsif ( 'draft' eq $params->{stage} ) {
839 # Create the to header
840 my $to = $params->{partners};
841 $to =~ s/^\x00//; # Strip leading NULLs
842 $to =~ s/\x00/; /; # Replace others with '; '
843 die "No target email addresses found. Either select at least one partner or check your ILL partner library records." if ( !$to );
844 # Create the from, replyto and sender headers
845 my $from = $branch->branchemail;
846 my $replyto = $branch->branchreplyto || $from;
847 die "Your branch has no email address. Please set it."
850 my $message = Koha::Email->new;
851 my %mail = $message->create_message_headers(
856 subject => Encode::encode( "utf8", $params->{subject} ),
857 message => Encode::encode( "utf8", $params->{body} ),
858 contenttype => 'text/plain',
862 my $result = sendmail(%mail);
864 $self->status("GENREQ")->store;
869 method => 'generic_confirm',
876 status => 'email_failed',
877 message => $Mail::Sendmail::error,
878 method => 'generic_confirm',
883 die "Unknown stage, should not have happened."
889 my $prefix = $record->id_prefix;
891 Return the prefix appropriate for the current Illrequest as derived from the
892 borrower and branch associated with this request's Status, and the config
899 my $brw = $self->patron;
900 my $brw_cat = "dummy";
901 $brw_cat = $brw->categorycode
902 unless ( 'HASH' eq ref($brw) && $brw->{deleted} );
903 my $prefix = $self->getPrefix( {
905 branch => $self->branchcode,
907 $prefix .= "-" if ( $prefix );
913 my $params = $illRequest->_censor($params);
915 Return $params, modified to reflect our censorship requirements.
920 my ( $self, $params ) = @_;
921 my $censorship = $self->_config->censorship;
922 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
923 if ( $params->{opac} );
924 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
931 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>