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>.
23 use File::Basename qw( basename );
24 use Encode qw( encode );
31 use Koha::DateUtils qw/ dt_from_string /;
33 use Koha::Exceptions::Ill;
34 use Koha::Illcomments;
35 use Koha::Illrequestattributes;
36 use Koha::AuthorisedValue;
37 use Koha::Illrequest::Logger;
39 use Koha::AuthorisedValues;
45 use C4::Circulation qw( CanBookBeIssued AddIssue );
47 use base qw(Koha::Object);
51 Koha::Illrequest - Koha Illrequest Object class
55 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
56 of related Illrequestattributes.
58 The former encapsulates the basic necessary information that any ILL requires
59 to be usable in Koha. The latter is a set of additional properties used by
62 The former subsumes the legacy "Status" object. The latter remains
63 encapsulated in the "Record" object.
67 - Anything invoking the ->status method; annotated with:
68 + # Old use of ->status !
72 =head2 Backend API Response Principles
74 All methods should return a hashref in the following format:
80 This should be set to 1 if an error was encountered.
84 The status should be a string from the list of statuses detailed below.
88 The message is a free text field that can be passed on to the end user.
92 The value returned by the method.
96 =head2 Interface Status Messages
100 =item * branch_address_incomplete
102 An interface request has determined branch address details are incomplete.
104 =item * cancel_success
106 The interface's cancel_request method was successful in cancelling the
107 Illrequest using the API.
111 The interface's cancel_request method failed to cancel the Illrequest using
116 The interface's request method returned saying that the desired item is not
117 available for request.
125 my $statusalias = $request->statusalias;
127 Returns a request's status alias, as a Koha::AuthorisedValue instance
128 or implicit undef. This is distinct from status_alias, which only returns
129 the value in the status_alias column, this method returns the entire
130 AuthorisedValue object
136 return unless $self->status_alias;
137 # We can't know which result is the right one if there are multiple
138 # ILLSTATUS authorised values with the same authorised_value column value
139 # so we just use the first
140 return Koha::AuthorisedValues->search({
141 branchcode => $self->branchcode,
142 category => 'ILLSTATUS',
143 authorised_value => $self->SUPER::status_alias
147 =head3 illrequestattributes
151 sub illrequestattributes {
153 return Koha::Illrequestattributes->_new_from_dbic(
154 scalar $self->_result->illrequestattributes
164 return Koha::Illcomments->_new_from_dbic(
165 scalar $self->_result->illcomments
175 my $logger = Koha::Illrequest::Logger->new;
176 return $logger->get_request_logs($self);
185 return Koha::Patron->_new_from_dbic(
186 scalar $self->_result->borrowernumber
192 $Illrequest->status_alias(143);
194 Overloaded getter/setter for status_alias,
195 that only returns authorised values from the
196 correct category and records the fact that the status has changed
201 my ($self, $new_status_alias) = @_;
203 my $current_status_alias = $self->SUPER::status_alias;
205 if ($new_status_alias) {
206 # Keep a record of the previous status before we change it,
208 $self->{previous_status} = $current_status_alias ?
209 $current_status_alias :
210 scalar $self->status;
211 # This is hackery to enable us to undefine
212 # status_alias, since we need to have an overloaded
213 # status_alias method to get us around the problem described
215 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
216 # We need a way of accepting implied undef, so we can nullify
217 # the status_alias column, when called from $self->status
218 my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
219 my $ret = $self->SUPER::status_alias($val);
220 my $val_to_log = $val ? $new_status_alias : scalar $self->status;
222 my $logger = Koha::Illrequest::Logger->new;
223 $logger->log_status_change({
228 delete $self->{previous_status};
232 # We can't know which result is the right one if there are multiple
233 # ILLSTATUS authorised values with the same authorised_value column value
234 # so we just use the first
235 my $alias = Koha::AuthorisedValues->search({
236 branchcode => $self->branchcode,
237 category => 'ILLSTATUS',
238 authorised_value => $self->SUPER::status_alias
241 return $alias->authorised_value;
249 $Illrequest->status('CANREQ');
251 Overloaded getter/setter for request status,
252 also nullifies status_alias and records the fact that the status has changed
253 and sends a notice if appropriate
258 my ( $self, $new_status) = @_;
260 my $current_status = $self->SUPER::status;
261 my $current_status_alias = $self->SUPER::status_alias;
264 # Keep a record of the previous status before we change it,
266 $self->{previous_status} = $current_status_alias ?
267 $current_status_alias :
269 my $ret = $self->SUPER::status($new_status)->store;
270 if ($current_status_alias) {
271 # This is hackery to enable us to undefine
272 # status_alias, since we need to have an overloaded
273 # status_alias method to get us around the problem described
275 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
276 # We need a way of passing implied undef to nullify status_alias
277 # so we pass -1, which is special cased in the overloaded setter
278 $self->status_alias("-1");
280 my $logger = Koha::Illrequest::Logger->new;
281 $logger->log_status_change({
286 delete $self->{previous_status};
287 # If status has changed to cancellation requested, send a notice
288 if ($new_status eq 'CANCREQ') {
289 $self->send_staff_notice('ILL_REQUEST_CANCEL');
293 return $current_status;
299 Require "Base.pm" from the relevant ILL backend.
304 my ( $self, $backend_id ) = @_;
306 my @raw = qw/Koha Illbackends/; # Base Path
308 my $backend_name = $backend_id || $self->backend;
310 unless ( defined $backend_name && $backend_name ne '' ) {
311 Koha::Exceptions::Ill::InvalidBackendId->throw(
312 "An invalid backend ID was requested ('')");
315 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
316 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
318 $self->{_my_backend} = $backend_class->new({
319 config => $self->_config,
320 logger => Koha::Illrequest::Logger->new
328 my $backend = $abstract->_backend($new_backend);
329 my $backend = $abstract->_backend;
331 Getter/Setter for our API object.
336 my ( $self, $backend ) = @_;
337 $self->{_my_backend} = $backend if ( $backend );
338 # Dynamically load our backend object, as late as possible.
339 $self->load_backend unless ( $self->{_my_backend} );
340 return $self->{_my_backend};
343 =head3 _backend_capability
345 my $backend_capability_result = $self->_backend_capability($name, $args);
347 This is a helper method to invoke optional capabilities in the backend. If
348 the capability named by $name is not supported, return 0, else invoke it,
349 passing $args along with the invocation, and return its return value.
351 NOTE: this module suffers from a confusion in termninology:
353 in _backend_capability, the notion of capability refers to an optional feature
354 that is implemented in core, but might not be supported by a given backend.
356 in capabilities & custom_capability, capability refers to entries in the
357 status_graph (after union between backend and core).
359 The easiest way to fix this would be to fix the terminology in
360 capabilities & custom_capability and their callers.
364 sub _backend_capability {
365 my ( $self, $name, $args ) = @_;
367 # See if capability is defined in backend
369 $capability = $self->_backend->capabilities($name);
374 if ( $capability && ref($capability) eq 'CODE' ) {
375 return &{$capability}($args);
383 my $config = $abstract->_config($config);
384 my $config = $abstract->_config;
386 Getter/Setter for our config object.
391 my ( $self, $config ) = @_;
392 $self->{_my_config} = $config if ( $config );
393 # Load our config object, as late as possible.
394 unless ( $self->{_my_config} ) {
395 $self->{_my_config} = Koha::Illrequest::Config->new;
397 return $self->{_my_config};
406 return $self->_backend->metadata($self);
409 =head3 _core_status_graph
411 my $core_status_graph = $illrequest->_core_status_graph;
413 Returns ILL module's default status graph. A status graph defines the list of
414 available actions at any stage in the ILL workflow. This is for instance used
415 by the perl script & template to generate the correct buttons to display to
416 the end user at any given point.
420 sub _core_status_graph {
424 prev_actions => [ ], # Actions containing buttons
425 # leading to this status
426 id => 'NEW', # ID of this status
427 name => 'New request', # UI name of this status
428 ui_method_name => 'New request', # UI name of method leading
430 method => 'create', # method to this status
431 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
432 # requests with this status
433 ui_method_icon => 'fa-plus', # UI Style class
436 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
439 ui_method_name => 'Confirm request',
441 next_actions => [ 'REQREV', 'COMP', 'CHK' ],
442 ui_method_icon => 'fa-check',
445 prev_actions => [ 'NEW', 'REQREV' ],
447 name => 'Requested from partners',
448 ui_method_name => 'Place request with partners',
449 method => 'generic_confirm',
450 next_actions => [ 'COMP', 'CHK' ],
451 ui_method_icon => 'fa-send-o',
454 prev_actions => [ 'REQ' ],
456 name => 'Request reverted',
457 ui_method_name => 'Revert Request',
459 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
460 ui_method_icon => 'fa-times',
465 name => 'Queued request',
468 next_actions => [ 'REQ', 'KILL' ],
472 prev_actions => [ 'NEW' ],
474 name => 'Cancellation requested',
477 next_actions => [ 'KILL', 'REQ' ],
481 prev_actions => [ 'REQ' ],
484 ui_method_name => 'Mark completed',
485 method => 'mark_completed',
486 next_actions => [ 'CHK' ],
487 ui_method_icon => 'fa-check',
490 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
493 ui_method_name => 'Delete request',
496 ui_method_icon => 'fa-trash',
499 prev_actions => [ 'REQ', 'GENREQ', 'COMP' ],
501 name => 'Checked out',
502 ui_method_name => 'Check out',
503 needs_prefs => [ 'CirculateILL' ],
504 needs_perms => [ 'user_circulate_circulate_remaining_permissions' ],
505 # An array of functions that all must return true
506 needs_all => [ sub { my $r = shift; return $r->biblio; } ],
507 method => 'check_out',
509 ui_method_icon => 'fa-upload',
512 prev_actions => [ 'CHK' ],
514 name => 'Returned to library',
515 ui_method_name => 'Check in',
516 method => 'check_in',
517 next_actions => [ 'COMP' ],
518 ui_method_icon => 'fa-download',
523 =head3 _status_graph_union
525 my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
527 Return a new status_graph, the result of merging $origin & new_graph. This is
528 operation is a union over the sets defied by the two graphs.
530 Each entry in $new_graph is added to $origin. We do not provide a syntax for
531 'subtraction' of entries from $origin.
533 Whilst it is not intended that this works, you can override entries in $origin
534 with entries with the same key in $new_graph. This can lead to problematic
535 behaviour when $new_graph adds an entry, which modifies a dependent entry in
536 $origin, only for the entry in $origin to be replaced later with a new entry
539 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
540 i.e. each of the graphs need to be correct at the outset of the operation.
544 sub _status_graph_union {
545 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
546 # Create new status graph with:
547 # - all core_status_graph
548 # - for-each each backend_status_graph
549 # + add to new status graph
550 # + for each core prev_action:
551 # * locate core_status
552 # * update next_actions with additional next action.
553 # + for each core next_action:
554 # * locate core_status
555 # * update prev_actions with additional prev action
557 my @core_status_ids = keys %{$core_status_graph};
558 my $status_graph = clone($core_status_graph);
560 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
561 my $backend_status = $backend_status_graph->{$backend_status_key};
562 # Add to new status graph
563 $status_graph->{$backend_status_key} = $backend_status;
564 # Update all core methods' next_actions.
565 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
566 if ( grep { $prev_action eq $_ } @core_status_ids ) {
568 @{$status_graph->{$prev_action}->{next_actions}};
569 push @next_actions, $backend_status_key;
570 $status_graph->{$prev_action}->{next_actions}
574 # Update all core methods' prev_actions
575 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
576 if ( grep { $next_action eq $_ } @core_status_ids ) {
578 @{$status_graph->{$next_action}->{prev_actions}};
579 push @prev_actions, $backend_status_key;
580 $status_graph->{$next_action}->{prev_actions}
586 return $status_graph;
593 my $capabilities = $illrequest->capabilities;
595 Return a hashref mapping methods to operation names supported by the queried
598 Example return value:
600 { create => "Create Request", confirm => "Progress Request" }
602 NOTE: this module suffers from a confusion in termninology:
604 in _backend_capability, the notion of capability refers to an optional feature
605 that is implemented in core, but might not be supported by a given backend.
607 in capabilities & custom_capability, capability refers to entries in the
608 status_graph (after union between backend and core).
610 The easiest way to fix this would be to fix the terminology in
611 capabilities & custom_capability and their callers.
616 my ( $self, $status ) = @_;
617 # Generate up to date status_graph
618 my $status_graph = $self->_status_graph_union(
619 $self->_core_status_graph,
620 $self->_backend->status_graph({
625 # Extract available actions from graph.
626 return $status_graph->{$status} if $status;
627 # Or return entire graph.
628 return $status_graph;
631 =head3 custom_capability
633 Return the result of invoking $CANDIDATE on this request's backend with
634 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
636 NOTE: this module suffers from a confusion in termninology:
638 in _backend_capability, the notion of capability refers to an optional feature
639 that is implemented in core, but might not be supported by a given backend.
641 in capabilities & custom_capability, capability refers to entries in the
642 status_graph (after union between backend and core).
644 The easiest way to fix this would be to fix the terminology in
645 capabilities & custom_capability and their callers.
649 sub custom_capability {
650 my ( $self, $candidate, $params ) = @_;
651 foreach my $capability ( values %{$self->capabilities} ) {
652 if ( $candidate eq $capability->{method} ) {
654 $self->_backend->$candidate({
658 return $self->expandTemplate($response);
664 =head3 available_backends
666 Return a list of available backends.
670 sub available_backends {
671 my ( $self, $reduced ) = @_;
672 my $backends = $self->_config->available_backends($reduced);
676 =head3 available_actions
678 Return a list of available actions.
682 sub available_actions {
684 my $current_action = $self->capabilities($self->status);
685 my @available_actions = map { $self->capabilities($_) }
686 @{$current_action->{next_actions}};
687 return \@available_actions;
690 =head3 mark_completed
692 Mark a request as completed (status = COMP).
698 $self->status('COMP')->store;
699 $self->completed(dt_from_string())->store;
704 method => 'mark_completed',
710 =head2 backend_migrate
712 Migrate a request from one backend to another.
716 sub backend_migrate {
717 my ( $self, $params ) = @_;
719 my $response = $self->_backend_capability('migrate',{
723 return $self->expandTemplate($response) if $response;
727 =head2 backend_confirm
729 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
735 =item * accessurl, cost (if available).
741 sub backend_confirm {
742 my ( $self, $params ) = @_;
744 my $response = $self->_backend->confirm({
748 return $self->expandTemplate($response);
751 =head3 backend_update_status
755 sub backend_update_status {
756 my ( $self, $params ) = @_;
757 return $self->expandTemplate($self->_backend->update_status($params));
760 =head3 backend_cancel
762 my $ILLResponse = $illRequest->backend_cancel;
764 The standard interface method allowing for request cancellation.
769 my ( $self, $params ) = @_;
771 my $result = $self->_backend->cancel({
776 return $self->expandTemplate($result);
781 my $renew_response = $illRequest->backend_renew;
783 The standard interface method allowing for request renewal queries.
789 return $self->expandTemplate(
790 $self->_backend->renew({
796 =head3 backend_create
798 my $create_response = $abstractILL->backend_create($params);
800 Return an array of Record objects created by querying our backend with
803 In the context of the other ILL methods, this is a special method: we only
804 pass it $params, as it does not yet have any other data associated with it.
809 my ( $self, $params ) = @_;
811 # Establish whether we need to do a generic copyright clearance.
812 if ($params->{opac}) {
813 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
814 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
820 stage => 'copyrightclearance',
823 backend => $self->_backend->name
826 } elsif ( defined $params->{stage}
827 && $params->{stage} eq 'copyrightclearance' ) {
828 $params->{stage} = 'init';
831 # First perform API action, then...
836 my $result = $self->_backend->create($args);
838 # ... simple case: we're not at 'commit' stage.
839 my $stage = $result->{stage};
840 return $self->expandTemplate($result)
841 unless ( 'commit' eq $stage );
843 # ... complex case: commit!
845 # Do we still have space for an ILL or should we queue?
846 my $permitted = $self->check_limits(
847 { patron => $self->patron }, { librarycode => $self->branchcode }
850 # Now augment our committed request.
852 $result->{permitted} = $permitted; # Queue request?
856 # ...Updating status!
857 $self->status('QUEUED')->store unless ( $permitted );
859 ## Handle Unmediated ILLs
861 # For the unmediated workflow we only need to delegate to our backend. If
862 # that backend supports unmediateld_ill, it will do its thing and return a
863 # proper response. If it doesn't then _backend_capability returns 0, so
864 # we keep the current result.
865 if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
866 my $unmediated_result = $self->_backend_capability(
870 $result = $unmediated_result if $unmediated_result;
873 return $self->expandTemplate($result);
876 =head3 expandTemplate
878 my $params = $abstract->expandTemplate($params);
880 Return a version of $PARAMS augmented with our required template path.
885 my ( $self, $params ) = @_;
886 my $backend = $self->_backend->name;
887 # Generate path to file to load
888 my $backend_dir = $self->_config->backend_dir;
889 my $backend_tmpl = join "/", $backend_dir, $backend;
890 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
891 ( $params->{method}//q{} ) . ".inc";
892 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
893 ( $params->{method}//q{} ) . ".inc";
895 $params->{template} = $intra_tmpl;
896 $params->{opac_template} = $opac_tmpl;
900 #### Abstract Imports
904 my $limit_rules = $abstract->getLimits( {
905 type => 'brw_cat' | 'branch',
909 Return the ILL limit rules for the supplied combination of type / value.
911 As the config may have no rules for this particular type / value combination,
912 or for the default, we must define fall-back values here.
917 my ( $self, $params ) = @_;
918 my $limits = $self->_config->getLimitRules($params->{type});
920 if ( defined $params->{value}
921 && defined $limits->{$params->{value}} ) {
922 return $limits->{$params->{value}};
925 return $limits->{default} || { count => -1, method => 'active' };
931 my $prefix = $abstract->getPrefix( {
932 branch => $branch_code
935 Return the ILL prefix as defined by our $params: either per borrower category,
936 per branch or the default.
941 my ( $self, $params ) = @_;
942 my $brn_prefixes = $self->_config->getPrefixes();
943 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
948 my $type = $abstract->get_type();
950 Return a string representing the material type of this request or undef
956 my $attr = $self->illrequestattributes->find({ type => 'type'});
961 #### Illrequests Imports
965 my $ok = $illRequests->check_limits( {
966 borrower => $borrower,
967 branchcode => 'branchcode' | undef,
970 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
971 see whether we are still able to place ILLs.
973 LimitRules are derived from koha-conf.xml:
974 + default limit counts, and counting method
975 + branch specific limit counts & counting method
976 + borrower category specific limit counts & counting method
977 + err on the side of caution: a counting fail will cause fail, even if
978 the other counts passes.
983 my ( $self, $params ) = @_;
984 my $patron = $params->{patron};
985 my $branchcode = $params->{librarycode} || $patron->branchcode;
987 # Establish maximum number of allowed requests
988 my ( $branch_rules, $brw_rules ) = (
995 value => $patron->categorycode,
998 my ( $branch_limit, $brw_limit )
999 = ( $branch_rules->{count}, $brw_rules->{count} );
1000 # Establish currently existing requests
1001 my ( $branch_count, $brw_count ) = (
1002 $self->_limit_counter(
1003 $branch_rules->{method}, { branchcode => $branchcode }
1005 $self->_limit_counter(
1006 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
1010 # Compare and return
1011 # A limit of -1 means no limit exists.
1012 # We return blocked if either branch limit or brw limit is reached.
1013 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
1014 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
1021 sub _limit_counter {
1022 my ( $self, $method, $target ) = @_;
1024 # Establish parameters of counts
1026 if ($method && $method eq 'annual') {
1027 $resultset = Koha::Illrequests->search({
1030 \"YEAR(placed) = YEAR(NOW())"
1033 } else { # assume 'active'
1034 # XXX: This status list is ugly. There should be a method in config
1036 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1037 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1041 return $resultset->count;
1044 =head3 requires_moderation
1046 my $status = $illRequest->requires_moderation;
1048 Return the name of the status if moderation by staff is required; or 0
1053 sub requires_moderation {
1055 my $require_moderation = {
1056 'CANCREQ' => 'CANCREQ',
1058 return $require_moderation->{$self->status};
1063 my $biblio = $request->biblio;
1065 For a given request, return the biblio associated with it,
1066 or undef if none exists
1073 return if !$self->biblio_id;
1075 return Koha::Biblios->find({
1076 biblionumber => $self->biblio_id
1082 my $stage_summary = $request->check_out;
1084 Handle the check_out method. The first stage involves gathering the required
1085 data from the user via a form, the second stage creates an item and tries to
1086 issue it to the patron. If successful, it notifies the patron, then it
1087 returns a summary of how things went
1092 my ( $self, $params ) = @_;
1094 # Objects required by the template
1095 my $itemtypes = Koha::ItemTypes->search(
1097 { order_by => ['description'] }
1099 my $libraries = Koha::Libraries->search(
1101 { order_by => ['branchcode'] }
1103 my $biblio = $self->biblio;
1105 # Find all statistical patrons
1106 my $statistical_patrons = Koha::Patrons->search(
1107 { 'category_type' => 'x' },
1108 { join => { 'categorycode' => 'borrowers' } }
1111 if (!$params->{stage} || $params->{stage} eq 'init') {
1112 # Present a form to gather the required data
1114 # We may be viewing this page having previously tried to issue
1115 # the item (in which case, we may already have created an item)
1116 # so we pass the biblio for this request
1118 method => 'check_out',
1121 itemtypes => $itemtypes,
1122 libraries => $libraries,
1123 statistical => $statistical_patrons,
1127 } elsif ($params->{stage} eq 'form') {
1128 # Validate what we've got and return with an error if we fail
1130 if (!$params->{item_type} || length $params->{item_type} == 0) {
1131 $errors->{item_type} = 1;
1133 if ($params->{inhouse} && length $params->{inhouse} > 0) {
1134 my $patron_count = Koha::Patrons->search({
1135 cardnumber => $params->{inhouse}
1137 if ($patron_count != 1) {
1138 $errors->{inhouse} = 1;
1142 # Check we don't have more than one item for this bib,
1143 # if we do, something very odd is going on
1144 # Having 1 is OK, it means we're likely trying to issue
1145 # following a previously failed attempt, the item exists
1147 my @items = $biblio->items->as_list;
1148 my $item_count = scalar @items;
1149 if ($item_count > 1) {
1150 $errors->{itemcount} = 1;
1153 # Failed validation, go back to the form
1156 method => 'check_out',
1160 statistical => $statistical_patrons,
1161 itemtypes => $itemtypes,
1162 libraries => $libraries,
1171 # Create an item if one doesn't already exist,
1172 # if one does, use that
1174 if ($item_count == 0) {
1176 biblionumber => $self->biblio_id,
1177 homebranch => $params->{branchcode},
1178 holdingbranch => $params->{branchcode},
1179 location => $params->{branchcode},
1180 itype => $params->{item_type},
1181 barcode => 'ILL-' . $self->illrequest_id
1184 my $item = Koha::Item->new($item_hash)->store;
1185 $itemnumber = $item->itemnumber;
1188 $itemnumber = $items[0]->itemnumber;
1190 # Check we have an item before going forward
1193 method => 'check_out',
1197 itemtypes => $itemtypes,
1198 libraries => $libraries,
1199 statistical => $statistical_patrons,
1200 errors => { item_creation => 1 }
1207 # Gather what we need
1208 my $target_item = Koha::Items->find( $itemnumber );
1209 # Determine who we're issuing to
1210 my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
1211 Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
1216 scalar $target_item->barcode
1218 if ($params->{duedate} && length $params->{duedate} > 0) {
1219 push @issue_args, $params->{duedate};
1221 # Check if we can check out
1222 my ( $error, $confirm, $alerts, $messages ) =
1223 C4::Circulation::CanBookBeIssued(@issue_args);
1225 # If we got anything back saying we can't check out,
1226 # return it to the template
1228 if ( $error && %{$error} ) { $problems->{error} = $error };
1229 if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
1230 if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
1231 if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
1235 method => 'check_out',
1239 itemtypes => $itemtypes,
1240 libraries => $libraries,
1241 statistical => $statistical_patrons,
1244 check_out_errors => $problems
1249 # We can allegedly check out, so make it so
1250 # For some reason, AddIssue requires an unblessed Patron
1251 $issue_args[0] = $patron->unblessed;
1252 my $issue = C4::Circulation::AddIssue(@issue_args);
1255 # Update the request status
1256 $self->status('CHK')->store;
1258 method => 'check_out',
1259 stage => 'done_check_out',
1268 method => 'check_out',
1272 itemtypes => $itemtypes,
1273 libraries => $libraries,
1274 errors => { item_check_out => 1 }
1282 =head3 generic_confirm
1284 my $stage_summary = $illRequest->generic_confirm;
1286 Handle the generic_confirm extended method. The first stage involves creating
1287 a template email for the end user to edit in the browser. The second stage
1288 attempts to submit the email.
1292 sub generic_confirm {
1293 my ( $self, $params ) = @_;
1294 my $library = Koha::Libraries->find($params->{current_branchcode})
1295 || die "Invalid current branchcode. Are you logged in as the database user?";
1296 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
1297 # Get the message body from the notice definition
1298 my $letter = $self->get_notice({
1299 notice_code => 'ILL_PARTNER_REQ',
1300 transport => 'email'
1303 my $partners = Koha::Patrons->search({
1304 categorycode => $self->_config->partner_code
1310 method => 'generic_confirm',
1314 subject => $letter->{title},
1315 body => $letter->{content}
1317 partners => $partners,
1321 } elsif ( 'draft' eq $params->{stage} ) {
1322 # Create the to header
1323 my $to = $params->{partners};
1324 if ( defined $to ) {
1325 $to =~ s/^\x00//; # Strip leading NULLs
1326 $to =~ s/\x00/; /; # Replace others with '; '
1328 Koha::Exceptions::Ill::NoTargetEmail->throw(
1329 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
1331 # Create the from, replyto and sender headers
1332 my $from = $branch->branchillemail || $branch->branchemail;
1333 my $replyto = $branch->branchreplyto || $from;
1334 Koha::Exceptions::Ill::NoLibraryEmail->throw(
1335 "Your library has no usable email address. Please set it.")
1338 # So we get a notice hashref, then substitute the possibly
1339 # modified title and body from the draft stage
1340 my $letter = $self->get_notice({
1341 notice_code => 'ILL_PARTNER_REQ',
1342 transport => 'email'
1344 $letter->{title} = $params->{subject};
1345 $letter->{content} = $params->{body};
1350 borrowernumber => $self->borrowernumber,
1351 message_transport_type => 'email',
1353 from_address => $from
1357 my $result = C4::Letters::EnqueueLetter($params);
1359 $self->status("GENREQ")->store;
1360 $self->_backend_capability(
1361 'set_requested_partners',
1371 method => 'generic_confirm',
1379 status => 'email_failed',
1380 message => 'Email queueing failed',
1381 method => 'generic_confirm',
1385 die "Unknown stage, should not have happened."
1389 =head3 get_staff_to_address
1391 my $email = $request->get_staff_to_address();
1393 Get the email address to which staff notices should be sent
1397 sub get_staff_to_address {
1400 # The various places we can get an ILL staff email address from
1401 # (In order of preference)
1403 # Dedicated branch address
1404 my $library = Koha::Libraries->find( $self->branchcode );
1405 my $branch_ill_to = $library->branchillemail;
1406 # General purpose ILL address from syspref
1407 my $syspref = C4::Context->preference("ILLDefaultStaffEmail");
1408 # Branch general email address
1409 my $branch_to = $library->branchemail;
1411 my $koha_admin = C4::Context->preference('KohaAdminEmailAddress');
1414 if ($branch_ill_to) {
1415 $to = $branch_ill_to;
1416 } elsif ($syspref) {
1418 } elsif ($branch_to) {
1420 } elsif ($koha_admin) {
1424 # $to will not be defined if we didn't find a usable address
1428 =head3 send_patron_notice
1430 my $result = $request->send_patron_notice($notice_code);
1432 Send a specified notice regarding this request to a patron
1436 sub send_patron_notice {
1437 my ( $self, $notice_code ) = @_;
1439 # We need a notice code
1440 if (!$notice_code) {
1442 error => 'notice_no_type'
1446 # Map from the notice code to the messaging preference
1447 my %message_name = (
1448 ILL_PICKUP_READY => 'Ill_ready',
1449 ILL_REQUEST_UNAVAIL => 'Ill_unavailable'
1452 # Get the patron's messaging preferences
1453 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
1454 borrowernumber => $self->borrowernumber,
1455 message_name => $message_name{$notice_code}
1457 my @transports = keys %{ $borrower_preferences->{transports} };
1459 # Send the notice to the patron via the chosen transport methods
1460 # and record the results
1463 for my $transport (@transports) {
1464 my $letter = $self->get_notice({
1465 notice_code => $notice_code,
1466 transport => $transport
1469 my $result = C4::Letters::EnqueueLetter({
1471 borrowernumber => $self->borrowernumber,
1472 message_transport_type => $transport,
1475 push @success, $transport;
1477 push @fail, $transport;
1480 push @fail, $transport;
1483 if (scalar @success > 0) {
1484 my $logger = Koha::Illrequest::Logger->new;
1485 $logger->log_patron_notice({
1487 notice_code => $notice_code
1492 success => \@success,
1498 =head3 send_staff_notice
1500 my $result = $request->send_staff_notice($notice_code);
1502 Send a specified notice regarding this request to staff
1506 sub send_staff_notice {
1507 my ( $self, $notice_code ) = @_;
1509 # We need a notice code
1510 if (!$notice_code) {
1512 error => 'notice_no_type'
1516 # Get the staff notices that have been assigned for sending in
1518 my $staff_to_send = C4::Context->preference('ILLSendStaffNotices');
1520 # If it hasn't been enabled in the syspref, we don't want to send it
1521 if ($staff_to_send !~ /\b$notice_code\b/) {
1523 error => 'notice_not_enabled'
1527 my $letter = $self->get_notice({
1528 notice_code => $notice_code,
1529 transport => 'email'
1532 # Try and get an address to which to send staff notices
1533 my $to_address = scalar $self->get_staff_to_address;
1537 borrowernumber => $self->borrowernumber,
1538 message_transport_type => 'email',
1542 $params->{to_address} = $to_address;
1543 $params->{from_address} = $to_address;
1546 error => 'notice_no_create'
1551 C4::Letters::EnqueueLetter($params)
1552 or warn "can't enqueue letter $letter";
1554 success => 'notice_queued'
1558 error => 'notice_no_create'
1565 my $notice = $request->get_notice($params);
1567 Return a compiled notice hashref for the passed notice code
1573 my ( $self, $params ) = @_;
1575 my $title = $self->illrequestattributes->find(
1578 my $author = $self->illrequestattributes->find(
1579 { type => 'author' }
1581 my $metahash = $self->metadata;
1583 while (my($key, $value) = each %{$metahash}) {
1584 push @metaarray, "- $key: $value" if $value;
1586 my $metastring = join("\n", @metaarray);
1587 my $letter = C4::Letters::GetPreparedLetter(
1589 letter_code => $params->{notice_code},
1590 message_transport_type => $params->{transport},
1591 lang => $self->patron->lang,
1593 illrequests => $self->illrequest_id,
1594 borrowers => $self->borrowernumber,
1595 biblio => $self->biblio_id,
1596 branches => $self->branchcode,
1599 ill_bib_title => $title ? $title->value : 'N/A',
1600 ill_bib_author => $author ? $author->value : 'N/A',
1601 ill_full_metadata => $metastring
1610 my $prefix = $record->id_prefix;
1612 Return the prefix appropriate for the current Illrequest as derived from the
1613 borrower and branch associated with this request's Status, and the config
1620 my $prefix = $self->getPrefix( {
1621 branch => $self->branchcode,
1623 $prefix .= "-" if ( $prefix );
1629 my $params = $illRequest->_censor($params);
1631 Return $params, modified to reflect our censorship requirements.
1636 my ( $self, $params ) = @_;
1637 my $censorship = $self->_config->censorship;
1638 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1639 if ( $params->{opac} );
1640 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1649 Overloaded I<store> method that, in addition to performing the 'store',
1650 possibly records the fact that something happened
1655 my ( $self, $attrs ) = @_;
1657 my $ret = $self->SUPER::store;
1659 $attrs->{log_origin} = 'core';
1661 if ($ret && defined $attrs) {
1662 my $logger = Koha::Illrequest::Logger->new;
1663 $logger->log_maybe({
1672 =head3 requested_partners
1674 my $partners_string = $illRequest->requested_partners;
1676 Return the string representing the email addresses of the partners to
1677 whom a request has been sent
1681 sub requested_partners {
1683 return $self->_backend_capability(
1684 'get_requested_partners',
1685 { request => $self }
1691 $json = $illrequest->TO_JSON
1693 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1694 into the unblessed representation of the object.
1696 TODO: This method does nothing and is not called anywhere. However, bug 74325
1697 touches it, so keeping this for now until both this and bug 74325 are merged,
1698 at which point we can sort it out and remove it completely
1703 my ( $self, $embed ) = @_;
1705 my $object = $self->SUPER::TO_JSON();
1710 =head2 Internal methods
1717 return 'Illrequest';
1722 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1723 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>