Bug 21443: Update Schema
[koha.git] / Koha / Illrequest.pm
blobbc39152d58793851bfd234a1dc0d31ffb1e58c9e
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::Email;
31 use Koha::Exceptions::Ill;
32 use Koha::Illcomments;
33 use Koha::Illrequestattributes;
34 use Koha::AuthorisedValue;
35 use Koha::Illrequest::Logger;
36 use Koha::Patron;
37 use Koha::AuthorisedValues;
38 use Koha::Biblios;
39 use Koha::Items;
40 use Koha::ItemTypes;
41 use Koha::Libraries;
42 use C4::Circulation qw( CanBookBeIssued AddIssue );
44 use base qw(Koha::Object);
46 =head1 NAME
48 Koha::Illrequest - Koha Illrequest Object class
50 =head1 (Re)Design
52 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
53 of related Illrequestattributes.
55 The former encapsulates the basic necessary information that any ILL requires
56 to be usable in Koha. The latter is a set of additional properties used by
57 one of the backends.
59 The former subsumes the legacy "Status" object. The latter remains
60 encapsulated in the "Record" object.
62 TODO:
64 - Anything invoking the ->status method; annotated with:
65 + # Old use of ->status !
67 =head1 API
69 =head2 Backend API Response Principles
71 All methods should return a hashref in the following format:
73 =over
75 =item * error
77 This should be set to 1 if an error was encountered.
79 =item * status
81 The status should be a string from the list of statuses detailed below.
83 =item * message
85 The message is a free text field that can be passed on to the end user.
87 =item * value
89 The value returned by the method.
91 =back
93 =head2 Interface Status Messages
95 =over
97 =item * branch_address_incomplete
99 An interface request has determined branch address details are incomplete.
101 =item * cancel_success
103 The interface's cancel_request method was successful in cancelling the
104 Illrequest using the API.
106 =item * cancel_fail
108 The interface's cancel_request method failed to cancel the Illrequest using
109 the API.
111 =item * unavailable
113 The interface's request method returned saying that the desired item is not
114 available for request.
116 =back
118 =head2 Class methods
120 =head3 statusalias
122 my $statusalias = $request->statusalias;
124 Returns a request's status alias, as a Koha::AuthorisedValue instance
125 or implicit undef. This is distinct from status_alias, which only returns
126 the value in the status_alias column, this method returns the entire
127 AuthorisedValue object
129 =cut
131 sub statusalias {
132 my ( $self ) = @_;
133 return unless $self->status_alias;
134 # We can't know which result is the right one if there are multiple
135 # ILLSTATUS authorised values with the same authorised_value column value
136 # so we just use the first
137 return Koha::AuthorisedValues->search({
138 branchcode => $self->branchcode,
139 category => 'ILLSTATUS',
140 authorised_value => $self->SUPER::status_alias
141 })->next;
144 =head3 illrequestattributes
146 =cut
148 sub illrequestattributes {
149 my ( $self ) = @_;
150 return Koha::Illrequestattributes->_new_from_dbic(
151 scalar $self->_result->illrequestattributes
155 =head3 illcomments
157 =cut
159 sub illcomments {
160 my ( $self ) = @_;
161 return Koha::Illcomments->_new_from_dbic(
162 scalar $self->_result->illcomments
166 =head3 logs
168 =cut
170 sub logs {
171 my ( $self ) = @_;
172 my $logger = Koha::Illrequest::Logger->new;
173 return $logger->get_request_logs($self);
176 =head3 patron
178 =cut
180 sub patron {
181 my ( $self ) = @_;
182 return Koha::Patron->_new_from_dbic(
183 scalar $self->_result->borrowernumber
187 =head3 status_alias
189 $Illrequest->status_alias(143);
191 Overloaded getter/setter for status_alias,
192 that only returns authorised values from the
193 correct category and records the fact that the status has changed
195 =cut
197 sub status_alias {
198 my ($self, $new_status_alias) = @_;
200 my $current_status_alias = $self->SUPER::status_alias;
202 if ($new_status_alias) {
203 # Keep a record of the previous status before we change it,
204 # we might need it
205 $self->{previous_status} = $current_status_alias ?
206 $current_status_alias :
207 scalar $self->status;
208 # This is hackery to enable us to undefine
209 # status_alias, since we need to have an overloaded
210 # status_alias method to get us around the problem described
211 # here:
212 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
213 # We need a way of accepting implied undef, so we can nullify
214 # the status_alias column, when called from $self->status
215 my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
216 my $ret = $self->SUPER::status_alias($val);
217 my $val_to_log = $val ? $new_status_alias : scalar $self->status;
218 if ($ret) {
219 my $logger = Koha::Illrequest::Logger->new;
220 $logger->log_status_change({
221 request => $self,
222 value => $val_to_log
224 } else {
225 delete $self->{previous_status};
227 return $ret;
229 # We can't know which result is the right one if there are multiple
230 # ILLSTATUS authorised values with the same authorised_value column value
231 # so we just use the first
232 my $alias = Koha::AuthorisedValues->search({
233 branchcode => $self->branchcode,
234 category => 'ILLSTATUS',
235 authorised_value => $self->SUPER::status_alias
236 })->next;
237 if ($alias) {
238 return $alias->authorised_value;
239 } else {
240 return;
244 =head3 status
246 $Illrequest->status('CANREQ');
248 Overloaded getter/setter for request status,
249 also nullifies status_alias and records the fact that the status has changed
251 =cut
253 sub status {
254 my ( $self, $new_status) = @_;
256 my $current_status = $self->SUPER::status;
257 my $current_status_alias = $self->SUPER::status_alias;
259 if ($new_status) {
260 # Keep a record of the previous status before we change it,
261 # we might need it
262 $self->{previous_status} = $current_status_alias ?
263 $current_status_alias :
264 $current_status;
265 my $ret = $self->SUPER::status($new_status)->store;
266 if ($current_status_alias) {
267 # This is hackery to enable us to undefine
268 # status_alias, since we need to have an overloaded
269 # status_alias method to get us around the problem described
270 # here:
271 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
272 # We need a way of passing implied undef to nullify status_alias
273 # so we pass -1, which is special cased in the overloaded setter
274 $self->status_alias("-1");
275 } else {
276 my $logger = Koha::Illrequest::Logger->new;
277 $logger->log_status_change({
278 request => $self,
279 value => $new_status
282 delete $self->{previous_status};
283 return $ret;
284 } else {
285 return $current_status;
289 =head3 load_backend
291 Require "Base.pm" from the relevant ILL backend.
293 =cut
295 sub load_backend {
296 my ( $self, $backend_id ) = @_;
298 my @raw = qw/Koha Illbackends/; # Base Path
300 my $backend_name = $backend_id || $self->backend;
302 unless ( defined $backend_name && $backend_name ne '' ) {
303 Koha::Exceptions::Ill::InvalidBackendId->throw(
304 "An invalid backend ID was requested ('')");
307 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
308 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
309 require $location;
310 $self->{_my_backend} = $backend_class->new({
311 config => $self->_config,
312 logger => Koha::Illrequest::Logger->new
314 return $self;
318 =head3 _backend
320 my $backend = $abstract->_backend($new_backend);
321 my $backend = $abstract->_backend;
323 Getter/Setter for our API object.
325 =cut
327 sub _backend {
328 my ( $self, $backend ) = @_;
329 $self->{_my_backend} = $backend if ( $backend );
330 # Dynamically load our backend object, as late as possible.
331 $self->load_backend unless ( $self->{_my_backend} );
332 return $self->{_my_backend};
335 =head3 _backend_capability
337 my $backend_capability_result = $self->_backend_capability($name, $args);
339 This is a helper method to invoke optional capabilities in the backend. If
340 the capability named by $name is not supported, return 0, else invoke it,
341 passing $args along with the invocation, and return its return value.
343 NOTE: this module suffers from a confusion in termninology:
345 in _backend_capability, the notion of capability refers to an optional feature
346 that is implemented in core, but might not be supported by a given backend.
348 in capabilities & custom_capability, capability refers to entries in the
349 status_graph (after union between backend and core).
351 The easiest way to fix this would be to fix the terminology in
352 capabilities & custom_capability and their callers.
354 =cut
356 sub _backend_capability {
357 my ( $self, $name, $args ) = @_;
358 my $capability = 0;
359 # See if capability is defined in backend
360 try {
361 $capability = $self->_backend->capabilities($name);
362 } catch {
363 return 0;
365 # Try to invoke it
366 if ( $capability && ref($capability) eq 'CODE' ) {
367 return &{$capability}($args);
368 } else {
369 return 0;
373 =head3 _config
375 my $config = $abstract->_config($config);
376 my $config = $abstract->_config;
378 Getter/Setter for our config object.
380 =cut
382 sub _config {
383 my ( $self, $config ) = @_;
384 $self->{_my_config} = $config if ( $config );
385 # Load our config object, as late as possible.
386 unless ( $self->{_my_config} ) {
387 $self->{_my_config} = Koha::Illrequest::Config->new;
389 return $self->{_my_config};
392 =head3 metadata
394 =cut
396 sub metadata {
397 my ( $self ) = @_;
398 return $self->_backend->metadata($self);
401 =head3 _core_status_graph
403 my $core_status_graph = $illrequest->_core_status_graph;
405 Returns ILL module's default status graph. A status graph defines the list of
406 available actions at any stage in the ILL workflow. This is for instance used
407 by the perl script & template to generate the correct buttons to display to
408 the end user at any given point.
410 =cut
412 sub _core_status_graph {
413 my ( $self ) = @_;
414 return {
415 NEW => {
416 prev_actions => [ ], # Actions containing buttons
417 # leading to this status
418 id => 'NEW', # ID of this status
419 name => 'New request', # UI name of this status
420 ui_method_name => 'New request', # UI name of method leading
421 # to this status
422 method => 'create', # method to this status
423 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
424 # requests with this status
425 ui_method_icon => 'fa-plus', # UI Style class
427 REQ => {
428 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
429 id => 'REQ',
430 name => 'Requested',
431 ui_method_name => 'Confirm request',
432 method => 'confirm',
433 next_actions => [ 'REQREV', 'COMP', 'CHK' ],
434 ui_method_icon => 'fa-check',
436 GENREQ => {
437 prev_actions => [ 'NEW', 'REQREV' ],
438 id => 'GENREQ',
439 name => 'Requested from partners',
440 ui_method_name => 'Place request with partners',
441 method => 'generic_confirm',
442 next_actions => [ 'COMP', 'CHK' ],
443 ui_method_icon => 'fa-send-o',
445 REQREV => {
446 prev_actions => [ 'REQ' ],
447 id => 'REQREV',
448 name => 'Request reverted',
449 ui_method_name => 'Revert Request',
450 method => 'cancel',
451 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
452 ui_method_icon => 'fa-times',
454 QUEUED => {
455 prev_actions => [ ],
456 id => 'QUEUED',
457 name => 'Queued request',
458 ui_method_name => 0,
459 method => 0,
460 next_actions => [ 'REQ', 'KILL' ],
461 ui_method_icon => 0,
463 CANCREQ => {
464 prev_actions => [ 'NEW' ],
465 id => 'CANCREQ',
466 name => 'Cancellation requested',
467 ui_method_name => 0,
468 method => 0,
469 next_actions => [ 'KILL', 'REQ' ],
470 ui_method_icon => 0,
472 COMP => {
473 prev_actions => [ 'REQ' ],
474 id => 'COMP',
475 name => 'Completed',
476 ui_method_name => 'Mark completed',
477 method => 'mark_completed',
478 next_actions => [ 'CHK' ],
479 ui_method_icon => 'fa-check',
481 KILL => {
482 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
483 id => 'KILL',
484 name => 0,
485 ui_method_name => 'Delete request',
486 method => 'delete',
487 next_actions => [ ],
488 ui_method_icon => 'fa-trash',
490 CHK => {
491 prev_actions => [ 'REQ', 'GENREQ', 'COMP' ],
492 id => 'CHK',
493 name => 'Checked out',
494 ui_method_name => 'Check out',
495 needs_prefs => [ 'CirculateILL' ],
496 needs_perms => [ 'user_circulate_circulate_remaining_permissions' ],
497 # An array of functions that all must return true
498 needs_all => [ sub { my $r = shift; return $r->biblio; } ],
499 method => 'check_out',
500 next_actions => [ ],
501 ui_method_icon => 'fa-upload',
503 RET => {
504 prev_actions => [ 'CHK' ],
505 id => 'RET',
506 name => 'Returned to library',
507 ui_method_name => 'Check in',
508 method => 'check_in',
509 next_actions => [ 'COMP' ],
510 ui_method_icon => 'fa-download',
515 =head3 _status_graph_union
517 my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
519 Return a new status_graph, the result of merging $origin & new_graph. This is
520 operation is a union over the sets defied by the two graphs.
522 Each entry in $new_graph is added to $origin. We do not provide a syntax for
523 'subtraction' of entries from $origin.
525 Whilst it is not intended that this works, you can override entries in $origin
526 with entries with the same key in $new_graph. This can lead to problematic
527 behaviour when $new_graph adds an entry, which modifies a dependent entry in
528 $origin, only for the entry in $origin to be replaced later with a new entry
529 from $new_graph.
531 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
532 i.e. each of the graphs need to be correct at the outset of the operation.
534 =cut
536 sub _status_graph_union {
537 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
538 # Create new status graph with:
539 # - all core_status_graph
540 # - for-each each backend_status_graph
541 # + add to new status graph
542 # + for each core prev_action:
543 # * locate core_status
544 # * update next_actions with additional next action.
545 # + for each core next_action:
546 # * locate core_status
547 # * update prev_actions with additional prev action
549 my @core_status_ids = keys %{$core_status_graph};
550 my $status_graph = clone($core_status_graph);
552 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
553 my $backend_status = $backend_status_graph->{$backend_status_key};
554 # Add to new status graph
555 $status_graph->{$backend_status_key} = $backend_status;
556 # Update all core methods' next_actions.
557 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
558 if ( grep { $prev_action eq $_ } @core_status_ids ) {
559 my @next_actions =
560 @{$status_graph->{$prev_action}->{next_actions}};
561 push @next_actions, $backend_status_key;
562 $status_graph->{$prev_action}->{next_actions}
563 = \@next_actions;
566 # Update all core methods' prev_actions
567 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
568 if ( grep { $next_action eq $_ } @core_status_ids ) {
569 my @prev_actions =
570 @{$status_graph->{$next_action}->{prev_actions}};
571 push @prev_actions, $backend_status_key;
572 $status_graph->{$next_action}->{prev_actions}
573 = \@prev_actions;
578 return $status_graph;
581 ### Core API methods
583 =head3 capabilities
585 my $capabilities = $illrequest->capabilities;
587 Return a hashref mapping methods to operation names supported by the queried
588 backend.
590 Example return value:
592 { create => "Create Request", confirm => "Progress Request" }
594 NOTE: this module suffers from a confusion in termninology:
596 in _backend_capability, the notion of capability refers to an optional feature
597 that is implemented in core, but might not be supported by a given backend.
599 in capabilities & custom_capability, capability refers to entries in the
600 status_graph (after union between backend and core).
602 The easiest way to fix this would be to fix the terminology in
603 capabilities & custom_capability and their callers.
605 =cut
607 sub capabilities {
608 my ( $self, $status ) = @_;
609 # Generate up to date status_graph
610 my $status_graph = $self->_status_graph_union(
611 $self->_core_status_graph,
612 $self->_backend->status_graph({
613 request => $self,
614 other => {}
617 # Extract available actions from graph.
618 return $status_graph->{$status} if $status;
619 # Or return entire graph.
620 return $status_graph;
623 =head3 custom_capability
625 Return the result of invoking $CANDIDATE on this request's backend with
626 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
628 NOTE: this module suffers from a confusion in termninology:
630 in _backend_capability, the notion of capability refers to an optional feature
631 that is implemented in core, but might not be supported by a given backend.
633 in capabilities & custom_capability, capability refers to entries in the
634 status_graph (after union between backend and core).
636 The easiest way to fix this would be to fix the terminology in
637 capabilities & custom_capability and their callers.
639 =cut
641 sub custom_capability {
642 my ( $self, $candidate, $params ) = @_;
643 foreach my $capability ( values %{$self->capabilities} ) {
644 if ( $candidate eq $capability->{method} ) {
645 my $response =
646 $self->_backend->$candidate({
647 request => $self,
648 other => $params,
650 return $self->expandTemplate($response);
653 return 0;
656 =head3 available_backends
658 Return a list of available backends.
660 =cut
662 sub available_backends {
663 my ( $self, $reduced ) = @_;
664 my $backends = $self->_config->available_backends($reduced);
665 return $backends;
668 =head3 available_actions
670 Return a list of available actions.
672 =cut
674 sub available_actions {
675 my ( $self ) = @_;
676 my $current_action = $self->capabilities($self->status);
677 my @available_actions = map { $self->capabilities($_) }
678 @{$current_action->{next_actions}};
679 return \@available_actions;
682 =head3 mark_completed
684 Mark a request as completed (status = COMP).
686 =cut
688 sub mark_completed {
689 my ( $self ) = @_;
690 $self->status('COMP')->store;
691 $self->completed(DateTime->now)->store;
692 return {
693 error => 0,
694 status => '',
695 message => '',
696 method => 'mark_completed',
697 stage => 'commit',
698 next => 'illview',
702 =head2 backend_migrate
704 Migrate a request from one backend to another.
706 =cut
708 sub backend_migrate {
709 my ( $self, $params ) = @_;
711 my $response = $self->_backend_capability('migrate',{
712 request => $self,
713 other => $params,
715 return $self->expandTemplate($response) if $response;
716 return $response;
719 =head2 backend_confirm
721 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
723 =over
725 =item * orderid
727 =item * accessurl, cost (if available).
729 =back
731 =cut
733 sub backend_confirm {
734 my ( $self, $params ) = @_;
736 my $response = $self->_backend->confirm({
737 request => $self,
738 other => $params,
740 return $self->expandTemplate($response);
743 =head3 backend_update_status
745 =cut
747 sub backend_update_status {
748 my ( $self, $params ) = @_;
749 return $self->expandTemplate($self->_backend->update_status($params));
752 =head3 backend_cancel
754 my $ILLResponse = $illRequest->backend_cancel;
756 The standard interface method allowing for request cancellation.
758 =cut
760 sub backend_cancel {
761 my ( $self, $params ) = @_;
763 my $result = $self->_backend->cancel({
764 request => $self,
765 other => $params
768 return $self->expandTemplate($result);
771 =head3 backend_renew
773 my $renew_response = $illRequest->backend_renew;
775 The standard interface method allowing for request renewal queries.
777 =cut
779 sub backend_renew {
780 my ( $self ) = @_;
781 return $self->expandTemplate(
782 $self->_backend->renew({
783 request => $self,
788 =head3 backend_create
790 my $create_response = $abstractILL->backend_create($params);
792 Return an array of Record objects created by querying our backend with
793 a Search query.
795 In the context of the other ILL methods, this is a special method: we only
796 pass it $params, as it does not yet have any other data associated with it.
798 =cut
800 sub backend_create {
801 my ( $self, $params ) = @_;
803 # Establish whether we need to do a generic copyright clearance.
804 if ($params->{opac}) {
805 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
806 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
807 return {
808 error => 0,
809 status => '',
810 message => '',
811 method => 'create',
812 stage => 'copyrightclearance',
813 value => {
814 other => $params,
815 backend => $self->_backend->name
818 } elsif ( defined $params->{stage}
819 && $params->{stage} eq 'copyrightclearance' ) {
820 $params->{stage} = 'init';
823 # First perform API action, then...
824 my $args = {
825 request => $self,
826 other => $params,
828 my $result = $self->_backend->create($args);
830 # ... simple case: we're not at 'commit' stage.
831 my $stage = $result->{stage};
832 return $self->expandTemplate($result)
833 unless ( 'commit' eq $stage );
835 # ... complex case: commit!
837 # Do we still have space for an ILL or should we queue?
838 my $permitted = $self->check_limits(
839 { patron => $self->patron }, { librarycode => $self->branchcode }
842 # Now augment our committed request.
844 $result->{permitted} = $permitted; # Queue request?
846 # This involves...
848 # ...Updating status!
849 $self->status('QUEUED')->store unless ( $permitted );
851 ## Handle Unmediated ILLs
853 # For the unmediated workflow we only need to delegate to our backend. If
854 # that backend supports unmediateld_ill, it will do its thing and return a
855 # proper response. If it doesn't then _backend_capability returns 0, so
856 # we keep the current result.
857 if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
858 my $unmediated_result = $self->_backend_capability(
859 'unmediated_ill',
860 $args
862 $result = $unmediated_result if $unmediated_result;
865 return $self->expandTemplate($result);
868 =head3 expandTemplate
870 my $params = $abstract->expandTemplate($params);
872 Return a version of $PARAMS augmented with our required template path.
874 =cut
876 sub expandTemplate {
877 my ( $self, $params ) = @_;
878 my $backend = $self->_backend->name;
879 # Generate path to file to load
880 my $backend_dir = $self->_config->backend_dir;
881 my $backend_tmpl = join "/", $backend_dir, $backend;
882 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
883 ( $params->{method}//q{} ) . ".inc";
884 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
885 ( $params->{method}//q{} ) . ".inc";
886 # Set files to load
887 $params->{template} = $intra_tmpl;
888 $params->{opac_template} = $opac_tmpl;
889 return $params;
892 #### Abstract Imports
894 =head3 getLimits
896 my $limit_rules = $abstract->getLimits( {
897 type => 'brw_cat' | 'branch',
898 value => $value
899 } );
901 Return the ILL limit rules for the supplied combination of type / value.
903 As the config may have no rules for this particular type / value combination,
904 or for the default, we must define fall-back values here.
906 =cut
908 sub getLimits {
909 my ( $self, $params ) = @_;
910 my $limits = $self->_config->getLimitRules($params->{type});
912 if ( defined $params->{value}
913 && defined $limits->{$params->{value}} ) {
914 return $limits->{$params->{value}};
916 else {
917 return $limits->{default} || { count => -1, method => 'active' };
921 =head3 getPrefix
923 my $prefix = $abstract->getPrefix( {
924 branch => $branch_code
925 } );
927 Return the ILL prefix as defined by our $params: either per borrower category,
928 per branch or the default.
930 =cut
932 sub getPrefix {
933 my ( $self, $params ) = @_;
934 my $brn_prefixes = $self->_config->getPrefixes();
935 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
938 =head3 get_type
940 my $type = $abstract->get_type();
942 Return a string representing the material type of this request or undef
944 =cut
946 sub get_type {
947 my ($self) = @_;
948 my $attr = $self->illrequestattributes->find({ type => 'type'});
949 return if !$attr;
950 return $attr->value;
953 #### Illrequests Imports
955 =head3 check_limits
957 my $ok = $illRequests->check_limits( {
958 borrower => $borrower,
959 branchcode => 'branchcode' | undef,
960 } );
962 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
963 see whether we are still able to place ILLs.
965 LimitRules are derived from koha-conf.xml:
966 + default limit counts, and counting method
967 + branch specific limit counts & counting method
968 + borrower category specific limit counts & counting method
969 + err on the side of caution: a counting fail will cause fail, even if
970 the other counts passes.
972 =cut
974 sub check_limits {
975 my ( $self, $params ) = @_;
976 my $patron = $params->{patron};
977 my $branchcode = $params->{librarycode} || $patron->branchcode;
979 # Establish maximum number of allowed requests
980 my ( $branch_rules, $brw_rules ) = (
981 $self->getLimits( {
982 type => 'branch',
983 value => $branchcode
984 } ),
985 $self->getLimits( {
986 type => 'brw_cat',
987 value => $patron->categorycode,
988 } ),
990 my ( $branch_limit, $brw_limit )
991 = ( $branch_rules->{count}, $brw_rules->{count} );
992 # Establish currently existing requests
993 my ( $branch_count, $brw_count ) = (
994 $self->_limit_counter(
995 $branch_rules->{method}, { branchcode => $branchcode }
997 $self->_limit_counter(
998 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
1002 # Compare and return
1003 # A limit of -1 means no limit exists.
1004 # We return blocked if either branch limit or brw limit is reached.
1005 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
1006 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
1007 return 0;
1008 } else {
1009 return 1;
1013 sub _limit_counter {
1014 my ( $self, $method, $target ) = @_;
1016 # Establish parameters of counts
1017 my $resultset;
1018 if ($method && $method eq 'annual') {
1019 $resultset = Koha::Illrequests->search({
1020 -and => [
1021 %{$target},
1022 \"YEAR(placed) = YEAR(NOW())"
1025 } else { # assume 'active'
1026 # XXX: This status list is ugly. There should be a method in config
1027 # to return these.
1028 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1029 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1032 # Fetch counts
1033 return $resultset->count;
1036 =head3 requires_moderation
1038 my $status = $illRequest->requires_moderation;
1040 Return the name of the status if moderation by staff is required; or 0
1041 otherwise.
1043 =cut
1045 sub requires_moderation {
1046 my ( $self ) = @_;
1047 my $require_moderation = {
1048 'CANCREQ' => 'CANCREQ',
1050 return $require_moderation->{$self->status};
1053 =head3 biblio
1055 my $biblio = $request->biblio;
1057 For a given request, return the biblio associated with it,
1058 or undef if none exists
1060 =cut
1062 sub biblio {
1063 my ( $self ) = @_;
1065 return if !$self->biblio_id;
1067 return Koha::Biblios->find({
1068 biblionumber => $self->biblio_id
1072 =head3 check_out
1074 my $stage_summary = $request->check_out;
1076 Handle the check_out method. The first stage involves gathering the required
1077 data from the user via a form, the second stage creates an item and tries to
1078 issue it to the patron. If successful, it notifies the patron, then it
1079 returns a summary of how things went
1081 =cut
1083 sub check_out {
1084 my ( $self, $params ) = @_;
1086 # Objects required by the template
1087 my $itemtypes = Koha::ItemTypes->search(
1089 { order_by => ['description'] }
1091 my $libraries = Koha::Libraries->search(
1093 { order_by => ['branchcode'] }
1095 my $biblio = $self->biblio;
1097 # Find all statistical patrons
1098 my $statistical_patrons = Koha::Patrons->search(
1099 { 'category_type' => 'x' },
1100 { join => { 'categorycode' => 'borrowers' } }
1103 if (!$params->{stage} || $params->{stage} eq 'init') {
1104 # Present a form to gather the required data
1106 # We may be viewing this page having previously tried to issue
1107 # the item (in which case, we may already have created an item)
1108 # so we pass the biblio for this request
1109 return {
1110 method => 'check_out',
1111 stage => 'form',
1112 value => {
1113 itemtypes => $itemtypes,
1114 libraries => $libraries,
1115 statistical => $statistical_patrons,
1116 biblio => $biblio
1119 } elsif ($params->{stage} eq 'form') {
1120 # Validate what we've got and return with an error if we fail
1121 my $errors = {};
1122 if (!$params->{item_type} || length $params->{item_type} == 0) {
1123 $errors->{item_type} = 1;
1125 if ($params->{inhouse} && length $params->{inhouse} > 0) {
1126 my $patron_count = Koha::Patrons->search({
1127 cardnumber => $params->{inhouse}
1128 })->count();
1129 if ($patron_count != 1) {
1130 $errors->{inhouse} = 1;
1134 # Check we don't have more than one item for this bib,
1135 # if we do, something very odd is going on
1136 # Having 1 is OK, it means we're likely trying to issue
1137 # following a previously failed attempt, the item exists
1138 # so we'll use it
1139 my @items = $biblio->items->as_list;
1140 my $item_count = scalar @items;
1141 if ($item_count > 1) {
1142 $errors->{itemcount} = 1;
1145 # Failed validation, go back to the form
1146 if (%{$errors}) {
1147 return {
1148 method => 'check_out',
1149 stage => 'form',
1150 value => {
1151 params => $params,
1152 statistical => $statistical_patrons,
1153 itemtypes => $itemtypes,
1154 libraries => $libraries,
1155 biblio => $biblio,
1156 errors => $errors
1161 # Passed validation
1163 # Create an item if one doesn't already exist,
1164 # if one does, use that
1165 my $itemnumber;
1166 if ($item_count == 0) {
1167 my $item_hash = {
1168 biblionumber => $self->biblio_id,
1169 homebranch => $params->{branchcode},
1170 holdingbranch => $params->{branchcode},
1171 location => $params->{branchcode},
1172 itype => $params->{item_type},
1173 barcode => 'ILL-' . $self->illrequest_id
1175 try {
1176 my $item = Koha::Item->new($item_hash)->store;
1177 $itemnumber = $item->itemnumber;
1179 } else {
1180 $itemnumber = $items[0]->itemnumber;
1182 # Check we have an item before going forward
1183 if (!$itemnumber) {
1184 return {
1185 method => 'check_out',
1186 stage => 'form',
1187 value => {
1188 params => $params,
1189 itemtypes => $itemtypes,
1190 libraries => $libraries,
1191 statistical => $statistical_patrons,
1192 errors => { item_creation => 1 }
1197 # Do the check out
1199 # Gather what we need
1200 my $target_item = Koha::Items->find( $itemnumber );
1201 # Determine who we're issuing to
1202 my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
1203 Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
1204 $self->patron;
1206 my @issue_args = (
1207 $patron,
1208 scalar $target_item->barcode
1210 if ($params->{duedate} && length $params->{duedate} > 0) {
1211 push @issue_args, $params->{duedate};
1213 # Check if we can check out
1214 my ( $error, $confirm, $alerts, $messages ) =
1215 C4::Circulation::CanBookBeIssued(@issue_args);
1217 # If we got anything back saying we can't check out,
1218 # return it to the template
1219 my $problems = {};
1220 if ( $error && %{$error} ) { $problems->{error} = $error };
1221 if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
1222 if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
1223 if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
1225 if (%{$problems}) {
1226 return {
1227 method => 'check_out',
1228 stage => 'form',
1229 value => {
1230 params => $params,
1231 itemtypes => $itemtypes,
1232 libraries => $libraries,
1233 statistical => $statistical_patrons,
1234 patron => $patron,
1235 biblio => $biblio,
1236 check_out_errors => $problems
1241 # We can allegedly check out, so make it so
1242 # For some reason, AddIssue requires an unblessed Patron
1243 $issue_args[0] = $patron->unblessed;
1244 my $issue = C4::Circulation::AddIssue(@issue_args);
1246 if ($issue) {
1247 # Update the request status
1248 $self->status('CHK')->store;
1249 return {
1250 method => 'check_out',
1251 stage => 'done_check_out',
1252 value => {
1253 params => $params,
1254 patron => $patron,
1255 check_out => $issue
1258 } else {
1259 return {
1260 method => 'check_out',
1261 stage => 'form',
1262 value => {
1263 params => $params,
1264 itemtypes => $itemtypes,
1265 libraries => $libraries,
1266 errors => { item_check_out => 1 }
1274 =head3 generic_confirm
1276 my $stage_summary = $illRequest->generic_confirm;
1278 Handle the generic_confirm extended method. The first stage involves creating
1279 a template email for the end user to edit in the browser. The second stage
1280 attempts to submit the email.
1282 =cut
1284 sub generic_confirm {
1285 my ( $self, $params ) = @_;
1286 my $branch = Koha::Libraries->find($params->{current_branchcode})
1287 || die "Invalid current branchcode. Are you logged in as the database user?";
1288 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
1289 my $draft->{subject} = "ILL Request";
1290 $draft->{body} = <<EOF;
1291 Dear Sir/Madam,
1293 We would like to request an interlibrary loan for a title matching the
1294 following description:
1298 my $details = $self->metadata;
1299 while (my ($title, $value) = each %{$details}) {
1300 $draft->{body} .= " - " . $title . ": " . $value . "\n"
1301 if $value;
1303 $draft->{body} .= <<EOF;
1305 Please let us know if you are able to supply this to us.
1307 Kind Regards
1311 my @address = map { $branch->$_ }
1312 qw/ branchname branchaddress1 branchaddress2 branchaddress3
1313 branchzip branchcity branchstate branchcountry branchphone
1314 branchemail /;
1315 my $address = "";
1316 foreach my $line ( @address ) {
1317 $address .= $line . "\n" if $line;
1320 $draft->{body} .= $address;
1322 my $partners = Koha::Patrons->search({
1323 categorycode => $self->_config->partner_code
1325 return {
1326 error => 0,
1327 status => '',
1328 message => '',
1329 method => 'generic_confirm',
1330 stage => 'draft',
1331 value => {
1332 draft => $draft,
1333 partners => $partners,
1337 } elsif ( 'draft' eq $params->{stage} ) {
1338 # Create the to header
1339 my $to = $params->{partners};
1340 if ( defined $to ) {
1341 $to =~ s/^\x00//; # Strip leading NULLs
1342 $to =~ s/\x00/; /; # Replace others with '; '
1344 Koha::Exceptions::Ill::NoTargetEmail->throw(
1345 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
1346 if ( !$to );
1347 # Create the from, replyto and sender headers
1348 my $from = $branch->branchemail;
1349 my $replyto = $branch->branchreplyto || $from;
1350 Koha::Exceptions::Ill::NoLibraryEmail->throw(
1351 "Your library has no usable email address. Please set it.")
1352 if ( !$from );
1354 # Create the email
1355 my $message = Koha::Email->new;
1356 my %mail = $message->create_message_headers(
1358 to => $to,
1359 from => $from,
1360 replyto => $replyto,
1361 subject => Encode::encode( "utf8", $params->{subject} ),
1362 message => Encode::encode( "utf8", $params->{body} ),
1363 contenttype => 'text/plain',
1366 # Send it
1367 my $result = sendmail(%mail);
1368 if ( $result ) {
1369 $self->status("GENREQ")->store;
1370 $self->_backend_capability(
1371 'set_requested_partners',
1373 request => $self,
1374 to => $to
1377 return {
1378 error => 0,
1379 status => '',
1380 message => '',
1381 method => 'generic_confirm',
1382 stage => 'commit',
1383 next => 'illview',
1385 } else {
1386 return {
1387 error => 1,
1388 status => 'email_failed',
1389 message => $Mail::Sendmail::error,
1390 method => 'generic_confirm',
1391 stage => 'draft',
1394 } else {
1395 die "Unknown stage, should not have happened."
1399 =head3 id_prefix
1401 my $prefix = $record->id_prefix;
1403 Return the prefix appropriate for the current Illrequest as derived from the
1404 borrower and branch associated with this request's Status, and the config
1405 file.
1407 =cut
1409 sub id_prefix {
1410 my ( $self ) = @_;
1411 my $prefix = $self->getPrefix( {
1412 branch => $self->branchcode,
1413 } );
1414 $prefix .= "-" if ( $prefix );
1415 return $prefix;
1418 =head3 _censor
1420 my $params = $illRequest->_censor($params);
1422 Return $params, modified to reflect our censorship requirements.
1424 =cut
1426 sub _censor {
1427 my ( $self, $params ) = @_;
1428 my $censorship = $self->_config->censorship;
1429 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1430 if ( $params->{opac} );
1431 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1433 return $params;
1436 =head3 store
1438 $Illrequest->store;
1440 Overloaded I<store> method that, in addition to performing the 'store',
1441 possibly records the fact that something happened
1443 =cut
1445 sub store {
1446 my ( $self, $attrs ) = @_;
1448 my $ret = $self->SUPER::store;
1450 $attrs->{log_origin} = 'core';
1452 if ($ret && defined $attrs) {
1453 my $logger = Koha::Illrequest::Logger->new;
1454 $logger->log_maybe({
1455 request => $self,
1456 attrs => $attrs
1460 return $ret;
1463 =head3 requested_partners
1465 my $partners_string = $illRequest->requested_partners;
1467 Return the string representing the email addresses of the partners to
1468 whom a request has been sent
1470 =cut
1472 sub requested_partners {
1473 my ( $self ) = @_;
1474 return $self->_backend_capability(
1475 'get_requested_partners',
1476 { request => $self }
1480 =head3 TO_JSON
1482 $json = $illrequest->TO_JSON
1484 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1485 into the unblessed representation of the object.
1487 TODO: This method does nothing and is not called anywhere. However, bug 74325
1488 touches it, so keeping this for now until both this and bug 74325 are merged,
1489 at which point we can sort it out and remove it completely
1491 =cut
1493 sub TO_JSON {
1494 my ( $self, $embed ) = @_;
1496 my $object = $self->SUPER::TO_JSON();
1498 return $object;
1501 =head2 Internal methods
1503 =head3 _type
1505 =cut
1507 sub _type {
1508 return 'Illrequest';
1511 =head1 AUTHOR
1513 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1514 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>
1516 =cut