1 package Koha::Illrequest;
3 # Copyright PTFS Europe 2016,2018
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
22 use Clone qw( clone );
23 use Try::Tiny qw( catch try );
28 use Koha::DateUtils qw( dt_from_string );
29 use Koha::Exceptions::Ill;
30 use Koha::Illcomments;
31 use Koha::Illrequestattributes;
32 use Koha::AuthorisedValue;
33 use Koha::Illrequest::Logger;
35 use Koha::AuthorisedValues;
41 use C4::Circulation qw( CanBookBeIssued AddIssue );
43 use base qw(Koha::Object);
47 Koha::Illrequest - Koha Illrequest Object class
51 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
52 of related Illrequestattributes.
54 The former encapsulates the basic necessary information that any ILL requires
55 to be usable in Koha. The latter is a set of additional properties used by
58 The former subsumes the legacy "Status" object. The latter remains
59 encapsulated in the "Record" object.
63 - Anything invoking the ->status method; annotated with:
64 + # Old use of ->status !
68 =head2 Backend API Response Principles
70 All methods should return a hashref in the following format:
76 This should be set to 1 if an error was encountered.
80 The status should be a string from the list of statuses detailed below.
84 The message is a free text field that can be passed on to the end user.
88 The value returned by the method.
92 =head2 Interface Status Messages
96 =item * branch_address_incomplete
98 An interface request has determined branch address details are incomplete.
100 =item * cancel_success
102 The interface's cancel_request method was successful in cancelling the
103 Illrequest using the API.
107 The interface's cancel_request method failed to cancel the Illrequest using
112 The interface's request method returned saying that the desired item is not
113 available for request.
119 =head3 init_processors
121 $request->init_processors()
123 Initialises an empty processors arrayref
127 sub init_processors {
130 $self->{processors} = [];
133 =head3 push_processor
135 $request->push_processors(sub { ...something... });
137 Pushes a passed processor function into our processors arrayref
142 my ( $self, $processor ) = @_;
143 push @{$self->{processors}}, $processor;
148 my $statusalias = $request->statusalias;
150 Returns a request's status alias, as a Koha::AuthorisedValue instance
151 or implicit undef. This is distinct from status_alias, which only returns
152 the value in the status_alias column, this method returns the entire
153 AuthorisedValue object
159 return unless $self->status_alias;
160 # We can't know which result is the right one if there are multiple
161 # ILL_STATUS_ALIAS authorised values with the same authorised_value column value
162 # so we just use the first
163 return Koha::AuthorisedValues->search(
165 category => 'ILL_STATUS_ALIAS',
166 authorised_value => $self->SUPER::status_alias
173 =head3 illrequestattributes
177 sub illrequestattributes {
179 return Koha::Illrequestattributes->_new_from_dbic(
180 scalar $self->_result->illrequestattributes
190 return Koha::Illcomments->_new_from_dbic(
191 scalar $self->_result->illcomments
197 my $ill_comments = $req->comments;
199 Returns a I<Koha::Illcomments> resultset for the linked comments.
205 return Koha::Illcomments->_new_from_dbic(
206 scalar $self->_result->comments
216 my $logger = Koha::Illrequest::Logger->new;
217 return $logger->get_request_logs($self);
222 my $patron = $request->patron;
224 Returns the linked I<Koha::Patron> object.
231 return Koha::Patron->_new_from_dbic( scalar $self->_result->patron );
236 my $library = $request->library;
238 Returns the linked I<Koha::Library> object.
245 return Koha::Library->_new_from_dbic( scalar $self->_result->library );
248 =head3 ill_extended_attributes
250 my $ill_extended_attributes = $request->ill_extended_attributes;
252 Returns the linked I<Koha::Illrequestattributes> resultset object.
256 sub ill_extended_attributes {
259 return Koha::Illrequestattributes->_new_from_dbic(
260 scalar $self->_result->ill_extended_attributes
266 $Illrequest->status_alias(143);
268 Overloaded getter/setter for status_alias,
269 that only returns authorised values from the
270 correct category and records the fact that the status has changed
275 my ($self, $new_status_alias) = @_;
277 my $current_status_alias = $self->SUPER::status_alias;
279 if ($new_status_alias) {
280 # Keep a record of the previous status before we change it,
282 $self->{previous_status} = $current_status_alias ?
283 $current_status_alias :
284 scalar $self->status;
285 # This is hackery to enable us to undefine
286 # status_alias, since we need to have an overloaded
287 # status_alias method to get us around the problem described
289 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
290 # We need a way of accepting implied undef, so we can nullify
291 # the status_alias column, when called from $self->status
292 my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
293 my $ret = $self->SUPER::status_alias($val);
294 my $val_to_log = $val ? $new_status_alias : scalar $self->status;
296 my $logger = Koha::Illrequest::Logger->new;
297 $logger->log_status_change({
302 delete $self->{previous_status};
306 # We can't know which result is the right one if there are multiple
307 # ILL_STATUS_ALIAS authorised values with the same authorised_value column value
308 # so we just use the first
309 my $alias = Koha::AuthorisedValues->search(
311 category => 'ILL_STATUS_ALIAS',
312 authorised_value => $self->SUPER::status_alias
319 return $alias->authorised_value;
327 $Illrequest->status('CANREQ');
329 Overloaded getter/setter for request status,
330 also nullifies status_alias and records the fact that the status has changed
331 and sends a notice if appropriate
336 my ( $self, $new_status) = @_;
338 my $current_status = $self->SUPER::status;
339 my $current_status_alias = $self->SUPER::status_alias;
342 # Keep a record of the previous status before we change it,
344 $self->{previous_status} = $current_status_alias ?
345 $current_status_alias :
347 my $ret = $self->SUPER::status($new_status)->store;
348 if ($current_status_alias) {
349 # This is hackery to enable us to undefine
350 # status_alias, since we need to have an overloaded
351 # status_alias method to get us around the problem described
353 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
354 # We need a way of passing implied undef to nullify status_alias
355 # so we pass -1, which is special cased in the overloaded setter
356 $self->status_alias("-1");
358 my $logger = Koha::Illrequest::Logger->new;
359 $logger->log_status_change({
364 delete $self->{previous_status};
365 # If status has changed to cancellation requested, send a notice
366 if ($new_status eq 'CANCREQ') {
367 $self->send_staff_notice('ILL_REQUEST_CANCEL');
371 return $current_status;
377 Require "Base.pm" from the relevant ILL backend.
382 my ( $self, $backend_id ) = @_;
384 my @raw = qw/Koha Illbackends/; # Base Path
386 my $backend_name = $backend_id || $self->backend;
388 unless ( defined $backend_name && $backend_name ne '' ) {
389 Koha::Exceptions::Ill::InvalidBackendId->throw(
390 "An invalid backend ID was requested ('')");
393 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
394 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
396 $self->{_my_backend} = $backend_class->new({
397 config => $self->_config,
398 logger => Koha::Illrequest::Logger->new
406 my $backend = $abstract->_backend($new_backend);
407 my $backend = $abstract->_backend;
409 Getter/Setter for our API object.
414 my ( $self, $backend ) = @_;
415 $self->{_my_backend} = $backend if ( $backend );
416 # Dynamically load our backend object, as late as possible.
417 $self->load_backend unless ( $self->{_my_backend} );
418 return $self->{_my_backend};
421 =head3 _backend_capability
423 my $backend_capability_result = $self->_backend_capability($name, $args);
425 This is a helper method to invoke optional capabilities in the backend. If
426 the capability named by $name is not supported, return 0, else invoke it,
427 passing $args along with the invocation, and return its return value.
429 NOTE: this module suffers from a confusion in termninology:
431 in _backend_capability, the notion of capability refers to an optional feature
432 that is implemented in core, but might not be supported by a given backend.
434 in capabilities & custom_capability, capability refers to entries in the
435 status_graph (after union between backend and core).
437 The easiest way to fix this would be to fix the terminology in
438 capabilities & custom_capability and their callers.
442 sub _backend_capability {
443 my ( $self, $name, $args ) = @_;
445 # See if capability is defined in backend
447 $capability = $self->_backend->capabilities($name);
453 if ( $capability && ref($capability) eq 'CODE' ) {
454 return &{$capability}($args);
462 my $config = $abstract->_config($config);
463 my $config = $abstract->_config;
465 Getter/Setter for our config object.
470 my ( $self, $config ) = @_;
471 $self->{_my_config} = $config if ( $config );
472 # Load our config object, as late as possible.
473 unless ( $self->{_my_config} ) {
474 $self->{_my_config} = Koha::Illrequest::Config->new;
476 return $self->{_my_config};
485 return $self->_backend->metadata($self);
488 =head3 _core_status_graph
490 my $core_status_graph = $illrequest->_core_status_graph;
492 Returns ILL module's default status graph. A status graph defines the list of
493 available actions at any stage in the ILL workflow. This is for instance used
494 by the perl script & template to generate the correct buttons to display to
495 the end user at any given point.
499 sub _core_status_graph {
503 prev_actions => [ ], # Actions containing buttons
504 # leading to this status
505 id => 'NEW', # ID of this status
506 name => 'New request', # UI name of this status
507 ui_method_name => 'New request', # UI name of method leading
509 method => 'create', # method to this status
510 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
511 # requests with this status
512 ui_method_icon => 'fa-plus', # UI Style class
515 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
518 ui_method_name => 'Confirm request',
520 next_actions => [ 'REQREV', 'COMP', 'CHK' ],
521 ui_method_icon => 'fa-check',
524 prev_actions => [ 'NEW', 'REQREV' ],
526 name => 'Requested from partners',
527 ui_method_name => 'Place request with partners',
528 method => 'generic_confirm',
529 next_actions => [ 'COMP', 'CHK' ],
530 ui_method_icon => 'fa-send-o',
533 prev_actions => [ 'REQ' ],
535 name => 'Request reverted',
536 ui_method_name => 'Revert Request',
538 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
539 ui_method_icon => 'fa-times',
544 name => 'Queued request',
547 next_actions => [ 'REQ', 'KILL' ],
551 prev_actions => [ 'NEW' ],
553 name => 'Cancellation requested',
556 next_actions => [ 'KILL', 'REQ' ],
560 prev_actions => [ 'REQ' ],
563 ui_method_name => 'Mark completed',
564 method => 'mark_completed',
565 next_actions => [ 'CHK' ],
566 ui_method_icon => 'fa-check',
569 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
572 ui_method_name => 'Delete request',
575 ui_method_icon => 'fa-trash',
578 prev_actions => [ 'REQ', 'GENREQ', 'COMP' ],
580 name => 'Checked out',
581 ui_method_name => 'Check out',
582 needs_prefs => [ 'CirculateILL' ],
583 needs_perms => [ 'user_circulate_circulate_remaining_permissions' ],
584 # An array of functions that all must return true
585 needs_all => [ sub { my $r = shift; return $r->biblio; } ],
586 method => 'check_out',
588 ui_method_icon => 'fa-upload',
591 prev_actions => [ 'CHK' ],
593 name => 'Returned to library',
594 ui_method_name => 'Check in',
595 method => 'check_in',
596 next_actions => [ 'COMP' ],
597 ui_method_icon => 'fa-download',
602 =head3 _status_graph_union
604 my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
606 Return a new status_graph, the result of merging $origin & new_graph. This is
607 operation is a union over the sets defied by the two graphs.
609 Each entry in $new_graph is added to $origin. We do not provide a syntax for
610 'subtraction' of entries from $origin.
612 Whilst it is not intended that this works, you can override entries in $origin
613 with entries with the same key in $new_graph. This can lead to problematic
614 behaviour when $new_graph adds an entry, which modifies a dependent entry in
615 $origin, only for the entry in $origin to be replaced later with a new entry
618 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
619 i.e. each of the graphs need to be correct at the outset of the operation.
623 sub _status_graph_union {
624 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
625 # Create new status graph with:
626 # - all core_status_graph
627 # - for-each each backend_status_graph
628 # + add to new status graph
629 # + for each core prev_action:
630 # * locate core_status
631 # * update next_actions with additional next action.
632 # + for each core next_action:
633 # * locate core_status
634 # * update prev_actions with additional prev action
636 my @core_status_ids = keys %{$core_status_graph};
637 my $status_graph = clone($core_status_graph);
639 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
640 my $backend_status = $backend_status_graph->{$backend_status_key};
641 # Add to new status graph
642 $status_graph->{$backend_status_key} = $backend_status;
643 # Update all core methods' next_actions.
644 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
645 if ( grep { $prev_action eq $_ } @core_status_ids ) {
647 @{$status_graph->{$prev_action}->{next_actions}};
648 push @next_actions, $backend_status_key
649 if (!grep(/^$backend_status_key$/, @next_actions));
650 $status_graph->{$prev_action}->{next_actions}
654 # Update all core methods' prev_actions
655 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
656 if ( grep { $next_action eq $_ } @core_status_ids ) {
658 @{$status_graph->{$next_action}->{prev_actions}};
659 push @prev_actions, $backend_status_key
660 if (!grep(/^$backend_status_key$/, @prev_actions));
661 $status_graph->{$next_action}->{prev_actions}
667 return $status_graph;
674 my $capabilities = $illrequest->capabilities;
676 Return a hashref mapping methods to operation names supported by the queried
679 Example return value:
681 { create => "Create Request", confirm => "Progress Request" }
683 NOTE: this module suffers from a confusion in termninology:
685 in _backend_capability, the notion of capability refers to an optional feature
686 that is implemented in core, but might not be supported by a given backend.
688 in capabilities & custom_capability, capability refers to entries in the
689 status_graph (after union between backend and core).
691 The easiest way to fix this would be to fix the terminology in
692 capabilities & custom_capability and their callers.
697 my ( $self, $status ) = @_;
698 # Generate up to date status_graph
699 my $status_graph = $self->_status_graph_union(
700 $self->_core_status_graph,
701 $self->_backend->status_graph({
706 # Extract available actions from graph.
707 return $status_graph->{$status} if $status;
708 # Or return entire graph.
709 return $status_graph;
712 =head3 custom_capability
714 Return the result of invoking $CANDIDATE on this request's backend with
715 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
717 NOTE: this module suffers from a confusion in termninology:
719 in _backend_capability, the notion of capability refers to an optional feature
720 that is implemented in core, but might not be supported by a given backend.
722 in capabilities & custom_capability, capability refers to entries in the
723 status_graph (after union between backend and core).
725 The easiest way to fix this would be to fix the terminology in
726 capabilities & custom_capability and their callers.
730 sub custom_capability {
731 my ( $self, $candidate, $params ) = @_;
732 foreach my $capability ( values %{$self->capabilities} ) {
733 if ( $candidate eq $capability->{method} ) {
735 $self->_backend->$candidate({
739 return $self->expandTemplate($response);
745 =head3 available_backends
747 Return a list of available backends.
751 sub available_backends {
752 my ( $self, $reduced ) = @_;
753 my $backends = $self->_config->available_backends($reduced);
757 =head3 available_actions
759 Return a list of available actions.
763 sub available_actions {
765 my $current_action = $self->capabilities($self->status);
766 my @available_actions = map { $self->capabilities($_) }
767 @{$current_action->{next_actions}};
768 return \@available_actions;
771 =head3 mark_completed
773 Mark a request as completed (status = COMP).
779 $self->status('COMP')->store;
780 $self->completed(dt_from_string())->store;
785 method => 'mark_completed',
791 =head2 backend_illview
793 View and manage an ILL request
797 sub backend_illview {
798 my ( $self, $params ) = @_;
800 my $response = $self->_backend_capability('illview',{
804 return $self->expandTemplate($response) if $response;
808 =head2 backend_migrate
810 Migrate a request from one backend to another.
814 sub backend_migrate {
815 my ( $self, $params ) = @_;
816 # Set the request's backend to be the destination backend
817 $self->load_backend($params->{backend});
818 my $response = $self->_backend_capability('migrate',{
822 return $self->expandTemplate($response) if $response;
826 =head2 backend_confirm
828 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
834 =item * accessurl, cost (if available).
840 sub backend_confirm {
841 my ( $self, $params ) = @_;
843 my $response = $self->_backend->confirm({
847 return $self->expandTemplate($response);
850 =head3 backend_update_status
854 sub backend_update_status {
855 my ( $self, $params ) = @_;
856 return $self->expandTemplate($self->_backend->update_status($params));
859 =head3 backend_cancel
861 my $ILLResponse = $illRequest->backend_cancel;
863 The standard interface method allowing for request cancellation.
868 my ( $self, $params ) = @_;
870 my $result = $self->_backend->cancel({
875 return $self->expandTemplate($result);
880 my $renew_response = $illRequest->backend_renew;
882 The standard interface method allowing for request renewal queries.
888 return $self->expandTemplate(
889 $self->_backend->renew({
895 =head3 backend_create
897 my $create_response = $abstractILL->backend_create($params);
899 Return an array of Record objects created by querying our backend with
902 In the context of the other ILL methods, this is a special method: we only
903 pass it $params, as it does not yet have any other data associated with it.
908 my ( $self, $params ) = @_;
910 # Establish whether we need to do a generic copyright clearance.
911 if ($params->{opac}) {
912 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
913 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
919 stage => 'copyrightclearance',
922 backend => $self->_backend->name
925 } elsif ( defined $params->{stage}
926 && $params->{stage} eq 'copyrightclearance' ) {
927 $params->{stage} = 'init';
930 # First perform API action, then...
935 my $result = $self->_backend->create($args);
937 # ... simple case: we're not at 'commit' stage.
938 my $stage = $result->{stage};
939 return $self->expandTemplate($result)
940 unless ( 'commit' eq $stage );
942 # ... complex case: commit!
944 # Do we still have space for an ILL or should we queue?
945 my $permitted = $self->check_limits(
946 { patron => $self->patron }, { librarycode => $self->branchcode }
949 # Now augment our committed request.
951 $result->{permitted} = $permitted; # Queue request?
955 # ...Updating status!
956 $self->status('QUEUED')->store unless ( $permitted );
958 ## Handle Unmediated ILLs
960 # For the unmediated workflow we only need to delegate to our backend. If
961 # that backend supports unmediateld_ill, it will do its thing and return a
962 # proper response. If it doesn't then _backend_capability returns 0, so
963 # we keep the current result.
964 if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
965 my $unmediated_result = $self->_backend_capability(
969 $result = $unmediated_result if $unmediated_result;
972 return $self->expandTemplate($result);
975 =head3 backend_get_update
977 my $update = backend_get_update($request);
979 Given a request, returns an update in a prescribed
980 format that can then be passed to update parsers
984 sub backend_get_update {
985 my ( $self, $options ) = @_;
987 my $response = $self->_backend_capability(
988 'get_supplier_update',
997 =head3 expandTemplate
999 my $params = $abstract->expandTemplate($params);
1001 Return a version of $PARAMS augmented with our required template path.
1005 sub expandTemplate {
1006 my ( $self, $params ) = @_;
1007 my $backend = $self->_backend->name;
1008 # Generate path to file to load
1009 my $backend_dir = $self->_config->backend_dir;
1010 my $backend_tmpl = join "/", $backend_dir, $backend;
1011 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
1012 ( $params->{method}//q{} ) . ".inc";
1013 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
1014 ( $params->{method}//q{} ) . ".inc";
1016 $params->{template} = $intra_tmpl;
1017 $params->{opac_template} = $opac_tmpl;
1021 #### Abstract Imports
1025 my $limit_rules = $abstract->getLimits( {
1026 type => 'brw_cat' | 'branch',
1030 Return the ILL limit rules for the supplied combination of type / value.
1032 As the config may have no rules for this particular type / value combination,
1033 or for the default, we must define fall-back values here.
1038 my ( $self, $params ) = @_;
1039 my $limits = $self->_config->getLimitRules($params->{type});
1041 if ( defined $params->{value}
1042 && defined $limits->{$params->{value}} ) {
1043 return $limits->{$params->{value}};
1046 return $limits->{default} || { count => -1, method => 'active' };
1052 my $prefix = $abstract->getPrefix( {
1053 branch => $branch_code
1056 Return the ILL prefix as defined by our $params: either per borrower category,
1057 per branch or the default.
1062 my ( $self, $params ) = @_;
1063 my $brn_prefixes = $self->_config->getPrefixes();
1064 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
1069 my $type = $abstract->get_type();
1071 Return a string representing the material type of this request or undef
1077 my $attr = $self->illrequestattributes->find({ type => 'type'});
1079 return $attr->value;
1082 #### Illrequests Imports
1086 my $ok = $illRequests->check_limits( {
1087 borrower => $borrower,
1088 branchcode => 'branchcode' | undef,
1091 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
1092 see whether we are still able to place ILLs.
1094 LimitRules are derived from koha-conf.xml:
1095 + default limit counts, and counting method
1096 + branch specific limit counts & counting method
1097 + borrower category specific limit counts & counting method
1098 + err on the side of caution: a counting fail will cause fail, even if
1099 the other counts passes.
1104 my ( $self, $params ) = @_;
1105 my $patron = $params->{patron};
1106 my $branchcode = $params->{librarycode} || $patron->branchcode;
1108 # Establish maximum number of allowed requests
1109 my ( $branch_rules, $brw_rules ) = (
1112 value => $branchcode
1116 value => $patron->categorycode,
1119 my ( $branch_limit, $brw_limit )
1120 = ( $branch_rules->{count}, $brw_rules->{count} );
1121 # Establish currently existing requests
1122 my ( $branch_count, $brw_count ) = (
1123 $self->_limit_counter(
1124 $branch_rules->{method}, { branchcode => $branchcode }
1126 $self->_limit_counter(
1127 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
1131 # Compare and return
1132 # A limit of -1 means no limit exists.
1133 # We return blocked if either branch limit or brw limit is reached.
1134 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
1135 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
1142 sub _limit_counter {
1143 my ( $self, $method, $target ) = @_;
1145 # Establish parameters of counts
1147 if ($method && $method eq 'annual') {
1148 $resultset = Koha::Illrequests->search({
1151 \"YEAR(placed) = YEAR(NOW())"
1154 } else { # assume 'active'
1155 # XXX: This status list is ugly. There should be a method in config
1157 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1158 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1162 return $resultset->count;
1165 =head3 requires_moderation
1167 my $status = $illRequest->requires_moderation;
1169 Return the name of the status if moderation by staff is required; or 0
1174 sub requires_moderation {
1176 my $require_moderation = {
1177 'CANCREQ' => 'CANCREQ',
1179 return $require_moderation->{$self->status};
1184 my $biblio = $request->biblio;
1186 For a given request, return the biblio associated with it,
1187 or undef if none exists
1193 my $biblio_rs = $self->_result->biblio;
1194 return unless $biblio_rs;
1195 return Koha::Biblio->_new_from_dbic($biblio_rs);
1200 my $stage_summary = $request->check_out;
1202 Handle the check_out method. The first stage involves gathering the required
1203 data from the user via a form, the second stage creates an item and tries to
1204 issue it to the patron. If successful, it notifies the patron, then it
1205 returns a summary of how things went
1210 my ( $self, $params ) = @_;
1212 # Objects required by the template
1213 my $itemtypes = Koha::ItemTypes->search(
1215 { order_by => ['description'] }
1217 my $libraries = Koha::Libraries->search(
1219 { order_by => ['branchcode'] }
1221 my $biblio = $self->biblio;
1223 # Find all statistical patrons
1224 my $statistical_patrons = Koha::Patrons->search(
1225 { 'category_type' => 'x' },
1226 { join => { 'categorycode' => 'borrowers' } }
1229 if (!$params->{stage} || $params->{stage} eq 'init') {
1230 # Present a form to gather the required data
1232 # We may be viewing this page having previously tried to issue
1233 # the item (in which case, we may already have created an item)
1234 # so we pass the biblio for this request
1236 method => 'check_out',
1239 itemtypes => $itemtypes,
1240 libraries => $libraries,
1241 statistical => $statistical_patrons,
1245 } elsif ($params->{stage} eq 'form') {
1246 # Validate what we've got and return with an error if we fail
1248 if (!$params->{item_type} || length $params->{item_type} == 0) {
1249 $errors->{item_type} = 1;
1251 if ($params->{inhouse} && length $params->{inhouse} > 0) {
1252 my $patron_count = Koha::Patrons->search({
1253 cardnumber => $params->{inhouse}
1255 if ($patron_count != 1) {
1256 $errors->{inhouse} = 1;
1260 # Check we don't have more than one item for this bib,
1261 # if we do, something very odd is going on
1262 # Having 1 is OK, it means we're likely trying to issue
1263 # following a previously failed attempt, the item exists
1265 my @items = $biblio->items->as_list;
1266 my $item_count = scalar @items;
1267 if ($item_count > 1) {
1268 $errors->{itemcount} = 1;
1271 # Failed validation, go back to the form
1274 method => 'check_out',
1278 statistical => $statistical_patrons,
1279 itemtypes => $itemtypes,
1280 libraries => $libraries,
1289 # Create an item if one doesn't already exist,
1290 # if one does, use that
1292 if ($item_count == 0) {
1294 biblionumber => $self->biblio_id,
1295 homebranch => $params->{branchcode},
1296 holdingbranch => $params->{branchcode},
1297 location => $params->{branchcode},
1298 itype => $params->{item_type},
1299 barcode => 'ILL-' . $self->illrequest_id
1302 my $item = Koha::Item->new($item_hash)->store;
1303 $itemnumber = $item->itemnumber;
1306 $itemnumber = $items[0]->itemnumber;
1308 # Check we have an item before going forward
1311 method => 'check_out',
1315 itemtypes => $itemtypes,
1316 libraries => $libraries,
1317 statistical => $statistical_patrons,
1318 errors => { item_creation => 1 }
1325 # Gather what we need
1326 my $target_item = Koha::Items->find( $itemnumber );
1327 # Determine who we're issuing to
1328 my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
1329 Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
1334 scalar $target_item->barcode
1336 if ($params->{duedate} && length $params->{duedate} > 0) {
1337 push @issue_args, dt_from_string($params->{duedate});
1339 # Check if we can check out
1340 my ( $error, $confirm, $alerts, $messages ) =
1341 C4::Circulation::CanBookBeIssued(@issue_args);
1343 # If we got anything back saying we can't check out,
1344 # return it to the template
1346 if ( $error && %{$error} ) { $problems->{error} = $error };
1347 if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
1348 if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
1349 if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
1353 method => 'check_out',
1357 itemtypes => $itemtypes,
1358 libraries => $libraries,
1359 statistical => $statistical_patrons,
1362 check_out_errors => $problems
1367 # We can allegedly check out, so make it so
1368 # For some reason, AddIssue requires an unblessed Patron
1369 $issue_args[0] = $patron->unblessed;
1370 my $issue = C4::Circulation::AddIssue(@issue_args);
1373 # Update the request status
1374 $self->status('CHK')->store;
1376 method => 'check_out',
1377 stage => 'done_check_out',
1386 method => 'check_out',
1390 itemtypes => $itemtypes,
1391 libraries => $libraries,
1392 errors => { item_check_out => 1 }
1400 =head3 generic_confirm
1402 my $stage_summary = $illRequest->generic_confirm;
1404 Handle the generic_confirm extended method. The first stage involves creating
1405 a template email for the end user to edit in the browser. The second stage
1406 attempts to submit the email.
1410 sub generic_confirm {
1411 my ( $self, $params ) = @_;
1412 my $branch = Koha::Libraries->find($params->{current_branchcode})
1413 || die "Invalid current branchcode. Are you logged in as the database user?";
1414 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
1415 # Get the message body from the notice definition
1416 my $letter = $self->get_notice({
1417 notice_code => 'ILL_PARTNER_REQ',
1418 transport => 'email'
1421 my $partners = Koha::Patrons->search({
1422 categorycode => $self->_config->partner_code
1428 method => 'generic_confirm',
1432 subject => $letter->{title},
1433 body => $letter->{content}
1435 partners => $partners,
1439 } elsif ( 'draft' eq $params->{stage} ) {
1440 # Create the to header
1441 my $to = $params->{partners};
1442 if ( defined $to ) {
1443 $to =~ s/^\x00//; # Strip leading NULLs
1445 Koha::Exceptions::Ill::NoTargetEmail->throw(
1446 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
1449 # Take the null delimited string that we receive and create
1450 # an array of associated patron objects
1451 my @to_patrons = map {
1452 Koha::Patrons->find({ borrowernumber => $_ })
1453 } split(/\x00/, $to);
1455 # Create the from, replyto and sender headers
1456 my $from = $branch->from_email_address;
1457 my $replyto = $branch->inbound_ill_address;
1458 Koha::Exceptions::Ill::NoLibraryEmail->throw(
1459 "Your library has no usable email address. Please set it.")
1462 # So we get a notice hashref, then substitute the possibly
1463 # modified title and body from the draft stage
1464 my $letter = $self->get_notice({
1465 notice_code => 'ILL_PARTNER_REQ',
1466 transport => 'email'
1468 $letter->{title} = $params->{subject};
1469 $letter->{content} = $params->{body};
1473 # Keep track of who received this notice
1475 # Iterate our array of recipient patron objects
1476 foreach my $patron(@to_patrons) {
1477 # Create the params we pass to the notice
1480 borrowernumber => $patron->borrowernumber,
1481 message_transport_type => 'email',
1482 to_address => $patron->email,
1483 from_address => $from,
1484 reply_address => $replyto
1486 my $result = C4::Letters::EnqueueLetter($params);
1488 push @queued, $patron->email;
1492 # If all notices were queued successfully,
1494 if (scalar @queued == scalar @to_patrons) {
1495 $self->status("GENREQ")->store;
1496 $self->_backend_capability(
1497 'set_requested_partners',
1500 to => join("; ", @queued)
1507 method => 'generic_confirm',
1516 status => 'email_failed',
1517 message => 'Email queueing failed',
1518 method => 'generic_confirm',
1522 die "Unknown stage, should not have happened."
1526 =head3 send_patron_notice
1528 my $result = $request->send_patron_notice($notice_code);
1530 Send a specified notice regarding this request to a patron
1534 sub send_patron_notice {
1535 my ( $self, $notice_code, $additional_text ) = @_;
1537 # We need a notice code
1538 if (!$notice_code) {
1540 error => 'notice_no_type'
1544 # Map from the notice code to the messaging preference
1545 my %message_name = (
1546 ILL_PICKUP_READY => 'Ill_ready',
1547 ILL_REQUEST_UNAVAIL => 'Ill_unavailable',
1548 ILL_REQUEST_UPDATE => 'Ill_update'
1551 # Get the patron's messaging preferences
1552 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
1553 borrowernumber => $self->borrowernumber,
1554 message_name => $message_name{$notice_code}
1556 my @transports = keys %{ $borrower_preferences->{transports} };
1558 # Notice should come from the library where the request was placed,
1559 # not the patrons home library
1560 my $branch = Koha::Libraries->find($self->branchcode);
1561 my $from_address = $branch->from_email_address;
1562 my $reply_address = $branch->inbound_ill_address;
1564 # Send the notice to the patron via the chosen transport methods
1565 # and record the results
1568 for my $transport (@transports) {
1569 my $letter = $self->get_notice({
1570 notice_code => $notice_code,
1571 transport => $transport,
1572 additional_text => $additional_text
1575 my $result = C4::Letters::EnqueueLetter({
1577 borrowernumber => $self->borrowernumber,
1578 message_transport_type => $transport,
1579 from_address => $from_address,
1580 reply_address => $reply_address
1583 push @success, $transport;
1585 push @fail, $transport;
1588 push @fail, $transport;
1591 if (scalar @success > 0) {
1592 my $logger = Koha::Illrequest::Logger->new;
1593 $logger->log_patron_notice({
1595 notice_code => $notice_code
1600 success => \@success,
1606 =head3 send_staff_notice
1608 my $result = $request->send_staff_notice($notice_code);
1610 Send a specified notice regarding this request to staff
1614 sub send_staff_notice {
1615 my ( $self, $notice_code ) = @_;
1617 # We need a notice code
1618 if (!$notice_code) {
1620 error => 'notice_no_type'
1624 # Get the staff notices that have been assigned for sending in
1626 my $staff_to_send = C4::Context->preference('ILLSendStaffNotices') // q{};
1628 # If it hasn't been enabled in the syspref, we don't want to send it
1629 if ($staff_to_send !~ /\b$notice_code\b/) {
1631 error => 'notice_not_enabled'
1635 my $letter = $self->get_notice({
1636 notice_code => $notice_code,
1637 transport => 'email'
1640 # Try and get an address to which to send staff notices
1641 my $branch = Koha::Libraries->find($self->branchcode);
1642 my $to_address = $branch->inbound_ill_address;
1643 my $from_address = $branch->inbound_ill_address;
1647 borrowernumber => $self->borrowernumber,
1648 message_transport_type => 'email',
1649 from_address => $from_address
1653 $params->{to_address} = $to_address;
1656 error => 'notice_no_create'
1661 C4::Letters::EnqueueLetter($params)
1662 or warn "can't enqueue letter $letter";
1664 success => 'notice_queued'
1668 error => 'notice_no_create'
1675 my $notice = $request->get_notice($params);
1677 Return a compiled notice hashref for the passed notice code
1683 my ( $self, $params ) = @_;
1685 my $title = $self->illrequestattributes->find(
1688 my $author = $self->illrequestattributes->find(
1689 { type => 'author' }
1691 my $metahash = $self->metadata;
1693 foreach my $key (sort { lc $a cmp lc $b } keys %{$metahash}) {
1694 my $value = $metahash->{$key};
1695 push @metaarray, "- $key: $value" if $value;
1697 my $metastring = join("\n", @metaarray);
1698 my $letter = C4::Letters::GetPreparedLetter(
1700 letter_code => $params->{notice_code},
1701 branchcode => $self->branchcode,
1702 message_transport_type => $params->{transport},
1703 lang => $self->patron->lang,
1705 illrequests => $self->illrequest_id,
1706 borrowers => $self->borrowernumber,
1707 biblio => $self->biblio_id,
1708 branches => $self->branchcode,
1711 ill_bib_title => $title ? $title->value : '',
1712 ill_bib_author => $author ? $author->value : '',
1713 ill_full_metadata => $metastring,
1714 additional_text => $params->{additional_text}
1722 =head3 attach_processors
1724 Receive a Koha::Illrequest::SupplierUpdate and attach
1725 any processors we have for it
1729 sub attach_processors {
1730 my ( $self, $update ) = @_;
1732 foreach my $processor(@{$self->{processors}}) {
1734 $processor->{target_source_type} eq $update->{source_type} &&
1735 $processor->{target_source_name} eq $update->{source_name}
1737 $update->attach_processor($processor);
1742 =head3 append_to_note
1744 append_to_note("Some text");
1746 Append some text to the staff note
1750 sub append_to_note {
1751 my ($self, $text) = @_;
1752 my $current = $self->notesstaff;
1753 $text = ($current && length $current > 0) ? "$current\n\n$text" : $text;
1754 $self->notesstaff($text)->store;
1759 my $prefix = $record->id_prefix;
1761 Return the prefix appropriate for the current Illrequest as derived from the
1762 borrower and branch associated with this request's Status, and the config
1769 my $prefix = $self->getPrefix( {
1770 branch => $self->branchcode,
1772 $prefix .= "-" if ( $prefix );
1778 my $params = $illRequest->_censor($params);
1780 Return $params, modified to reflect our censorship requirements.
1785 my ( $self, $params ) = @_;
1786 my $censorship = $self->_config->censorship;
1787 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1788 if ( $params->{opac} );
1789 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1798 Overloaded I<store> method that, in addition to performing the 'store',
1799 possibly records the fact that something happened
1804 my ( $self, $attrs ) = @_;
1806 my %updated_columns = $self->_result->get_dirty_columns;
1809 if( $self->in_storage and defined $updated_columns{'borrowernumber'} and
1810 Koha::Patrons->find( $updated_columns{'borrowernumber'} ) )
1812 # borrowernumber has changed
1813 my $old_illreq = $self->get_from_storage;
1814 @holds = Koha::Holds->search( {
1815 borrowernumber => $old_illreq->borrowernumber,
1816 biblionumber => $self->biblio_id,
1817 } )->as_list if $old_illreq;
1820 my $ret = $self->SUPER::store;
1822 if ( scalar @holds ) {
1823 # move holds to the changed borrowernumber
1824 foreach my $hold ( @holds ) {
1825 $hold->borrowernumber( $updated_columns{'borrowernumber'} )->store;
1829 $attrs->{log_origin} = 'core';
1831 if ($ret && defined $attrs) {
1832 my $logger = Koha::Illrequest::Logger->new;
1833 $logger->log_maybe({
1842 =head3 requested_partners
1844 my $partners_string = $illRequest->requested_partners;
1846 Return the string representing the email addresses of the partners to
1847 whom a request has been sent
1851 sub requested_partners {
1853 return $self->_backend_capability(
1854 'get_requested_partners',
1855 { request => $self }
1861 $json = $illrequest->TO_JSON
1863 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1864 into the unblessed representation of the object.
1866 TODO: This method does nothing and is not called anywhere. However, bug 74325
1867 touches it, so keeping this for now until both this and bug 74325 are merged,
1868 at which point we can sort it out and remove it completely
1873 my ( $self, $embed ) = @_;
1875 my $object = $self->SUPER::TO_JSON();
1880 =head2 Internal methods
1887 return 'Illrequest';
1892 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1893 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>