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.
121 my $statusalias = $request->statusalias;
123 Returns a request's status alias, as a Koha::AuthorisedValue instance
124 or implicit undef. This is distinct from status_alias, which only returns
125 the value in the status_alias column, this method returns the entire
126 AuthorisedValue object
132 return unless $self->status_alias;
133 # We can't know which result is the right one if there are multiple
134 # ILLSTATUS authorised values with the same authorised_value column value
135 # so we just use the first
136 return Koha::AuthorisedValues->search(
138 category => 'ILLSTATUS',
139 authorised_value => $self->SUPER::status_alias
146 =head3 illrequestattributes
150 sub illrequestattributes {
152 return Koha::Illrequestattributes->_new_from_dbic(
153 scalar $self->_result->illrequestattributes
163 return Koha::Illcomments->_new_from_dbic(
164 scalar $self->_result->illcomments
174 my $logger = Koha::Illrequest::Logger->new;
175 return $logger->get_request_logs($self);
184 return Koha::Patron->_new_from_dbic(
185 scalar $self->_result->borrowernumber
191 $Illrequest->status_alias(143);
193 Overloaded getter/setter for status_alias,
194 that only returns authorised values from the
195 correct category and records the fact that the status has changed
200 my ($self, $new_status_alias) = @_;
202 my $current_status_alias = $self->SUPER::status_alias;
204 if ($new_status_alias) {
205 # Keep a record of the previous status before we change it,
207 $self->{previous_status} = $current_status_alias ?
208 $current_status_alias :
209 scalar $self->status;
210 # This is hackery to enable us to undefine
211 # status_alias, since we need to have an overloaded
212 # status_alias method to get us around the problem described
214 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
215 # We need a way of accepting implied undef, so we can nullify
216 # the status_alias column, when called from $self->status
217 my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
218 my $ret = $self->SUPER::status_alias($val);
219 my $val_to_log = $val ? $new_status_alias : scalar $self->status;
221 my $logger = Koha::Illrequest::Logger->new;
222 $logger->log_status_change({
227 delete $self->{previous_status};
231 # We can't know which result is the right one if there are multiple
232 # ILLSTATUS authorised values with the same authorised_value column value
233 # so we just use the first
234 my $alias = Koha::AuthorisedValues->search(
236 category => 'ILLSTATUS',
237 authorised_value => $self->SUPER::status_alias
244 return $alias->authorised_value;
252 $Illrequest->status('CANREQ');
254 Overloaded getter/setter for request status,
255 also nullifies status_alias and records the fact that the status has changed
256 and sends a notice if appropriate
261 my ( $self, $new_status) = @_;
263 my $current_status = $self->SUPER::status;
264 my $current_status_alias = $self->SUPER::status_alias;
267 # Keep a record of the previous status before we change it,
269 $self->{previous_status} = $current_status_alias ?
270 $current_status_alias :
272 my $ret = $self->SUPER::status($new_status)->store;
273 if ($current_status_alias) {
274 # This is hackery to enable us to undefine
275 # status_alias, since we need to have an overloaded
276 # status_alias method to get us around the problem described
278 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
279 # We need a way of passing implied undef to nullify status_alias
280 # so we pass -1, which is special cased in the overloaded setter
281 $self->status_alias("-1");
283 my $logger = Koha::Illrequest::Logger->new;
284 $logger->log_status_change({
289 delete $self->{previous_status};
290 # If status has changed to cancellation requested, send a notice
291 if ($new_status eq 'CANCREQ') {
292 $self->send_staff_notice('ILL_REQUEST_CANCEL');
296 return $current_status;
302 Require "Base.pm" from the relevant ILL backend.
307 my ( $self, $backend_id ) = @_;
309 my @raw = qw/Koha Illbackends/; # Base Path
311 my $backend_name = $backend_id || $self->backend;
313 unless ( defined $backend_name && $backend_name ne '' ) {
314 Koha::Exceptions::Ill::InvalidBackendId->throw(
315 "An invalid backend ID was requested ('')");
318 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
319 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
321 $self->{_my_backend} = $backend_class->new({
322 config => $self->_config,
323 logger => Koha::Illrequest::Logger->new
331 my $backend = $abstract->_backend($new_backend);
332 my $backend = $abstract->_backend;
334 Getter/Setter for our API object.
339 my ( $self, $backend ) = @_;
340 $self->{_my_backend} = $backend if ( $backend );
341 # Dynamically load our backend object, as late as possible.
342 $self->load_backend unless ( $self->{_my_backend} );
343 return $self->{_my_backend};
346 =head3 _backend_capability
348 my $backend_capability_result = $self->_backend_capability($name, $args);
350 This is a helper method to invoke optional capabilities in the backend. If
351 the capability named by $name is not supported, return 0, else invoke it,
352 passing $args along with the invocation, and return its return value.
354 NOTE: this module suffers from a confusion in termninology:
356 in _backend_capability, the notion of capability refers to an optional feature
357 that is implemented in core, but might not be supported by a given backend.
359 in capabilities & custom_capability, capability refers to entries in the
360 status_graph (after union between backend and core).
362 The easiest way to fix this would be to fix the terminology in
363 capabilities & custom_capability and their callers.
367 sub _backend_capability {
368 my ( $self, $name, $args ) = @_;
370 # See if capability is defined in backend
372 $capability = $self->_backend->capabilities($name);
377 if ( $capability && ref($capability) eq 'CODE' ) {
378 return &{$capability}($args);
386 my $config = $abstract->_config($config);
387 my $config = $abstract->_config;
389 Getter/Setter for our config object.
394 my ( $self, $config ) = @_;
395 $self->{_my_config} = $config if ( $config );
396 # Load our config object, as late as possible.
397 unless ( $self->{_my_config} ) {
398 $self->{_my_config} = Koha::Illrequest::Config->new;
400 return $self->{_my_config};
409 return $self->_backend->metadata($self);
412 =head3 _core_status_graph
414 my $core_status_graph = $illrequest->_core_status_graph;
416 Returns ILL module's default status graph. A status graph defines the list of
417 available actions at any stage in the ILL workflow. This is for instance used
418 by the perl script & template to generate the correct buttons to display to
419 the end user at any given point.
423 sub _core_status_graph {
427 prev_actions => [ ], # Actions containing buttons
428 # leading to this status
429 id => 'NEW', # ID of this status
430 name => 'New request', # UI name of this status
431 ui_method_name => 'New request', # UI name of method leading
433 method => 'create', # method to this status
434 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
435 # requests with this status
436 ui_method_icon => 'fa-plus', # UI Style class
439 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
442 ui_method_name => 'Confirm request',
444 next_actions => [ 'REQREV', 'COMP', 'CHK' ],
445 ui_method_icon => 'fa-check',
448 prev_actions => [ 'NEW', 'REQREV' ],
450 name => 'Requested from partners',
451 ui_method_name => 'Place request with partners',
452 method => 'generic_confirm',
453 next_actions => [ 'COMP', 'CHK' ],
454 ui_method_icon => 'fa-send-o',
457 prev_actions => [ 'REQ' ],
459 name => 'Request reverted',
460 ui_method_name => 'Revert Request',
462 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
463 ui_method_icon => 'fa-times',
468 name => 'Queued request',
471 next_actions => [ 'REQ', 'KILL' ],
475 prev_actions => [ 'NEW' ],
477 name => 'Cancellation requested',
480 next_actions => [ 'KILL', 'REQ' ],
484 prev_actions => [ 'REQ' ],
487 ui_method_name => 'Mark completed',
488 method => 'mark_completed',
489 next_actions => [ 'CHK' ],
490 ui_method_icon => 'fa-check',
493 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
496 ui_method_name => 'Delete request',
499 ui_method_icon => 'fa-trash',
502 prev_actions => [ 'REQ', 'GENREQ', 'COMP' ],
504 name => 'Checked out',
505 ui_method_name => 'Check out',
506 needs_prefs => [ 'CirculateILL' ],
507 needs_perms => [ 'user_circulate_circulate_remaining_permissions' ],
508 # An array of functions that all must return true
509 needs_all => [ sub { my $r = shift; return $r->biblio; } ],
510 method => 'check_out',
512 ui_method_icon => 'fa-upload',
515 prev_actions => [ 'CHK' ],
517 name => 'Returned to library',
518 ui_method_name => 'Check in',
519 method => 'check_in',
520 next_actions => [ 'COMP' ],
521 ui_method_icon => 'fa-download',
526 =head3 _status_graph_union
528 my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
530 Return a new status_graph, the result of merging $origin & new_graph. This is
531 operation is a union over the sets defied by the two graphs.
533 Each entry in $new_graph is added to $origin. We do not provide a syntax for
534 'subtraction' of entries from $origin.
536 Whilst it is not intended that this works, you can override entries in $origin
537 with entries with the same key in $new_graph. This can lead to problematic
538 behaviour when $new_graph adds an entry, which modifies a dependent entry in
539 $origin, only for the entry in $origin to be replaced later with a new entry
542 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
543 i.e. each of the graphs need to be correct at the outset of the operation.
547 sub _status_graph_union {
548 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
549 # Create new status graph with:
550 # - all core_status_graph
551 # - for-each each backend_status_graph
552 # + add to new status graph
553 # + for each core prev_action:
554 # * locate core_status
555 # * update next_actions with additional next action.
556 # + for each core next_action:
557 # * locate core_status
558 # * update prev_actions with additional prev action
560 my @core_status_ids = keys %{$core_status_graph};
561 my $status_graph = clone($core_status_graph);
563 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
564 my $backend_status = $backend_status_graph->{$backend_status_key};
565 # Add to new status graph
566 $status_graph->{$backend_status_key} = $backend_status;
567 # Update all core methods' next_actions.
568 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
569 if ( grep { $prev_action eq $_ } @core_status_ids ) {
571 @{$status_graph->{$prev_action}->{next_actions}};
572 push @next_actions, $backend_status_key
573 if (!grep(/^$backend_status_key$/, @next_actions));
574 $status_graph->{$prev_action}->{next_actions}
578 # Update all core methods' prev_actions
579 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
580 if ( grep { $next_action eq $_ } @core_status_ids ) {
582 @{$status_graph->{$next_action}->{prev_actions}};
583 push @prev_actions, $backend_status_key
584 if (!grep(/^$backend_status_key$/, @prev_actions));
585 $status_graph->{$next_action}->{prev_actions}
591 return $status_graph;
598 my $capabilities = $illrequest->capabilities;
600 Return a hashref mapping methods to operation names supported by the queried
603 Example return value:
605 { create => "Create Request", confirm => "Progress Request" }
607 NOTE: this module suffers from a confusion in termninology:
609 in _backend_capability, the notion of capability refers to an optional feature
610 that is implemented in core, but might not be supported by a given backend.
612 in capabilities & custom_capability, capability refers to entries in the
613 status_graph (after union between backend and core).
615 The easiest way to fix this would be to fix the terminology in
616 capabilities & custom_capability and their callers.
621 my ( $self, $status ) = @_;
622 # Generate up to date status_graph
623 my $status_graph = $self->_status_graph_union(
624 $self->_core_status_graph,
625 $self->_backend->status_graph({
630 # Extract available actions from graph.
631 return $status_graph->{$status} if $status;
632 # Or return entire graph.
633 return $status_graph;
636 =head3 custom_capability
638 Return the result of invoking $CANDIDATE on this request's backend with
639 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
641 NOTE: this module suffers from a confusion in termninology:
643 in _backend_capability, the notion of capability refers to an optional feature
644 that is implemented in core, but might not be supported by a given backend.
646 in capabilities & custom_capability, capability refers to entries in the
647 status_graph (after union between backend and core).
649 The easiest way to fix this would be to fix the terminology in
650 capabilities & custom_capability and their callers.
654 sub custom_capability {
655 my ( $self, $candidate, $params ) = @_;
656 foreach my $capability ( values %{$self->capabilities} ) {
657 if ( $candidate eq $capability->{method} ) {
659 $self->_backend->$candidate({
663 return $self->expandTemplate($response);
669 =head3 available_backends
671 Return a list of available backends.
675 sub available_backends {
676 my ( $self, $reduced ) = @_;
677 my $backends = $self->_config->available_backends($reduced);
681 =head3 available_actions
683 Return a list of available actions.
687 sub available_actions {
689 my $current_action = $self->capabilities($self->status);
690 my @available_actions = map { $self->capabilities($_) }
691 @{$current_action->{next_actions}};
692 return \@available_actions;
695 =head3 mark_completed
697 Mark a request as completed (status = COMP).
703 $self->status('COMP')->store;
704 $self->completed(dt_from_string())->store;
709 method => 'mark_completed',
715 =head2 backend_illview
717 View and manage an ILL request
721 sub backend_illview {
722 my ( $self, $params ) = @_;
724 my $response = $self->_backend_capability('illview',{
728 return $self->expandTemplate($response) if $response;
732 =head2 backend_migrate
734 Migrate a request from one backend to another.
738 sub backend_migrate {
739 my ( $self, $params ) = @_;
740 # Set the request's backend to be the destination backend
741 $self->load_backend($params->{backend});
742 my $response = $self->_backend_capability('migrate',{
746 return $self->expandTemplate($response) if $response;
750 =head2 backend_confirm
752 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
758 =item * accessurl, cost (if available).
764 sub backend_confirm {
765 my ( $self, $params ) = @_;
767 my $response = $self->_backend->confirm({
771 return $self->expandTemplate($response);
774 =head3 backend_update_status
778 sub backend_update_status {
779 my ( $self, $params ) = @_;
780 return $self->expandTemplate($self->_backend->update_status($params));
783 =head3 backend_cancel
785 my $ILLResponse = $illRequest->backend_cancel;
787 The standard interface method allowing for request cancellation.
792 my ( $self, $params ) = @_;
794 my $result = $self->_backend->cancel({
799 return $self->expandTemplate($result);
804 my $renew_response = $illRequest->backend_renew;
806 The standard interface method allowing for request renewal queries.
812 return $self->expandTemplate(
813 $self->_backend->renew({
819 =head3 backend_create
821 my $create_response = $abstractILL->backend_create($params);
823 Return an array of Record objects created by querying our backend with
826 In the context of the other ILL methods, this is a special method: we only
827 pass it $params, as it does not yet have any other data associated with it.
832 my ( $self, $params ) = @_;
834 # Establish whether we need to do a generic copyright clearance.
835 if ($params->{opac}) {
836 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
837 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
843 stage => 'copyrightclearance',
846 backend => $self->_backend->name
849 } elsif ( defined $params->{stage}
850 && $params->{stage} eq 'copyrightclearance' ) {
851 $params->{stage} = 'init';
854 # First perform API action, then...
859 my $result = $self->_backend->create($args);
861 # ... simple case: we're not at 'commit' stage.
862 my $stage = $result->{stage};
863 return $self->expandTemplate($result)
864 unless ( 'commit' eq $stage );
866 # ... complex case: commit!
868 # Do we still have space for an ILL or should we queue?
869 my $permitted = $self->check_limits(
870 { patron => $self->patron }, { librarycode => $self->branchcode }
873 # Now augment our committed request.
875 $result->{permitted} = $permitted; # Queue request?
879 # ...Updating status!
880 $self->status('QUEUED')->store unless ( $permitted );
882 ## Handle Unmediated ILLs
884 # For the unmediated workflow we only need to delegate to our backend. If
885 # that backend supports unmediateld_ill, it will do its thing and return a
886 # proper response. If it doesn't then _backend_capability returns 0, so
887 # we keep the current result.
888 if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
889 my $unmediated_result = $self->_backend_capability(
893 $result = $unmediated_result if $unmediated_result;
896 return $self->expandTemplate($result);
899 =head3 expandTemplate
901 my $params = $abstract->expandTemplate($params);
903 Return a version of $PARAMS augmented with our required template path.
908 my ( $self, $params ) = @_;
909 my $backend = $self->_backend->name;
910 # Generate path to file to load
911 my $backend_dir = $self->_config->backend_dir;
912 my $backend_tmpl = join "/", $backend_dir, $backend;
913 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
914 ( $params->{method}//q{} ) . ".inc";
915 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
916 ( $params->{method}//q{} ) . ".inc";
918 $params->{template} = $intra_tmpl;
919 $params->{opac_template} = $opac_tmpl;
923 #### Abstract Imports
927 my $limit_rules = $abstract->getLimits( {
928 type => 'brw_cat' | 'branch',
932 Return the ILL limit rules for the supplied combination of type / value.
934 As the config may have no rules for this particular type / value combination,
935 or for the default, we must define fall-back values here.
940 my ( $self, $params ) = @_;
941 my $limits = $self->_config->getLimitRules($params->{type});
943 if ( defined $params->{value}
944 && defined $limits->{$params->{value}} ) {
945 return $limits->{$params->{value}};
948 return $limits->{default} || { count => -1, method => 'active' };
954 my $prefix = $abstract->getPrefix( {
955 branch => $branch_code
958 Return the ILL prefix as defined by our $params: either per borrower category,
959 per branch or the default.
964 my ( $self, $params ) = @_;
965 my $brn_prefixes = $self->_config->getPrefixes();
966 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
971 my $type = $abstract->get_type();
973 Return a string representing the material type of this request or undef
979 my $attr = $self->illrequestattributes->find({ type => 'type'});
984 #### Illrequests Imports
988 my $ok = $illRequests->check_limits( {
989 borrower => $borrower,
990 branchcode => 'branchcode' | undef,
993 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
994 see whether we are still able to place ILLs.
996 LimitRules are derived from koha-conf.xml:
997 + default limit counts, and counting method
998 + branch specific limit counts & counting method
999 + borrower category specific limit counts & counting method
1000 + err on the side of caution: a counting fail will cause fail, even if
1001 the other counts passes.
1006 my ( $self, $params ) = @_;
1007 my $patron = $params->{patron};
1008 my $branchcode = $params->{librarycode} || $patron->branchcode;
1010 # Establish maximum number of allowed requests
1011 my ( $branch_rules, $brw_rules ) = (
1014 value => $branchcode
1018 value => $patron->categorycode,
1021 my ( $branch_limit, $brw_limit )
1022 = ( $branch_rules->{count}, $brw_rules->{count} );
1023 # Establish currently existing requests
1024 my ( $branch_count, $brw_count ) = (
1025 $self->_limit_counter(
1026 $branch_rules->{method}, { branchcode => $branchcode }
1028 $self->_limit_counter(
1029 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
1033 # Compare and return
1034 # A limit of -1 means no limit exists.
1035 # We return blocked if either branch limit or brw limit is reached.
1036 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
1037 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
1044 sub _limit_counter {
1045 my ( $self, $method, $target ) = @_;
1047 # Establish parameters of counts
1049 if ($method && $method eq 'annual') {
1050 $resultset = Koha::Illrequests->search({
1053 \"YEAR(placed) = YEAR(NOW())"
1056 } else { # assume 'active'
1057 # XXX: This status list is ugly. There should be a method in config
1059 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1060 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1064 return $resultset->count;
1067 =head3 requires_moderation
1069 my $status = $illRequest->requires_moderation;
1071 Return the name of the status if moderation by staff is required; or 0
1076 sub requires_moderation {
1078 my $require_moderation = {
1079 'CANCREQ' => 'CANCREQ',
1081 return $require_moderation->{$self->status};
1086 my $biblio = $request->biblio;
1088 For a given request, return the biblio associated with it,
1089 or undef if none exists
1096 return if !$self->biblio_id;
1098 return Koha::Biblios->find({
1099 biblionumber => $self->biblio_id
1105 my $stage_summary = $request->check_out;
1107 Handle the check_out method. The first stage involves gathering the required
1108 data from the user via a form, the second stage creates an item and tries to
1109 issue it to the patron. If successful, it notifies the patron, then it
1110 returns a summary of how things went
1115 my ( $self, $params ) = @_;
1117 # Objects required by the template
1118 my $itemtypes = Koha::ItemTypes->search(
1120 { order_by => ['description'] }
1122 my $libraries = Koha::Libraries->search(
1124 { order_by => ['branchcode'] }
1126 my $biblio = $self->biblio;
1128 # Find all statistical patrons
1129 my $statistical_patrons = Koha::Patrons->search(
1130 { 'category_type' => 'x' },
1131 { join => { 'categorycode' => 'borrowers' } }
1134 if (!$params->{stage} || $params->{stage} eq 'init') {
1135 # Present a form to gather the required data
1137 # We may be viewing this page having previously tried to issue
1138 # the item (in which case, we may already have created an item)
1139 # so we pass the biblio for this request
1141 method => 'check_out',
1144 itemtypes => $itemtypes,
1145 libraries => $libraries,
1146 statistical => $statistical_patrons,
1150 } elsif ($params->{stage} eq 'form') {
1151 # Validate what we've got and return with an error if we fail
1153 if (!$params->{item_type} || length $params->{item_type} == 0) {
1154 $errors->{item_type} = 1;
1156 if ($params->{inhouse} && length $params->{inhouse} > 0) {
1157 my $patron_count = Koha::Patrons->search({
1158 cardnumber => $params->{inhouse}
1160 if ($patron_count != 1) {
1161 $errors->{inhouse} = 1;
1165 # Check we don't have more than one item for this bib,
1166 # if we do, something very odd is going on
1167 # Having 1 is OK, it means we're likely trying to issue
1168 # following a previously failed attempt, the item exists
1170 my @items = $biblio->items->as_list;
1171 my $item_count = scalar @items;
1172 if ($item_count > 1) {
1173 $errors->{itemcount} = 1;
1176 # Failed validation, go back to the form
1179 method => 'check_out',
1183 statistical => $statistical_patrons,
1184 itemtypes => $itemtypes,
1185 libraries => $libraries,
1194 # Create an item if one doesn't already exist,
1195 # if one does, use that
1197 if ($item_count == 0) {
1199 biblionumber => $self->biblio_id,
1200 homebranch => $params->{branchcode},
1201 holdingbranch => $params->{branchcode},
1202 location => $params->{branchcode},
1203 itype => $params->{item_type},
1204 barcode => 'ILL-' . $self->illrequest_id
1207 my $item = Koha::Item->new($item_hash)->store;
1208 $itemnumber = $item->itemnumber;
1211 $itemnumber = $items[0]->itemnumber;
1213 # Check we have an item before going forward
1216 method => 'check_out',
1220 itemtypes => $itemtypes,
1221 libraries => $libraries,
1222 statistical => $statistical_patrons,
1223 errors => { item_creation => 1 }
1230 # Gather what we need
1231 my $target_item = Koha::Items->find( $itemnumber );
1232 # Determine who we're issuing to
1233 my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
1234 Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
1239 scalar $target_item->barcode
1241 if ($params->{duedate} && length $params->{duedate} > 0) {
1242 push @issue_args, dt_from_string($params->{duedate});
1244 # Check if we can check out
1245 my ( $error, $confirm, $alerts, $messages ) =
1246 C4::Circulation::CanBookBeIssued(@issue_args);
1248 # If we got anything back saying we can't check out,
1249 # return it to the template
1251 if ( $error && %{$error} ) { $problems->{error} = $error };
1252 if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
1253 if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
1254 if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
1258 method => 'check_out',
1262 itemtypes => $itemtypes,
1263 libraries => $libraries,
1264 statistical => $statistical_patrons,
1267 check_out_errors => $problems
1272 # We can allegedly check out, so make it so
1273 # For some reason, AddIssue requires an unblessed Patron
1274 $issue_args[0] = $patron->unblessed;
1275 my $issue = C4::Circulation::AddIssue(@issue_args);
1278 # Update the request status
1279 $self->status('CHK')->store;
1281 method => 'check_out',
1282 stage => 'done_check_out',
1291 method => 'check_out',
1295 itemtypes => $itemtypes,
1296 libraries => $libraries,
1297 errors => { item_check_out => 1 }
1305 =head3 generic_confirm
1307 my $stage_summary = $illRequest->generic_confirm;
1309 Handle the generic_confirm extended method. The first stage involves creating
1310 a template email for the end user to edit in the browser. The second stage
1311 attempts to submit the email.
1315 sub generic_confirm {
1316 my ( $self, $params ) = @_;
1317 my $branch = Koha::Libraries->find($params->{current_branchcode})
1318 || die "Invalid current branchcode. Are you logged in as the database user?";
1319 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
1320 # Get the message body from the notice definition
1321 my $letter = $self->get_notice({
1322 notice_code => 'ILL_PARTNER_REQ',
1323 transport => 'email'
1326 my $partners = Koha::Patrons->search({
1327 categorycode => $self->_config->partner_code
1333 method => 'generic_confirm',
1337 subject => $letter->{title},
1338 body => $letter->{content}
1340 partners => $partners,
1344 } elsif ( 'draft' eq $params->{stage} ) {
1345 # Create the to header
1346 my $to = $params->{partners};
1347 if ( defined $to ) {
1348 $to =~ s/^\x00//; # Strip leading NULLs
1349 $to =~ s/\x00/; /; # Replace others with '; '
1351 Koha::Exceptions::Ill::NoTargetEmail->throw(
1352 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
1354 # Create the from, replyto and sender headers
1355 my $from = $branch->from_email_address;
1356 my $replyto = $branch->inbound_ill_address;
1357 Koha::Exceptions::Ill::NoLibraryEmail->throw(
1358 "Your library has no usable email address. Please set it.")
1361 # So we get a notice hashref, then substitute the possibly
1362 # modified title and body from the draft stage
1363 my $letter = $self->get_notice({
1364 notice_code => 'ILL_PARTNER_REQ',
1365 transport => 'email'
1367 $letter->{title} = $params->{subject};
1368 $letter->{content} = $params->{body};
1373 borrowernumber => $self->borrowernumber,
1374 message_transport_type => 'email',
1376 from_address => $from,
1377 reply_address => $replyto
1381 my $result = C4::Letters::EnqueueLetter($params);
1383 $self->status("GENREQ")->store;
1384 $self->_backend_capability(
1385 'set_requested_partners',
1395 method => 'generic_confirm',
1403 status => 'email_failed',
1404 message => 'Email queueing failed',
1405 method => 'generic_confirm',
1409 die "Unknown stage, should not have happened."
1413 =head3 send_patron_notice
1415 my $result = $request->send_patron_notice($notice_code);
1417 Send a specified notice regarding this request to a patron
1421 sub send_patron_notice {
1422 my ( $self, $notice_code ) = @_;
1424 # We need a notice code
1425 if (!$notice_code) {
1427 error => 'notice_no_type'
1431 # Map from the notice code to the messaging preference
1432 my %message_name = (
1433 ILL_PICKUP_READY => 'Ill_ready',
1434 ILL_REQUEST_UNAVAIL => 'Ill_unavailable'
1437 # Get the patron's messaging preferences
1438 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
1439 borrowernumber => $self->borrowernumber,
1440 message_name => $message_name{$notice_code}
1442 my @transports = keys %{ $borrower_preferences->{transports} };
1444 # Notice should come from the library where the request was placed,
1445 # not the patrons home library
1446 my $branch = Koha::Libraries->find($self->branchcode);
1447 my $from_address = $branch->from_email_address;
1448 my $reply_address = $branch->inbound_ill_address;
1450 # Send the notice to the patron via the chosen transport methods
1451 # and record the results
1454 for my $transport (@transports) {
1455 my $letter = $self->get_notice({
1456 notice_code => $notice_code,
1457 transport => $transport
1460 my $result = C4::Letters::EnqueueLetter({
1462 borrowernumber => $self->borrowernumber,
1463 message_transport_type => $transport,
1464 from_address => $from_address,
1465 reply_address => $reply_address
1468 push @success, $transport;
1470 push @fail, $transport;
1473 push @fail, $transport;
1476 if (scalar @success > 0) {
1477 my $logger = Koha::Illrequest::Logger->new;
1478 $logger->log_patron_notice({
1480 notice_code => $notice_code
1485 success => \@success,
1491 =head3 send_staff_notice
1493 my $result = $request->send_staff_notice($notice_code);
1495 Send a specified notice regarding this request to staff
1499 sub send_staff_notice {
1500 my ( $self, $notice_code ) = @_;
1502 # We need a notice code
1503 if (!$notice_code) {
1505 error => 'notice_no_type'
1509 # Get the staff notices that have been assigned for sending in
1511 my $staff_to_send = C4::Context->preference('ILLSendStaffNotices') // q{};
1513 # If it hasn't been enabled in the syspref, we don't want to send it
1514 if ($staff_to_send !~ /\b$notice_code\b/) {
1516 error => 'notice_not_enabled'
1520 my $letter = $self->get_notice({
1521 notice_code => $notice_code,
1522 transport => 'email'
1525 # Try and get an address to which to send staff notices
1526 my $branch = Koha::Libraries->find($self->branchcode);
1527 my $to_address = $branch->inbound_ill_address;
1528 my $from_address = $branch->inbound_ill_address;
1532 borrowernumber => $self->borrowernumber,
1533 message_transport_type => 'email',
1534 from_address => $from_address
1538 $params->{to_address} = $to_address;
1541 error => 'notice_no_create'
1546 C4::Letters::EnqueueLetter($params)
1547 or warn "can't enqueue letter $letter";
1549 success => 'notice_queued'
1553 error => 'notice_no_create'
1560 my $notice = $request->get_notice($params);
1562 Return a compiled notice hashref for the passed notice code
1568 my ( $self, $params ) = @_;
1570 my $title = $self->illrequestattributes->find(
1573 my $author = $self->illrequestattributes->find(
1574 { type => 'author' }
1576 my $metahash = $self->metadata;
1578 foreach my $key (sort { lc $a cmp lc $b } keys %{$metahash}) {
1579 my $value = $metahash->{$key};
1580 push @metaarray, "- $key: $value" if $value;
1582 my $metastring = join("\n", @metaarray);
1583 my $letter = C4::Letters::GetPreparedLetter(
1585 letter_code => $params->{notice_code},
1586 branchcode => $self->branchcode,
1587 message_transport_type => $params->{transport},
1588 lang => $self->patron->lang,
1590 illrequests => $self->illrequest_id,
1591 borrowers => $self->borrowernumber,
1592 biblio => $self->biblio_id,
1593 branches => $self->branchcode,
1596 ill_bib_title => $title ? $title->value : '',
1597 ill_bib_author => $author ? $author->value : '',
1598 ill_full_metadata => $metastring
1607 my $prefix = $record->id_prefix;
1609 Return the prefix appropriate for the current Illrequest as derived from the
1610 borrower and branch associated with this request's Status, and the config
1617 my $prefix = $self->getPrefix( {
1618 branch => $self->branchcode,
1620 $prefix .= "-" if ( $prefix );
1626 my $params = $illRequest->_censor($params);
1628 Return $params, modified to reflect our censorship requirements.
1633 my ( $self, $params ) = @_;
1634 my $censorship = $self->_config->censorship;
1635 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1636 if ( $params->{opac} );
1637 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1646 Overloaded I<store> method that, in addition to performing the 'store',
1647 possibly records the fact that something happened
1652 my ( $self, $attrs ) = @_;
1654 my $ret = $self->SUPER::store;
1656 $attrs->{log_origin} = 'core';
1658 if ($ret && defined $attrs) {
1659 my $logger = Koha::Illrequest::Logger->new;
1660 $logger->log_maybe({
1669 =head3 requested_partners
1671 my $partners_string = $illRequest->requested_partners;
1673 Return the string representing the email addresses of the partners to
1674 whom a request has been sent
1678 sub requested_partners {
1680 return $self->_backend_capability(
1681 'get_requested_partners',
1682 { request => $self }
1688 $json = $illrequest->TO_JSON
1690 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1691 into the unblessed representation of the object.
1693 TODO: This method does nothing and is not called anywhere. However, bug 74325
1694 touches it, so keeping this for now until both this and bug 74325 are merged,
1695 at which point we can sort it out and remove it completely
1700 my ( $self, $embed ) = @_;
1702 my $object = $self->SUPER::TO_JSON();
1707 =head2 Internal methods
1714 return 'Illrequest';
1719 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1720 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>