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_confirm
532 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
538 =item * accessurl, cost (if available).
544 sub backend_confirm {
545 my ( $self, $params ) = @_;
547 my $response = $self->_backend->confirm({
551 return $self->expandTemplate($response);
554 =head3 backend_update_status
558 sub backend_update_status {
559 my ( $self, $params ) = @_;
560 return $self->expandTemplate($self->_backend->update_status($params));
563 =head3 backend_cancel
565 my $ILLResponse = $illRequest->backend_cancel;
567 The standard interface method allowing for request cancellation.
572 my ( $self, $params ) = @_;
574 my $result = $self->_backend->cancel({
579 return $self->expandTemplate($result);
584 my $renew_response = $illRequest->backend_renew;
586 The standard interface method allowing for request renewal queries.
592 return $self->expandTemplate(
593 $self->_backend->renew({
599 =head3 backend_create
601 my $create_response = $abstractILL->backend_create($params);
603 Return an array of Record objects created by querying our backend with
606 In the context of the other ILL methods, this is a special method: we only
607 pass it $params, as it does not yet have any other data associated with it.
612 my ( $self, $params ) = @_;
614 # Establish whether we need to do a generic copyright clearance.
615 if ($params->{opac}) {
616 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
617 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
623 stage => 'copyrightclearance',
625 backend => $self->_backend->name
628 } elsif ( defined $params->{stage}
629 && $params->{stage} eq 'copyrightclearance' ) {
630 $params->{stage} = 'init';
633 # First perform API action, then...
638 my $result = $self->_backend->create($args);
640 # ... simple case: we're not at 'commit' stage.
641 my $stage = $result->{stage};
642 return $self->expandTemplate($result)
643 unless ( 'commit' eq $stage );
645 # ... complex case: commit!
647 # Do we still have space for an ILL or should we queue?
648 my $permitted = $self->check_limits(
649 { patron => $self->patron }, { librarycode => $self->branchcode }
652 # Now augment our committed request.
654 $result->{permitted} = $permitted; # Queue request?
658 # ...Updating status!
659 $self->status('QUEUED')->store unless ( $permitted );
661 return $self->expandTemplate($result);
664 =head3 expandTemplate
666 my $params = $abstract->expandTemplate($params);
668 Return a version of $PARAMS augmented with our required template path.
673 my ( $self, $params ) = @_;
674 my $backend = $self->_backend->name;
675 # Generate path to file to load
676 my $backend_dir = $self->_config->backend_dir;
677 my $backend_tmpl = join "/", $backend_dir, $backend;
678 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
679 $params->{method} . ".inc";
680 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
681 $params->{method} . ".inc";
683 $params->{template} = $intra_tmpl;
684 $params->{opac_template} = $opac_tmpl;
688 #### Abstract Imports
692 my $limit_rules = $abstract->getLimits( {
693 type => 'brw_cat' | 'branch',
697 Return the ILL limit rules for the supplied combination of type / value.
699 As the config may have no rules for this particular type / value combination,
700 or for the default, we must define fall-back values here.
705 my ( $self, $params ) = @_;
706 my $limits = $self->_config->getLimitRules($params->{type});
708 if ( defined $params->{value}
709 && defined $limits->{$params->{value}} ) {
710 return $limits->{$params->{value}};
713 return $limits->{default} || { count => -1, method => 'active' };
719 my $prefix = $abstract->getPrefix( {
721 branch => $branch_code,
724 Return the ILL prefix as defined by our $params: either per borrower category,
725 per branch or the default.
730 my ( $self, $params ) = @_;
731 my $brn_prefixes = $self->_config->getPrefixes('branch');
732 my $brw_prefixes = $self->_config->getPrefixes('brw_cat');
734 return $brw_prefixes->{$params->{brw_cat}}
735 || $brn_prefixes->{$params->{branch}}
736 || $brw_prefixes->{default}
737 || ""; # "the empty prefix"
742 my $type = $abstract->getType();
744 Return a string representing the material type of this request
750 my $attr = $self->illrequestattributes->find({ type => 'type'});
751 return $attr ? $attr->value : '<span>N/A</span>';
754 #### Illrequests Imports
758 my $ok = $illRequests->check_limits( {
759 borrower => $borrower,
760 branchcode => 'branchcode' | undef,
763 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
764 see whether we are still able to place ILLs.
766 LimitRules are derived from koha-conf.xml:
767 + default limit counts, and counting method
768 + branch specific limit counts & counting method
769 + borrower category specific limit counts & counting method
770 + err on the side of caution: a counting fail will cause fail, even if
771 the other counts passes.
776 my ( $self, $params ) = @_;
777 my $patron = $params->{patron};
778 my $branchcode = $params->{librarycode} || $patron->branchcode;
780 # Establish maximum number of allowed requests
781 my ( $branch_rules, $brw_rules ) = (
788 value => $patron->categorycode,
791 my ( $branch_limit, $brw_limit )
792 = ( $branch_rules->{count}, $brw_rules->{count} );
793 # Establish currently existing requests
794 my ( $branch_count, $brw_count ) = (
795 $self->_limit_counter(
796 $branch_rules->{method}, { branchcode => $branchcode }
798 $self->_limit_counter(
799 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
804 # A limit of -1 means no limit exists.
805 # We return blocked if either branch limit or brw limit is reached.
806 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
807 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
815 my ( $self, $method, $target ) = @_;
817 # Establish parameters of counts
819 if ($method && $method eq 'annual') {
820 $resultset = Koha::Illrequests->search({
823 \"YEAR(placed) = YEAR(NOW())"
826 } else { # assume 'active'
827 # XXX: This status list is ugly. There should be a method in config
829 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
830 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
834 return $resultset->count;
837 =head3 requires_moderation
839 my $status = $illRequest->requires_moderation;
841 Return the name of the status if moderation by staff is required; or 0
846 sub requires_moderation {
848 my $require_moderation = {
849 'CANCREQ' => 'CANCREQ',
851 return $require_moderation->{$self->status};
854 =head3 generic_confirm
856 my $stage_summary = $illRequest->generic_confirm;
858 Handle the generic_confirm extended method. The first stage involves creating
859 a template email for the end user to edit in the browser. The second stage
860 attempts to submit the email.
864 sub generic_confirm {
865 my ( $self, $params ) = @_;
866 my $branch = Koha::Libraries->find($params->{current_branchcode})
867 || die "Invalid current branchcode. Are you logged in as the database user?";
868 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
869 my $draft->{subject} = "ILL Request";
870 $draft->{body} = <<EOF;
873 We would like to request an interlibrary loan for a title matching the
874 following description:
878 my $details = $self->metadata;
879 while (my ($title, $value) = each %{$details}) {
880 $draft->{body} .= " - " . $title . ": " . $value . "\n"
883 $draft->{body} .= <<EOF;
885 Please let us know if you are able to supply this to us.
891 my @address = map { $branch->$_ }
892 qw/ branchname branchaddress1 branchaddress2 branchaddress3
893 branchzip branchcity branchstate branchcountry branchphone
896 foreach my $line ( @address ) {
897 $address .= $line . "\n" if $line;
900 $draft->{body} .= $address;
902 my $partners = Koha::Patrons->search({
903 categorycode => $self->_config->partner_code
909 method => 'generic_confirm',
913 partners => $partners,
917 } elsif ( 'draft' eq $params->{stage} ) {
918 # Create the to header
919 my $to = $params->{partners};
921 $to =~ s/^\x00//; # Strip leading NULLs
922 $to =~ s/\x00/; /; # Replace others with '; '
924 Koha::Exceptions::Ill::NoTargetEmail->throw(
925 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
927 # Create the from, replyto and sender headers
928 my $from = $branch->branchemail;
929 my $replyto = $branch->branchreplyto || $from;
930 Koha::Exceptions::Ill::NoLibraryEmail->throw(
931 "Your library has no usable email address. Please set it.")
935 my $message = Koha::Email->new;
936 my %mail = $message->create_message_headers(
941 subject => Encode::encode( "utf8", $params->{subject} ),
942 message => Encode::encode( "utf8", $params->{body} ),
943 contenttype => 'text/plain',
947 my $result = sendmail(%mail);
949 $self->status("GENREQ")->store;
954 method => 'generic_confirm',
961 status => 'email_failed',
962 message => $Mail::Sendmail::error,
963 method => 'generic_confirm',
968 die "Unknown stage, should not have happened."
974 my $prefix = $record->id_prefix;
976 Return the prefix appropriate for the current Illrequest as derived from the
977 borrower and branch associated with this request's Status, and the config
984 my $brw = $self->patron;
985 my $brw_cat = "dummy";
986 $brw_cat = $brw->categorycode
987 unless ( 'HASH' eq ref($brw) && $brw->{deleted} );
988 my $prefix = $self->getPrefix( {
990 branch => $self->branchcode,
992 $prefix .= "-" if ( $prefix );
998 my $params = $illRequest->_censor($params);
1000 Return $params, modified to reflect our censorship requirements.
1005 my ( $self, $params ) = @_;
1006 my $censorship = $self->_config->censorship;
1007 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1008 if ( $params->{opac} );
1009 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1016 $json = $illrequest->TO_JSON
1018 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1019 into the unblessed representation of the object.
1024 my ( $self, $embed ) = @_;
1026 my $object = $self->SUPER::TO_JSON();
1027 $object->{id_prefix} = $self->id_prefix;
1029 if ( scalar (keys %$embed) ) {
1030 # Augment the request response with patron details if appropriate
1031 if ( $embed->{patron} ) {
1032 my $patron = $self->patron;
1033 $object->{patron} = {
1034 firstname => $patron->firstname,
1035 surname => $patron->surname,
1036 cardnumber => $patron->cardnumber
1039 # Augment the request response with metadata details if appropriate
1040 if ( $embed->{metadata} ) {
1041 $object->{metadata} = $self->metadata;
1043 # Augment the request response with status details if appropriate
1044 if ( $embed->{capabilities} ) {
1045 $object->{capabilities} = $self->capabilities;
1047 # Augment the request response with library details if appropriate
1048 if ( $embed->{library} ) {
1049 $object->{library} = Koha::Libraries->find(
1053 # Augment the request response with the number of comments if appropriate
1054 if ( $embed->{comments} ) {
1055 $object->{comments} = $self->illcomments->count;
1062 =head2 Internal methods
1069 return 'Illrequest';
1074 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>