Bug 24807: (follow-up) Add support for spaces as unknown characters
[koha.git] / Koha / Illrequest.pm
blob5ab0adc21052e693a8f04a4ea6696e89cb8f821d
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>.
20 use Modern::Perl;
22 use Clone 'clone';
23 use File::Basename qw( basename );
24 use Encode qw( encode );
25 use Mail::Sendmail;
26 use Try::Tiny;
27 use DateTime;
29 use Koha::Database;
30 use Koha::DateUtils qw/ dt_from_string /;
31 use Koha::Email;
32 use Koha::Exceptions::Ill;
33 use Koha::Illcomments;
34 use Koha::Illrequestattributes;
35 use Koha::AuthorisedValue;
36 use Koha::Illrequest::Logger;
37 use Koha::Patron;
38 use Koha::AuthorisedValues;
39 use Koha::Biblios;
40 use Koha::Items;
41 use Koha::ItemTypes;
42 use Koha::Libraries;
43 use C4::Circulation qw( CanBookBeIssued AddIssue );
45 use base qw(Koha::Object);
47 =head1 NAME
49 Koha::Illrequest - Koha Illrequest Object class
51 =head1 (Re)Design
53 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
54 of related Illrequestattributes.
56 The former encapsulates the basic necessary information that any ILL requires
57 to be usable in Koha. The latter is a set of additional properties used by
58 one of the backends.
60 The former subsumes the legacy "Status" object. The latter remains
61 encapsulated in the "Record" object.
63 TODO:
65 - Anything invoking the ->status method; annotated with:
66 + # Old use of ->status !
68 =head1 API
70 =head2 Backend API Response Principles
72 All methods should return a hashref in the following format:
74 =over
76 =item * error
78 This should be set to 1 if an error was encountered.
80 =item * status
82 The status should be a string from the list of statuses detailed below.
84 =item * message
86 The message is a free text field that can be passed on to the end user.
88 =item * value
90 The value returned by the method.
92 =back
94 =head2 Interface Status Messages
96 =over
98 =item * branch_address_incomplete
100 An interface request has determined branch address details are incomplete.
102 =item * cancel_success
104 The interface's cancel_request method was successful in cancelling the
105 Illrequest using the API.
107 =item * cancel_fail
109 The interface's cancel_request method failed to cancel the Illrequest using
110 the API.
112 =item * unavailable
114 The interface's request method returned saying that the desired item is not
115 available for request.
117 =back
119 =head2 Class methods
121 =head3 statusalias
123 my $statusalias = $request->statusalias;
125 Returns a request's status alias, as a Koha::AuthorisedValue instance
126 or implicit undef. This is distinct from status_alias, which only returns
127 the value in the status_alias column, this method returns the entire
128 AuthorisedValue object
130 =cut
132 sub statusalias {
133 my ( $self ) = @_;
134 return unless $self->status_alias;
135 # We can't know which result is the right one if there are multiple
136 # ILLSTATUS authorised values with the same authorised_value column value
137 # so we just use the first
138 return Koha::AuthorisedValues->search({
139 branchcode => $self->branchcode,
140 category => 'ILLSTATUS',
141 authorised_value => $self->SUPER::status_alias
142 })->next;
145 =head3 illrequestattributes
147 =cut
149 sub illrequestattributes {
150 my ( $self ) = @_;
151 return Koha::Illrequestattributes->_new_from_dbic(
152 scalar $self->_result->illrequestattributes
156 =head3 illcomments
158 =cut
160 sub illcomments {
161 my ( $self ) = @_;
162 return Koha::Illcomments->_new_from_dbic(
163 scalar $self->_result->illcomments
167 =head3 logs
169 =cut
171 sub logs {
172 my ( $self ) = @_;
173 my $logger = Koha::Illrequest::Logger->new;
174 return $logger->get_request_logs($self);
177 =head3 patron
179 =cut
181 sub patron {
182 my ( $self ) = @_;
183 return Koha::Patron->_new_from_dbic(
184 scalar $self->_result->borrowernumber
188 =head3 status_alias
190 $Illrequest->status_alias(143);
192 Overloaded getter/setter for status_alias,
193 that only returns authorised values from the
194 correct category and records the fact that the status has changed
196 =cut
198 sub status_alias {
199 my ($self, $new_status_alias) = @_;
201 my $current_status_alias = $self->SUPER::status_alias;
203 if ($new_status_alias) {
204 # Keep a record of the previous status before we change it,
205 # we might need it
206 $self->{previous_status} = $current_status_alias ?
207 $current_status_alias :
208 scalar $self->status;
209 # This is hackery to enable us to undefine
210 # status_alias, since we need to have an overloaded
211 # status_alias method to get us around the problem described
212 # here:
213 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
214 # We need a way of accepting implied undef, so we can nullify
215 # the status_alias column, when called from $self->status
216 my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
217 my $ret = $self->SUPER::status_alias($val);
218 my $val_to_log = $val ? $new_status_alias : scalar $self->status;
219 if ($ret) {
220 my $logger = Koha::Illrequest::Logger->new;
221 $logger->log_status_change({
222 request => $self,
223 value => $val_to_log
225 } else {
226 delete $self->{previous_status};
228 return $ret;
230 # We can't know which result is the right one if there are multiple
231 # ILLSTATUS authorised values with the same authorised_value column value
232 # so we just use the first
233 my $alias = Koha::AuthorisedValues->search({
234 branchcode => $self->branchcode,
235 category => 'ILLSTATUS',
236 authorised_value => $self->SUPER::status_alias
237 })->next;
238 if ($alias) {
239 return $alias->authorised_value;
240 } else {
241 return;
245 =head3 status
247 $Illrequest->status('CANREQ');
249 Overloaded getter/setter for request status,
250 also nullifies status_alias and records the fact that the status has changed
252 =cut
254 sub status {
255 my ( $self, $new_status) = @_;
257 my $current_status = $self->SUPER::status;
258 my $current_status_alias = $self->SUPER::status_alias;
260 if ($new_status) {
261 # Keep a record of the previous status before we change it,
262 # we might need it
263 $self->{previous_status} = $current_status_alias ?
264 $current_status_alias :
265 $current_status;
266 my $ret = $self->SUPER::status($new_status)->store;
267 if ($current_status_alias) {
268 # This is hackery to enable us to undefine
269 # status_alias, since we need to have an overloaded
270 # status_alias method to get us around the problem described
271 # here:
272 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
273 # We need a way of passing implied undef to nullify status_alias
274 # so we pass -1, which is special cased in the overloaded setter
275 $self->status_alias("-1");
276 } else {
277 my $logger = Koha::Illrequest::Logger->new;
278 $logger->log_status_change({
279 request => $self,
280 value => $new_status
283 delete $self->{previous_status};
284 return $ret;
285 } else {
286 return $current_status;
290 =head3 load_backend
292 Require "Base.pm" from the relevant ILL backend.
294 =cut
296 sub load_backend {
297 my ( $self, $backend_id ) = @_;
299 my @raw = qw/Koha Illbackends/; # Base Path
301 my $backend_name = $backend_id || $self->backend;
303 unless ( defined $backend_name && $backend_name ne '' ) {
304 Koha::Exceptions::Ill::InvalidBackendId->throw(
305 "An invalid backend ID was requested ('')");
308 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
309 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
310 require $location;
311 $self->{_my_backend} = $backend_class->new({
312 config => $self->_config,
313 logger => Koha::Illrequest::Logger->new
315 return $self;
319 =head3 _backend
321 my $backend = $abstract->_backend($new_backend);
322 my $backend = $abstract->_backend;
324 Getter/Setter for our API object.
326 =cut
328 sub _backend {
329 my ( $self, $backend ) = @_;
330 $self->{_my_backend} = $backend if ( $backend );
331 # Dynamically load our backend object, as late as possible.
332 $self->load_backend unless ( $self->{_my_backend} );
333 return $self->{_my_backend};
336 =head3 _backend_capability
338 my $backend_capability_result = $self->_backend_capability($name, $args);
340 This is a helper method to invoke optional capabilities in the backend. If
341 the capability named by $name is not supported, return 0, else invoke it,
342 passing $args along with the invocation, and return its return value.
344 NOTE: this module suffers from a confusion in termninology:
346 in _backend_capability, the notion of capability refers to an optional feature
347 that is implemented in core, but might not be supported by a given backend.
349 in capabilities & custom_capability, capability refers to entries in the
350 status_graph (after union between backend and core).
352 The easiest way to fix this would be to fix the terminology in
353 capabilities & custom_capability and their callers.
355 =cut
357 sub _backend_capability {
358 my ( $self, $name, $args ) = @_;
359 my $capability = 0;
360 # See if capability is defined in backend
361 try {
362 $capability = $self->_backend->capabilities($name);
363 } catch {
364 return 0;
366 # Try to invoke it
367 if ( $capability && ref($capability) eq 'CODE' ) {
368 return &{$capability}($args);
369 } else {
370 return 0;
374 =head3 _config
376 my $config = $abstract->_config($config);
377 my $config = $abstract->_config;
379 Getter/Setter for our config object.
381 =cut
383 sub _config {
384 my ( $self, $config ) = @_;
385 $self->{_my_config} = $config if ( $config );
386 # Load our config object, as late as possible.
387 unless ( $self->{_my_config} ) {
388 $self->{_my_config} = Koha::Illrequest::Config->new;
390 return $self->{_my_config};
393 =head3 metadata
395 =cut
397 sub metadata {
398 my ( $self ) = @_;
399 return $self->_backend->metadata($self);
402 =head3 _core_status_graph
404 my $core_status_graph = $illrequest->_core_status_graph;
406 Returns ILL module's default status graph. A status graph defines the list of
407 available actions at any stage in the ILL workflow. This is for instance used
408 by the perl script & template to generate the correct buttons to display to
409 the end user at any given point.
411 =cut
413 sub _core_status_graph {
414 my ( $self ) = @_;
415 return {
416 NEW => {
417 prev_actions => [ ], # Actions containing buttons
418 # leading to this status
419 id => 'NEW', # ID of this status
420 name => 'New request', # UI name of this status
421 ui_method_name => 'New request', # UI name of method leading
422 # to this status
423 method => 'create', # method to this status
424 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
425 # requests with this status
426 ui_method_icon => 'fa-plus', # UI Style class
428 REQ => {
429 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
430 id => 'REQ',
431 name => 'Requested',
432 ui_method_name => 'Confirm request',
433 method => 'confirm',
434 next_actions => [ 'REQREV', 'COMP', 'CHK' ],
435 ui_method_icon => 'fa-check',
437 GENREQ => {
438 prev_actions => [ 'NEW', 'REQREV' ],
439 id => 'GENREQ',
440 name => 'Requested from partners',
441 ui_method_name => 'Place request with partners',
442 method => 'generic_confirm',
443 next_actions => [ 'COMP', 'CHK' ],
444 ui_method_icon => 'fa-send-o',
446 REQREV => {
447 prev_actions => [ 'REQ' ],
448 id => 'REQREV',
449 name => 'Request reverted',
450 ui_method_name => 'Revert Request',
451 method => 'cancel',
452 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
453 ui_method_icon => 'fa-times',
455 QUEUED => {
456 prev_actions => [ ],
457 id => 'QUEUED',
458 name => 'Queued request',
459 ui_method_name => 0,
460 method => 0,
461 next_actions => [ 'REQ', 'KILL' ],
462 ui_method_icon => 0,
464 CANCREQ => {
465 prev_actions => [ 'NEW' ],
466 id => 'CANCREQ',
467 name => 'Cancellation requested',
468 ui_method_name => 0,
469 method => 0,
470 next_actions => [ 'KILL', 'REQ' ],
471 ui_method_icon => 0,
473 COMP => {
474 prev_actions => [ 'REQ' ],
475 id => 'COMP',
476 name => 'Completed',
477 ui_method_name => 'Mark completed',
478 method => 'mark_completed',
479 next_actions => [ 'CHK' ],
480 ui_method_icon => 'fa-check',
482 KILL => {
483 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
484 id => 'KILL',
485 name => 0,
486 ui_method_name => 'Delete request',
487 method => 'delete',
488 next_actions => [ ],
489 ui_method_icon => 'fa-trash',
491 CHK => {
492 prev_actions => [ 'REQ', 'GENREQ', 'COMP' ],
493 id => 'CHK',
494 name => 'Checked out',
495 ui_method_name => 'Check out',
496 needs_prefs => [ 'CirculateILL' ],
497 needs_perms => [ 'user_circulate_circulate_remaining_permissions' ],
498 # An array of functions that all must return true
499 needs_all => [ sub { my $r = shift; return $r->biblio; } ],
500 method => 'check_out',
501 next_actions => [ ],
502 ui_method_icon => 'fa-upload',
504 RET => {
505 prev_actions => [ 'CHK' ],
506 id => 'RET',
507 name => 'Returned to library',
508 ui_method_name => 'Check in',
509 method => 'check_in',
510 next_actions => [ 'COMP' ],
511 ui_method_icon => 'fa-download',
516 =head3 _status_graph_union
518 my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
520 Return a new status_graph, the result of merging $origin & new_graph. This is
521 operation is a union over the sets defied by the two graphs.
523 Each entry in $new_graph is added to $origin. We do not provide a syntax for
524 'subtraction' of entries from $origin.
526 Whilst it is not intended that this works, you can override entries in $origin
527 with entries with the same key in $new_graph. This can lead to problematic
528 behaviour when $new_graph adds an entry, which modifies a dependent entry in
529 $origin, only for the entry in $origin to be replaced later with a new entry
530 from $new_graph.
532 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
533 i.e. each of the graphs need to be correct at the outset of the operation.
535 =cut
537 sub _status_graph_union {
538 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
539 # Create new status graph with:
540 # - all core_status_graph
541 # - for-each each backend_status_graph
542 # + add to new status graph
543 # + for each core prev_action:
544 # * locate core_status
545 # * update next_actions with additional next action.
546 # + for each core next_action:
547 # * locate core_status
548 # * update prev_actions with additional prev action
550 my @core_status_ids = keys %{$core_status_graph};
551 my $status_graph = clone($core_status_graph);
553 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
554 my $backend_status = $backend_status_graph->{$backend_status_key};
555 # Add to new status graph
556 $status_graph->{$backend_status_key} = $backend_status;
557 # Update all core methods' next_actions.
558 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
559 if ( grep { $prev_action eq $_ } @core_status_ids ) {
560 my @next_actions =
561 @{$status_graph->{$prev_action}->{next_actions}};
562 push @next_actions, $backend_status_key;
563 $status_graph->{$prev_action}->{next_actions}
564 = \@next_actions;
567 # Update all core methods' prev_actions
568 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
569 if ( grep { $next_action eq $_ } @core_status_ids ) {
570 my @prev_actions =
571 @{$status_graph->{$next_action}->{prev_actions}};
572 push @prev_actions, $backend_status_key;
573 $status_graph->{$next_action}->{prev_actions}
574 = \@prev_actions;
579 return $status_graph;
582 ### Core API methods
584 =head3 capabilities
586 my $capabilities = $illrequest->capabilities;
588 Return a hashref mapping methods to operation names supported by the queried
589 backend.
591 Example return value:
593 { create => "Create Request", confirm => "Progress Request" }
595 NOTE: this module suffers from a confusion in termninology:
597 in _backend_capability, the notion of capability refers to an optional feature
598 that is implemented in core, but might not be supported by a given backend.
600 in capabilities & custom_capability, capability refers to entries in the
601 status_graph (after union between backend and core).
603 The easiest way to fix this would be to fix the terminology in
604 capabilities & custom_capability and their callers.
606 =cut
608 sub capabilities {
609 my ( $self, $status ) = @_;
610 # Generate up to date status_graph
611 my $status_graph = $self->_status_graph_union(
612 $self->_core_status_graph,
613 $self->_backend->status_graph({
614 request => $self,
615 other => {}
618 # Extract available actions from graph.
619 return $status_graph->{$status} if $status;
620 # Or return entire graph.
621 return $status_graph;
624 =head3 custom_capability
626 Return the result of invoking $CANDIDATE on this request's backend with
627 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
629 NOTE: this module suffers from a confusion in termninology:
631 in _backend_capability, the notion of capability refers to an optional feature
632 that is implemented in core, but might not be supported by a given backend.
634 in capabilities & custom_capability, capability refers to entries in the
635 status_graph (after union between backend and core).
637 The easiest way to fix this would be to fix the terminology in
638 capabilities & custom_capability and their callers.
640 =cut
642 sub custom_capability {
643 my ( $self, $candidate, $params ) = @_;
644 foreach my $capability ( values %{$self->capabilities} ) {
645 if ( $candidate eq $capability->{method} ) {
646 my $response =
647 $self->_backend->$candidate({
648 request => $self,
649 other => $params,
651 return $self->expandTemplate($response);
654 return 0;
657 =head3 available_backends
659 Return a list of available backends.
661 =cut
663 sub available_backends {
664 my ( $self, $reduced ) = @_;
665 my $backends = $self->_config->available_backends($reduced);
666 return $backends;
669 =head3 available_actions
671 Return a list of available actions.
673 =cut
675 sub available_actions {
676 my ( $self ) = @_;
677 my $current_action = $self->capabilities($self->status);
678 my @available_actions = map { $self->capabilities($_) }
679 @{$current_action->{next_actions}};
680 return \@available_actions;
683 =head3 mark_completed
685 Mark a request as completed (status = COMP).
687 =cut
689 sub mark_completed {
690 my ( $self ) = @_;
691 $self->status('COMP')->store;
692 $self->completed(dt_from_string())->store;
693 return {
694 error => 0,
695 status => '',
696 message => '',
697 method => 'mark_completed',
698 stage => 'commit',
699 next => 'illview',
703 =head2 backend_migrate
705 Migrate a request from one backend to another.
707 =cut
709 sub backend_migrate {
710 my ( $self, $params ) = @_;
712 my $response = $self->_backend_capability('migrate',{
713 request => $self,
714 other => $params,
716 return $self->expandTemplate($response) if $response;
717 return $response;
720 =head2 backend_confirm
722 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
724 =over
726 =item * orderid
728 =item * accessurl, cost (if available).
730 =back
732 =cut
734 sub backend_confirm {
735 my ( $self, $params ) = @_;
737 my $response = $self->_backend->confirm({
738 request => $self,
739 other => $params,
741 return $self->expandTemplate($response);
744 =head3 backend_update_status
746 =cut
748 sub backend_update_status {
749 my ( $self, $params ) = @_;
750 return $self->expandTemplate($self->_backend->update_status($params));
753 =head3 backend_cancel
755 my $ILLResponse = $illRequest->backend_cancel;
757 The standard interface method allowing for request cancellation.
759 =cut
761 sub backend_cancel {
762 my ( $self, $params ) = @_;
764 my $result = $self->_backend->cancel({
765 request => $self,
766 other => $params
769 return $self->expandTemplate($result);
772 =head3 backend_renew
774 my $renew_response = $illRequest->backend_renew;
776 The standard interface method allowing for request renewal queries.
778 =cut
780 sub backend_renew {
781 my ( $self ) = @_;
782 return $self->expandTemplate(
783 $self->_backend->renew({
784 request => $self,
789 =head3 backend_create
791 my $create_response = $abstractILL->backend_create($params);
793 Return an array of Record objects created by querying our backend with
794 a Search query.
796 In the context of the other ILL methods, this is a special method: we only
797 pass it $params, as it does not yet have any other data associated with it.
799 =cut
801 sub backend_create {
802 my ( $self, $params ) = @_;
804 # Establish whether we need to do a generic copyright clearance.
805 if ($params->{opac}) {
806 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
807 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
808 return {
809 error => 0,
810 status => '',
811 message => '',
812 method => 'create',
813 stage => 'copyrightclearance',
814 value => {
815 other => $params,
816 backend => $self->_backend->name
819 } elsif ( defined $params->{stage}
820 && $params->{stage} eq 'copyrightclearance' ) {
821 $params->{stage} = 'init';
824 # First perform API action, then...
825 my $args = {
826 request => $self,
827 other => $params,
829 my $result = $self->_backend->create($args);
831 # ... simple case: we're not at 'commit' stage.
832 my $stage = $result->{stage};
833 return $self->expandTemplate($result)
834 unless ( 'commit' eq $stage );
836 # ... complex case: commit!
838 # Do we still have space for an ILL or should we queue?
839 my $permitted = $self->check_limits(
840 { patron => $self->patron }, { librarycode => $self->branchcode }
843 # Now augment our committed request.
845 $result->{permitted} = $permitted; # Queue request?
847 # This involves...
849 # ...Updating status!
850 $self->status('QUEUED')->store unless ( $permitted );
852 ## Handle Unmediated ILLs
854 # For the unmediated workflow we only need to delegate to our backend. If
855 # that backend supports unmediateld_ill, it will do its thing and return a
856 # proper response. If it doesn't then _backend_capability returns 0, so
857 # we keep the current result.
858 if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
859 my $unmediated_result = $self->_backend_capability(
860 'unmediated_ill',
861 $args
863 $result = $unmediated_result if $unmediated_result;
866 return $self->expandTemplate($result);
869 =head3 expandTemplate
871 my $params = $abstract->expandTemplate($params);
873 Return a version of $PARAMS augmented with our required template path.
875 =cut
877 sub expandTemplate {
878 my ( $self, $params ) = @_;
879 my $backend = $self->_backend->name;
880 # Generate path to file to load
881 my $backend_dir = $self->_config->backend_dir;
882 my $backend_tmpl = join "/", $backend_dir, $backend;
883 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
884 ( $params->{method}//q{} ) . ".inc";
885 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
886 ( $params->{method}//q{} ) . ".inc";
887 # Set files to load
888 $params->{template} = $intra_tmpl;
889 $params->{opac_template} = $opac_tmpl;
890 return $params;
893 #### Abstract Imports
895 =head3 getLimits
897 my $limit_rules = $abstract->getLimits( {
898 type => 'brw_cat' | 'branch',
899 value => $value
900 } );
902 Return the ILL limit rules for the supplied combination of type / value.
904 As the config may have no rules for this particular type / value combination,
905 or for the default, we must define fall-back values here.
907 =cut
909 sub getLimits {
910 my ( $self, $params ) = @_;
911 my $limits = $self->_config->getLimitRules($params->{type});
913 if ( defined $params->{value}
914 && defined $limits->{$params->{value}} ) {
915 return $limits->{$params->{value}};
917 else {
918 return $limits->{default} || { count => -1, method => 'active' };
922 =head3 getPrefix
924 my $prefix = $abstract->getPrefix( {
925 branch => $branch_code
926 } );
928 Return the ILL prefix as defined by our $params: either per borrower category,
929 per branch or the default.
931 =cut
933 sub getPrefix {
934 my ( $self, $params ) = @_;
935 my $brn_prefixes = $self->_config->getPrefixes();
936 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
939 =head3 get_type
941 my $type = $abstract->get_type();
943 Return a string representing the material type of this request or undef
945 =cut
947 sub get_type {
948 my ($self) = @_;
949 my $attr = $self->illrequestattributes->find({ type => 'type'});
950 return if !$attr;
951 return $attr->value;
954 #### Illrequests Imports
956 =head3 check_limits
958 my $ok = $illRequests->check_limits( {
959 borrower => $borrower,
960 branchcode => 'branchcode' | undef,
961 } );
963 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
964 see whether we are still able to place ILLs.
966 LimitRules are derived from koha-conf.xml:
967 + default limit counts, and counting method
968 + branch specific limit counts & counting method
969 + borrower category specific limit counts & counting method
970 + err on the side of caution: a counting fail will cause fail, even if
971 the other counts passes.
973 =cut
975 sub check_limits {
976 my ( $self, $params ) = @_;
977 my $patron = $params->{patron};
978 my $branchcode = $params->{librarycode} || $patron->branchcode;
980 # Establish maximum number of allowed requests
981 my ( $branch_rules, $brw_rules ) = (
982 $self->getLimits( {
983 type => 'branch',
984 value => $branchcode
985 } ),
986 $self->getLimits( {
987 type => 'brw_cat',
988 value => $patron->categorycode,
989 } ),
991 my ( $branch_limit, $brw_limit )
992 = ( $branch_rules->{count}, $brw_rules->{count} );
993 # Establish currently existing requests
994 my ( $branch_count, $brw_count ) = (
995 $self->_limit_counter(
996 $branch_rules->{method}, { branchcode => $branchcode }
998 $self->_limit_counter(
999 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
1003 # Compare and return
1004 # A limit of -1 means no limit exists.
1005 # We return blocked if either branch limit or brw limit is reached.
1006 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
1007 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
1008 return 0;
1009 } else {
1010 return 1;
1014 sub _limit_counter {
1015 my ( $self, $method, $target ) = @_;
1017 # Establish parameters of counts
1018 my $resultset;
1019 if ($method && $method eq 'annual') {
1020 $resultset = Koha::Illrequests->search({
1021 -and => [
1022 %{$target},
1023 \"YEAR(placed) = YEAR(NOW())"
1026 } else { # assume 'active'
1027 # XXX: This status list is ugly. There should be a method in config
1028 # to return these.
1029 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1030 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1033 # Fetch counts
1034 return $resultset->count;
1037 =head3 requires_moderation
1039 my $status = $illRequest->requires_moderation;
1041 Return the name of the status if moderation by staff is required; or 0
1042 otherwise.
1044 =cut
1046 sub requires_moderation {
1047 my ( $self ) = @_;
1048 my $require_moderation = {
1049 'CANCREQ' => 'CANCREQ',
1051 return $require_moderation->{$self->status};
1054 =head3 biblio
1056 my $biblio = $request->biblio;
1058 For a given request, return the biblio associated with it,
1059 or undef if none exists
1061 =cut
1063 sub biblio {
1064 my ( $self ) = @_;
1066 return if !$self->biblio_id;
1068 return Koha::Biblios->find({
1069 biblionumber => $self->biblio_id
1073 =head3 check_out
1075 my $stage_summary = $request->check_out;
1077 Handle the check_out method. The first stage involves gathering the required
1078 data from the user via a form, the second stage creates an item and tries to
1079 issue it to the patron. If successful, it notifies the patron, then it
1080 returns a summary of how things went
1082 =cut
1084 sub check_out {
1085 my ( $self, $params ) = @_;
1087 # Objects required by the template
1088 my $itemtypes = Koha::ItemTypes->search(
1090 { order_by => ['description'] }
1092 my $libraries = Koha::Libraries->search(
1094 { order_by => ['branchcode'] }
1096 my $biblio = $self->biblio;
1098 # Find all statistical patrons
1099 my $statistical_patrons = Koha::Patrons->search(
1100 { 'category_type' => 'x' },
1101 { join => { 'categorycode' => 'borrowers' } }
1104 if (!$params->{stage} || $params->{stage} eq 'init') {
1105 # Present a form to gather the required data
1107 # We may be viewing this page having previously tried to issue
1108 # the item (in which case, we may already have created an item)
1109 # so we pass the biblio for this request
1110 return {
1111 method => 'check_out',
1112 stage => 'form',
1113 value => {
1114 itemtypes => $itemtypes,
1115 libraries => $libraries,
1116 statistical => $statistical_patrons,
1117 biblio => $biblio
1120 } elsif ($params->{stage} eq 'form') {
1121 # Validate what we've got and return with an error if we fail
1122 my $errors = {};
1123 if (!$params->{item_type} || length $params->{item_type} == 0) {
1124 $errors->{item_type} = 1;
1126 if ($params->{inhouse} && length $params->{inhouse} > 0) {
1127 my $patron_count = Koha::Patrons->search({
1128 cardnumber => $params->{inhouse}
1129 })->count();
1130 if ($patron_count != 1) {
1131 $errors->{inhouse} = 1;
1135 # Check we don't have more than one item for this bib,
1136 # if we do, something very odd is going on
1137 # Having 1 is OK, it means we're likely trying to issue
1138 # following a previously failed attempt, the item exists
1139 # so we'll use it
1140 my @items = $biblio->items->as_list;
1141 my $item_count = scalar @items;
1142 if ($item_count > 1) {
1143 $errors->{itemcount} = 1;
1146 # Failed validation, go back to the form
1147 if (%{$errors}) {
1148 return {
1149 method => 'check_out',
1150 stage => 'form',
1151 value => {
1152 params => $params,
1153 statistical => $statistical_patrons,
1154 itemtypes => $itemtypes,
1155 libraries => $libraries,
1156 biblio => $biblio,
1157 errors => $errors
1162 # Passed validation
1164 # Create an item if one doesn't already exist,
1165 # if one does, use that
1166 my $itemnumber;
1167 if ($item_count == 0) {
1168 my $item_hash = {
1169 biblionumber => $self->biblio_id,
1170 homebranch => $params->{branchcode},
1171 holdingbranch => $params->{branchcode},
1172 location => $params->{branchcode},
1173 itype => $params->{item_type},
1174 barcode => 'ILL-' . $self->illrequest_id
1176 try {
1177 my $item = Koha::Item->new($item_hash)->store;
1178 $itemnumber = $item->itemnumber;
1180 } else {
1181 $itemnumber = $items[0]->itemnumber;
1183 # Check we have an item before going forward
1184 if (!$itemnumber) {
1185 return {
1186 method => 'check_out',
1187 stage => 'form',
1188 value => {
1189 params => $params,
1190 itemtypes => $itemtypes,
1191 libraries => $libraries,
1192 statistical => $statistical_patrons,
1193 errors => { item_creation => 1 }
1198 # Do the check out
1200 # Gather what we need
1201 my $target_item = Koha::Items->find( $itemnumber );
1202 # Determine who we're issuing to
1203 my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
1204 Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
1205 $self->patron;
1207 my @issue_args = (
1208 $patron,
1209 scalar $target_item->barcode
1211 if ($params->{duedate} && length $params->{duedate} > 0) {
1212 push @issue_args, $params->{duedate};
1214 # Check if we can check out
1215 my ( $error, $confirm, $alerts, $messages ) =
1216 C4::Circulation::CanBookBeIssued(@issue_args);
1218 # If we got anything back saying we can't check out,
1219 # return it to the template
1220 my $problems = {};
1221 if ( $error && %{$error} ) { $problems->{error} = $error };
1222 if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
1223 if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
1224 if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
1226 if (%{$problems}) {
1227 return {
1228 method => 'check_out',
1229 stage => 'form',
1230 value => {
1231 params => $params,
1232 itemtypes => $itemtypes,
1233 libraries => $libraries,
1234 statistical => $statistical_patrons,
1235 patron => $patron,
1236 biblio => $biblio,
1237 check_out_errors => $problems
1242 # We can allegedly check out, so make it so
1243 # For some reason, AddIssue requires an unblessed Patron
1244 $issue_args[0] = $patron->unblessed;
1245 my $issue = C4::Circulation::AddIssue(@issue_args);
1247 if ($issue) {
1248 # Update the request status
1249 $self->status('CHK')->store;
1250 return {
1251 method => 'check_out',
1252 stage => 'done_check_out',
1253 value => {
1254 params => $params,
1255 patron => $patron,
1256 check_out => $issue
1259 } else {
1260 return {
1261 method => 'check_out',
1262 stage => 'form',
1263 value => {
1264 params => $params,
1265 itemtypes => $itemtypes,
1266 libraries => $libraries,
1267 errors => { item_check_out => 1 }
1275 =head3 generic_confirm
1277 my $stage_summary = $illRequest->generic_confirm;
1279 Handle the generic_confirm extended method. The first stage involves creating
1280 a template email for the end user to edit in the browser. The second stage
1281 attempts to submit the email.
1283 =cut
1285 sub generic_confirm {
1286 my ( $self, $params ) = @_;
1287 my $branch = Koha::Libraries->find($params->{current_branchcode})
1288 || die "Invalid current branchcode. Are you logged in as the database user?";
1289 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
1290 my $draft->{subject} = "ILL Request";
1291 $draft->{body} = <<EOF;
1292 Dear Sir/Madam,
1294 We would like to request an interlibrary loan for a title matching the
1295 following description:
1299 my $details = $self->metadata;
1300 while (my ($title, $value) = each %{$details}) {
1301 $draft->{body} .= " - " . $title . ": " . $value . "\n"
1302 if $value;
1304 $draft->{body} .= <<EOF;
1306 Please let us know if you are able to supply this to us.
1308 Kind Regards
1312 my @address = map { $branch->$_ }
1313 qw/ branchname branchaddress1 branchaddress2 branchaddress3
1314 branchzip branchcity branchstate branchcountry branchphone
1315 branchemail /;
1316 my $address = "";
1317 foreach my $line ( @address ) {
1318 $address .= $line . "\n" if $line;
1321 $draft->{body} .= $address;
1323 my $partners = Koha::Patrons->search({
1324 categorycode => $self->_config->partner_code
1326 return {
1327 error => 0,
1328 status => '',
1329 message => '',
1330 method => 'generic_confirm',
1331 stage => 'draft',
1332 value => {
1333 draft => $draft,
1334 partners => $partners,
1338 } elsif ( 'draft' eq $params->{stage} ) {
1339 # Create the to header
1340 my $to = $params->{partners};
1341 if ( defined $to ) {
1342 $to =~ s/^\x00//; # Strip leading NULLs
1343 $to =~ s/\x00/; /; # Replace others with '; '
1345 Koha::Exceptions::Ill::NoTargetEmail->throw(
1346 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
1347 if ( !$to );
1348 # Create the from, replyto and sender headers
1349 my $from = $branch->branchemail;
1350 my $replyto = $branch->branchreplyto || $from;
1351 Koha::Exceptions::Ill::NoLibraryEmail->throw(
1352 "Your library has no usable email address. Please set it.")
1353 if ( !$from );
1355 # Create the email
1356 my $message = Koha::Email->new;
1357 my %mail = $message->create_message_headers(
1359 to => $to,
1360 from => $from,
1361 replyto => $replyto,
1362 subject => Encode::encode( "utf8", $params->{subject} ),
1363 message => Encode::encode( "utf8", $params->{body} ),
1364 contenttype => 'text/plain',
1367 # Send it
1368 my $result = sendmail(%mail);
1369 if ( $result ) {
1370 $self->status("GENREQ")->store;
1371 $self->_backend_capability(
1372 'set_requested_partners',
1374 request => $self,
1375 to => $to
1378 return {
1379 error => 0,
1380 status => '',
1381 message => '',
1382 method => 'generic_confirm',
1383 stage => 'commit',
1384 next => 'illview',
1386 } else {
1387 return {
1388 error => 1,
1389 status => 'email_failed',
1390 message => $Mail::Sendmail::error,
1391 method => 'generic_confirm',
1392 stage => 'draft',
1395 } else {
1396 die "Unknown stage, should not have happened."
1400 =head3 id_prefix
1402 my $prefix = $record->id_prefix;
1404 Return the prefix appropriate for the current Illrequest as derived from the
1405 borrower and branch associated with this request's Status, and the config
1406 file.
1408 =cut
1410 sub id_prefix {
1411 my ( $self ) = @_;
1412 my $prefix = $self->getPrefix( {
1413 branch => $self->branchcode,
1414 } );
1415 $prefix .= "-" if ( $prefix );
1416 return $prefix;
1419 =head3 _censor
1421 my $params = $illRequest->_censor($params);
1423 Return $params, modified to reflect our censorship requirements.
1425 =cut
1427 sub _censor {
1428 my ( $self, $params ) = @_;
1429 my $censorship = $self->_config->censorship;
1430 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1431 if ( $params->{opac} );
1432 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1434 return $params;
1437 =head3 store
1439 $Illrequest->store;
1441 Overloaded I<store> method that, in addition to performing the 'store',
1442 possibly records the fact that something happened
1444 =cut
1446 sub store {
1447 my ( $self, $attrs ) = @_;
1449 my $ret = $self->SUPER::store;
1451 $attrs->{log_origin} = 'core';
1453 if ($ret && defined $attrs) {
1454 my $logger = Koha::Illrequest::Logger->new;
1455 $logger->log_maybe({
1456 request => $self,
1457 attrs => $attrs
1461 return $ret;
1464 =head3 requested_partners
1466 my $partners_string = $illRequest->requested_partners;
1468 Return the string representing the email addresses of the partners to
1469 whom a request has been sent
1471 =cut
1473 sub requested_partners {
1474 my ( $self ) = @_;
1475 return $self->_backend_capability(
1476 'get_requested_partners',
1477 { request => $self }
1481 =head3 TO_JSON
1483 $json = $illrequest->TO_JSON
1485 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1486 into the unblessed representation of the object.
1488 TODO: This method does nothing and is not called anywhere. However, bug 74325
1489 touches it, so keeping this for now until both this and bug 74325 are merged,
1490 at which point we can sort it out and remove it completely
1492 =cut
1494 sub TO_JSON {
1495 my ( $self, $embed ) = @_;
1497 my $object = $self->SUPER::TO_JSON();
1499 return $object;
1502 =head2 Internal methods
1504 =head3 _type
1506 =cut
1508 sub _type {
1509 return 'Illrequest';
1512 =head1 AUTHOR
1514 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1515 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>
1517 =cut