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 /;
32 use Koha::Exceptions::Ill;
33 use Koha::Illcomments;
34 use Koha::Illrequestattributes;
35 use Koha::AuthorisedValue;
36 use Koha::Illrequest::Logger;
38 use Koha::AuthorisedValues;
44 use C4::Circulation qw( CanBookBeIssued AddIssue );
46 use base qw(Koha::Object);
50 Koha::Illrequest - Koha Illrequest Object class
54 An ILLRequest consists of two parts; the Illrequest Koha::Object, and a series
55 of related Illrequestattributes.
57 The former encapsulates the basic necessary information that any ILL requires
58 to be usable in Koha. The latter is a set of additional properties used by
61 The former subsumes the legacy "Status" object. The latter remains
62 encapsulated in the "Record" object.
66 - Anything invoking the ->status method; annotated with:
67 + # Old use of ->status !
71 =head2 Backend API Response Principles
73 All methods should return a hashref in the following format:
79 This should be set to 1 if an error was encountered.
83 The status should be a string from the list of statuses detailed below.
87 The message is a free text field that can be passed on to the end user.
91 The value returned by the method.
95 =head2 Interface Status Messages
99 =item * branch_address_incomplete
101 An interface request has determined branch address details are incomplete.
103 =item * cancel_success
105 The interface's cancel_request method was successful in cancelling the
106 Illrequest using the API.
110 The interface's cancel_request method failed to cancel the Illrequest using
115 The interface's request method returned saying that the desired item is not
116 available for request.
124 my $statusalias = $request->statusalias;
126 Returns a request's status alias, as a Koha::AuthorisedValue instance
127 or implicit undef. This is distinct from status_alias, which only returns
128 the value in the status_alias column, this method returns the entire
129 AuthorisedValue object
135 return unless $self->status_alias;
136 # We can't know which result is the right one if there are multiple
137 # ILLSTATUS authorised values with the same authorised_value column value
138 # so we just use the first
139 return Koha::AuthorisedValues->search(
141 category => 'ILLSTATUS',
142 authorised_value => $self->SUPER::status_alias
149 =head3 illrequestattributes
153 sub illrequestattributes {
155 return Koha::Illrequestattributes->_new_from_dbic(
156 scalar $self->_result->illrequestattributes
166 return Koha::Illcomments->_new_from_dbic(
167 scalar $self->_result->illcomments
177 my $logger = Koha::Illrequest::Logger->new;
178 return $logger->get_request_logs($self);
187 return Koha::Patron->_new_from_dbic(
188 scalar $self->_result->borrowernumber
194 $Illrequest->status_alias(143);
196 Overloaded getter/setter for status_alias,
197 that only returns authorised values from the
198 correct category and records the fact that the status has changed
203 my ($self, $new_status_alias) = @_;
205 my $current_status_alias = $self->SUPER::status_alias;
207 if ($new_status_alias) {
208 # Keep a record of the previous status before we change it,
210 $self->{previous_status} = $current_status_alias ?
211 $current_status_alias :
212 scalar $self->status;
213 # This is hackery to enable us to undefine
214 # status_alias, since we need to have an overloaded
215 # status_alias method to get us around the problem described
217 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
218 # We need a way of accepting implied undef, so we can nullify
219 # the status_alias column, when called from $self->status
220 my $val = $new_status_alias eq "-1" ? undef : $new_status_alias;
221 my $ret = $self->SUPER::status_alias($val);
222 my $val_to_log = $val ? $new_status_alias : scalar $self->status;
224 my $logger = Koha::Illrequest::Logger->new;
225 $logger->log_status_change({
230 delete $self->{previous_status};
234 # We can't know which result is the right one if there are multiple
235 # ILLSTATUS authorised values with the same authorised_value column value
236 # so we just use the first
237 my $alias = Koha::AuthorisedValues->search(
239 category => 'ILLSTATUS',
240 authorised_value => $self->SUPER::status_alias
247 return $alias->authorised_value;
255 $Illrequest->status('CANREQ');
257 Overloaded getter/setter for request status,
258 also nullifies status_alias and records the fact that the status has changed
259 and sends a notice if appropriate
264 my ( $self, $new_status) = @_;
266 my $current_status = $self->SUPER::status;
267 my $current_status_alias = $self->SUPER::status_alias;
270 # Keep a record of the previous status before we change it,
272 $self->{previous_status} = $current_status_alias ?
273 $current_status_alias :
275 my $ret = $self->SUPER::status($new_status)->store;
276 if ($current_status_alias) {
277 # This is hackery to enable us to undefine
278 # status_alias, since we need to have an overloaded
279 # status_alias method to get us around the problem described
281 # https://bugs.koha-community.org/bugzilla3/show_bug.cgi?id=20581#c156
282 # We need a way of passing implied undef to nullify status_alias
283 # so we pass -1, which is special cased in the overloaded setter
284 $self->status_alias("-1");
286 my $logger = Koha::Illrequest::Logger->new;
287 $logger->log_status_change({
292 delete $self->{previous_status};
293 # If status has changed to cancellation requested, send a notice
294 if ($new_status eq 'CANCREQ') {
295 $self->send_staff_notice('ILL_REQUEST_CANCEL');
299 return $current_status;
305 Require "Base.pm" from the relevant ILL backend.
310 my ( $self, $backend_id ) = @_;
312 my @raw = qw/Koha Illbackends/; # Base Path
314 my $backend_name = $backend_id || $self->backend;
316 unless ( defined $backend_name && $backend_name ne '' ) {
317 Koha::Exceptions::Ill::InvalidBackendId->throw(
318 "An invalid backend ID was requested ('')");
321 my $location = join "/", @raw, $backend_name, "Base.pm"; # File to load
322 my $backend_class = join "::", @raw, $backend_name, "Base"; # Package name
324 $self->{_my_backend} = $backend_class->new({
325 config => $self->_config,
326 logger => Koha::Illrequest::Logger->new
334 my $backend = $abstract->_backend($new_backend);
335 my $backend = $abstract->_backend;
337 Getter/Setter for our API object.
342 my ( $self, $backend ) = @_;
343 $self->{_my_backend} = $backend if ( $backend );
344 # Dynamically load our backend object, as late as possible.
345 $self->load_backend unless ( $self->{_my_backend} );
346 return $self->{_my_backend};
349 =head3 _backend_capability
351 my $backend_capability_result = $self->_backend_capability($name, $args);
353 This is a helper method to invoke optional capabilities in the backend. If
354 the capability named by $name is not supported, return 0, else invoke it,
355 passing $args along with the invocation, and return its return value.
357 NOTE: this module suffers from a confusion in termninology:
359 in _backend_capability, the notion of capability refers to an optional feature
360 that is implemented in core, but might not be supported by a given backend.
362 in capabilities & custom_capability, capability refers to entries in the
363 status_graph (after union between backend and core).
365 The easiest way to fix this would be to fix the terminology in
366 capabilities & custom_capability and their callers.
370 sub _backend_capability {
371 my ( $self, $name, $args ) = @_;
373 # See if capability is defined in backend
375 $capability = $self->_backend->capabilities($name);
380 if ( $capability && ref($capability) eq 'CODE' ) {
381 return &{$capability}($args);
389 my $config = $abstract->_config($config);
390 my $config = $abstract->_config;
392 Getter/Setter for our config object.
397 my ( $self, $config ) = @_;
398 $self->{_my_config} = $config if ( $config );
399 # Load our config object, as late as possible.
400 unless ( $self->{_my_config} ) {
401 $self->{_my_config} = Koha::Illrequest::Config->new;
403 return $self->{_my_config};
412 return $self->_backend->metadata($self);
415 =head3 _core_status_graph
417 my $core_status_graph = $illrequest->_core_status_graph;
419 Returns ILL module's default status graph. A status graph defines the list of
420 available actions at any stage in the ILL workflow. This is for instance used
421 by the perl script & template to generate the correct buttons to display to
422 the end user at any given point.
426 sub _core_status_graph {
430 prev_actions => [ ], # Actions containing buttons
431 # leading to this status
432 id => 'NEW', # ID of this status
433 name => 'New request', # UI name of this status
434 ui_method_name => 'New request', # UI name of method leading
436 method => 'create', # method to this status
437 next_actions => [ 'REQ', 'GENREQ', 'KILL' ], # buttons to add to all
438 # requests with this status
439 ui_method_icon => 'fa-plus', # UI Style class
442 prev_actions => [ 'NEW', 'REQREV', 'QUEUED', 'CANCREQ' ],
445 ui_method_name => 'Confirm request',
447 next_actions => [ 'REQREV', 'COMP', 'CHK' ],
448 ui_method_icon => 'fa-check',
451 prev_actions => [ 'NEW', 'REQREV' ],
453 name => 'Requested from partners',
454 ui_method_name => 'Place request with partners',
455 method => 'generic_confirm',
456 next_actions => [ 'COMP', 'CHK' ],
457 ui_method_icon => 'fa-send-o',
460 prev_actions => [ 'REQ' ],
462 name => 'Request reverted',
463 ui_method_name => 'Revert Request',
465 next_actions => [ 'REQ', 'GENREQ', 'KILL' ],
466 ui_method_icon => 'fa-times',
471 name => 'Queued request',
474 next_actions => [ 'REQ', 'KILL' ],
478 prev_actions => [ 'NEW' ],
480 name => 'Cancellation requested',
483 next_actions => [ 'KILL', 'REQ' ],
487 prev_actions => [ 'REQ' ],
490 ui_method_name => 'Mark completed',
491 method => 'mark_completed',
492 next_actions => [ 'CHK' ],
493 ui_method_icon => 'fa-check',
496 prev_actions => [ 'QUEUED', 'REQREV', 'NEW', 'CANCREQ' ],
499 ui_method_name => 'Delete request',
502 ui_method_icon => 'fa-trash',
505 prev_actions => [ 'REQ', 'GENREQ', 'COMP' ],
507 name => 'Checked out',
508 ui_method_name => 'Check out',
509 needs_prefs => [ 'CirculateILL' ],
510 needs_perms => [ 'user_circulate_circulate_remaining_permissions' ],
511 # An array of functions that all must return true
512 needs_all => [ sub { my $r = shift; return $r->biblio; } ],
513 method => 'check_out',
515 ui_method_icon => 'fa-upload',
518 prev_actions => [ 'CHK' ],
520 name => 'Returned to library',
521 ui_method_name => 'Check in',
522 method => 'check_in',
523 next_actions => [ 'COMP' ],
524 ui_method_icon => 'fa-download',
529 =head3 _status_graph_union
531 my $status_graph = $illrequest->_status_graph_union($origin, $new_graph);
533 Return a new status_graph, the result of merging $origin & new_graph. This is
534 operation is a union over the sets defied by the two graphs.
536 Each entry in $new_graph is added to $origin. We do not provide a syntax for
537 'subtraction' of entries from $origin.
539 Whilst it is not intended that this works, you can override entries in $origin
540 with entries with the same key in $new_graph. This can lead to problematic
541 behaviour when $new_graph adds an entry, which modifies a dependent entry in
542 $origin, only for the entry in $origin to be replaced later with a new entry
545 NOTE: this procedure does not "re-link" entries in $origin or $new_graph,
546 i.e. each of the graphs need to be correct at the outset of the operation.
550 sub _status_graph_union {
551 my ( $self, $core_status_graph, $backend_status_graph ) = @_;
552 # Create new status graph with:
553 # - all core_status_graph
554 # - for-each each backend_status_graph
555 # + add to new status graph
556 # + for each core prev_action:
557 # * locate core_status
558 # * update next_actions with additional next action.
559 # + for each core next_action:
560 # * locate core_status
561 # * update prev_actions with additional prev action
563 my @core_status_ids = keys %{$core_status_graph};
564 my $status_graph = clone($core_status_graph);
566 foreach my $backend_status_key ( keys %{$backend_status_graph} ) {
567 my $backend_status = $backend_status_graph->{$backend_status_key};
568 # Add to new status graph
569 $status_graph->{$backend_status_key} = $backend_status;
570 # Update all core methods' next_actions.
571 foreach my $prev_action ( @{$backend_status->{prev_actions}} ) {
572 if ( grep { $prev_action eq $_ } @core_status_ids ) {
574 @{$status_graph->{$prev_action}->{next_actions}};
575 push @next_actions, $backend_status_key
576 if (!grep(/^$backend_status_key$/, @next_actions));
577 $status_graph->{$prev_action}->{next_actions}
581 # Update all core methods' prev_actions
582 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
583 if ( grep { $next_action eq $_ } @core_status_ids ) {
585 @{$status_graph->{$next_action}->{prev_actions}};
586 push @prev_actions, $backend_status_key
587 if (!grep(/^$backend_status_key$/, @prev_actions));
588 $status_graph->{$next_action}->{prev_actions}
594 return $status_graph;
601 my $capabilities = $illrequest->capabilities;
603 Return a hashref mapping methods to operation names supported by the queried
606 Example return value:
608 { create => "Create Request", confirm => "Progress Request" }
610 NOTE: this module suffers from a confusion in termninology:
612 in _backend_capability, the notion of capability refers to an optional feature
613 that is implemented in core, but might not be supported by a given backend.
615 in capabilities & custom_capability, capability refers to entries in the
616 status_graph (after union between backend and core).
618 The easiest way to fix this would be to fix the terminology in
619 capabilities & custom_capability and their callers.
624 my ( $self, $status ) = @_;
625 # Generate up to date status_graph
626 my $status_graph = $self->_status_graph_union(
627 $self->_core_status_graph,
628 $self->_backend->status_graph({
633 # Extract available actions from graph.
634 return $status_graph->{$status} if $status;
635 # Or return entire graph.
636 return $status_graph;
639 =head3 custom_capability
641 Return the result of invoking $CANDIDATE on this request's backend with
642 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
644 NOTE: this module suffers from a confusion in termninology:
646 in _backend_capability, the notion of capability refers to an optional feature
647 that is implemented in core, but might not be supported by a given backend.
649 in capabilities & custom_capability, capability refers to entries in the
650 status_graph (after union between backend and core).
652 The easiest way to fix this would be to fix the terminology in
653 capabilities & custom_capability and their callers.
657 sub custom_capability {
658 my ( $self, $candidate, $params ) = @_;
659 foreach my $capability ( values %{$self->capabilities} ) {
660 if ( $candidate eq $capability->{method} ) {
662 $self->_backend->$candidate({
666 return $self->expandTemplate($response);
672 =head3 available_backends
674 Return a list of available backends.
678 sub available_backends {
679 my ( $self, $reduced ) = @_;
680 my $backends = $self->_config->available_backends($reduced);
684 =head3 available_actions
686 Return a list of available actions.
690 sub available_actions {
692 my $current_action = $self->capabilities($self->status);
693 my @available_actions = map { $self->capabilities($_) }
694 @{$current_action->{next_actions}};
695 return \@available_actions;
698 =head3 mark_completed
700 Mark a request as completed (status = COMP).
706 $self->status('COMP')->store;
707 $self->completed(dt_from_string())->store;
712 method => 'mark_completed',
718 =head2 backend_migrate
720 Migrate a request from one backend to another.
724 sub backend_migrate {
725 my ( $self, $params ) = @_;
727 my $response = $self->_backend_capability('migrate',{
731 return $self->expandTemplate($response) if $response;
735 =head2 backend_confirm
737 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
743 =item * accessurl, cost (if available).
749 sub backend_confirm {
750 my ( $self, $params ) = @_;
752 my $response = $self->_backend->confirm({
756 return $self->expandTemplate($response);
759 =head3 backend_update_status
763 sub backend_update_status {
764 my ( $self, $params ) = @_;
765 return $self->expandTemplate($self->_backend->update_status($params));
768 =head3 backend_cancel
770 my $ILLResponse = $illRequest->backend_cancel;
772 The standard interface method allowing for request cancellation.
777 my ( $self, $params ) = @_;
779 my $result = $self->_backend->cancel({
784 return $self->expandTemplate($result);
789 my $renew_response = $illRequest->backend_renew;
791 The standard interface method allowing for request renewal queries.
797 return $self->expandTemplate(
798 $self->_backend->renew({
804 =head3 backend_create
806 my $create_response = $abstractILL->backend_create($params);
808 Return an array of Record objects created by querying our backend with
811 In the context of the other ILL methods, this is a special method: we only
812 pass it $params, as it does not yet have any other data associated with it.
817 my ( $self, $params ) = @_;
819 # Establish whether we need to do a generic copyright clearance.
820 if ($params->{opac}) {
821 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
822 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
828 stage => 'copyrightclearance',
831 backend => $self->_backend->name
834 } elsif ( defined $params->{stage}
835 && $params->{stage} eq 'copyrightclearance' ) {
836 $params->{stage} = 'init';
839 # First perform API action, then...
844 my $result = $self->_backend->create($args);
846 # ... simple case: we're not at 'commit' stage.
847 my $stage = $result->{stage};
848 return $self->expandTemplate($result)
849 unless ( 'commit' eq $stage );
851 # ... complex case: commit!
853 # Do we still have space for an ILL or should we queue?
854 my $permitted = $self->check_limits(
855 { patron => $self->patron }, { librarycode => $self->branchcode }
858 # Now augment our committed request.
860 $result->{permitted} = $permitted; # Queue request?
864 # ...Updating status!
865 $self->status('QUEUED')->store unless ( $permitted );
867 ## Handle Unmediated ILLs
869 # For the unmediated workflow we only need to delegate to our backend. If
870 # that backend supports unmediateld_ill, it will do its thing and return a
871 # proper response. If it doesn't then _backend_capability returns 0, so
872 # we keep the current result.
873 if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
874 my $unmediated_result = $self->_backend_capability(
878 $result = $unmediated_result if $unmediated_result;
881 return $self->expandTemplate($result);
884 =head3 expandTemplate
886 my $params = $abstract->expandTemplate($params);
888 Return a version of $PARAMS augmented with our required template path.
893 my ( $self, $params ) = @_;
894 my $backend = $self->_backend->name;
895 # Generate path to file to load
896 my $backend_dir = $self->_config->backend_dir;
897 my $backend_tmpl = join "/", $backend_dir, $backend;
898 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
899 ( $params->{method}//q{} ) . ".inc";
900 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
901 ( $params->{method}//q{} ) . ".inc";
903 $params->{template} = $intra_tmpl;
904 $params->{opac_template} = $opac_tmpl;
908 #### Abstract Imports
912 my $limit_rules = $abstract->getLimits( {
913 type => 'brw_cat' | 'branch',
917 Return the ILL limit rules for the supplied combination of type / value.
919 As the config may have no rules for this particular type / value combination,
920 or for the default, we must define fall-back values here.
925 my ( $self, $params ) = @_;
926 my $limits = $self->_config->getLimitRules($params->{type});
928 if ( defined $params->{value}
929 && defined $limits->{$params->{value}} ) {
930 return $limits->{$params->{value}};
933 return $limits->{default} || { count => -1, method => 'active' };
939 my $prefix = $abstract->getPrefix( {
940 branch => $branch_code
943 Return the ILL prefix as defined by our $params: either per borrower category,
944 per branch or the default.
949 my ( $self, $params ) = @_;
950 my $brn_prefixes = $self->_config->getPrefixes();
951 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
956 my $type = $abstract->get_type();
958 Return a string representing the material type of this request or undef
964 my $attr = $self->illrequestattributes->find({ type => 'type'});
969 #### Illrequests Imports
973 my $ok = $illRequests->check_limits( {
974 borrower => $borrower,
975 branchcode => 'branchcode' | undef,
978 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
979 see whether we are still able to place ILLs.
981 LimitRules are derived from koha-conf.xml:
982 + default limit counts, and counting method
983 + branch specific limit counts & counting method
984 + borrower category specific limit counts & counting method
985 + err on the side of caution: a counting fail will cause fail, even if
986 the other counts passes.
991 my ( $self, $params ) = @_;
992 my $patron = $params->{patron};
993 my $branchcode = $params->{librarycode} || $patron->branchcode;
995 # Establish maximum number of allowed requests
996 my ( $branch_rules, $brw_rules ) = (
1003 value => $patron->categorycode,
1006 my ( $branch_limit, $brw_limit )
1007 = ( $branch_rules->{count}, $brw_rules->{count} );
1008 # Establish currently existing requests
1009 my ( $branch_count, $brw_count ) = (
1010 $self->_limit_counter(
1011 $branch_rules->{method}, { branchcode => $branchcode }
1013 $self->_limit_counter(
1014 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
1018 # Compare and return
1019 # A limit of -1 means no limit exists.
1020 # We return blocked if either branch limit or brw limit is reached.
1021 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
1022 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
1029 sub _limit_counter {
1030 my ( $self, $method, $target ) = @_;
1032 # Establish parameters of counts
1034 if ($method && $method eq 'annual') {
1035 $resultset = Koha::Illrequests->search({
1038 \"YEAR(placed) = YEAR(NOW())"
1041 } else { # assume 'active'
1042 # XXX: This status list is ugly. There should be a method in config
1044 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1045 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1049 return $resultset->count;
1052 =head3 requires_moderation
1054 my $status = $illRequest->requires_moderation;
1056 Return the name of the status if moderation by staff is required; or 0
1061 sub requires_moderation {
1063 my $require_moderation = {
1064 'CANCREQ' => 'CANCREQ',
1066 return $require_moderation->{$self->status};
1071 my $biblio = $request->biblio;
1073 For a given request, return the biblio associated with it,
1074 or undef if none exists
1081 return if !$self->biblio_id;
1083 return Koha::Biblios->find({
1084 biblionumber => $self->biblio_id
1090 my $stage_summary = $request->check_out;
1092 Handle the check_out method. The first stage involves gathering the required
1093 data from the user via a form, the second stage creates an item and tries to
1094 issue it to the patron. If successful, it notifies the patron, then it
1095 returns a summary of how things went
1100 my ( $self, $params ) = @_;
1102 # Objects required by the template
1103 my $itemtypes = Koha::ItemTypes->search(
1105 { order_by => ['description'] }
1107 my $libraries = Koha::Libraries->search(
1109 { order_by => ['branchcode'] }
1111 my $biblio = $self->biblio;
1113 # Find all statistical patrons
1114 my $statistical_patrons = Koha::Patrons->search(
1115 { 'category_type' => 'x' },
1116 { join => { 'categorycode' => 'borrowers' } }
1119 if (!$params->{stage} || $params->{stage} eq 'init') {
1120 # Present a form to gather the required data
1122 # We may be viewing this page having previously tried to issue
1123 # the item (in which case, we may already have created an item)
1124 # so we pass the biblio for this request
1126 method => 'check_out',
1129 itemtypes => $itemtypes,
1130 libraries => $libraries,
1131 statistical => $statistical_patrons,
1135 } elsif ($params->{stage} eq 'form') {
1136 # Validate what we've got and return with an error if we fail
1138 if (!$params->{item_type} || length $params->{item_type} == 0) {
1139 $errors->{item_type} = 1;
1141 if ($params->{inhouse} && length $params->{inhouse} > 0) {
1142 my $patron_count = Koha::Patrons->search({
1143 cardnumber => $params->{inhouse}
1145 if ($patron_count != 1) {
1146 $errors->{inhouse} = 1;
1150 # Check we don't have more than one item for this bib,
1151 # if we do, something very odd is going on
1152 # Having 1 is OK, it means we're likely trying to issue
1153 # following a previously failed attempt, the item exists
1155 my @items = $biblio->items->as_list;
1156 my $item_count = scalar @items;
1157 if ($item_count > 1) {
1158 $errors->{itemcount} = 1;
1161 # Failed validation, go back to the form
1164 method => 'check_out',
1168 statistical => $statistical_patrons,
1169 itemtypes => $itemtypes,
1170 libraries => $libraries,
1179 # Create an item if one doesn't already exist,
1180 # if one does, use that
1182 if ($item_count == 0) {
1184 biblionumber => $self->biblio_id,
1185 homebranch => $params->{branchcode},
1186 holdingbranch => $params->{branchcode},
1187 location => $params->{branchcode},
1188 itype => $params->{item_type},
1189 barcode => 'ILL-' . $self->illrequest_id
1192 my $item = Koha::Item->new($item_hash)->store;
1193 $itemnumber = $item->itemnumber;
1196 $itemnumber = $items[0]->itemnumber;
1198 # Check we have an item before going forward
1201 method => 'check_out',
1205 itemtypes => $itemtypes,
1206 libraries => $libraries,
1207 statistical => $statistical_patrons,
1208 errors => { item_creation => 1 }
1215 # Gather what we need
1216 my $target_item = Koha::Items->find( $itemnumber );
1217 # Determine who we're issuing to
1218 my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
1219 Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
1224 scalar $target_item->barcode
1226 if ($params->{duedate} && length $params->{duedate} > 0) {
1227 push @issue_args, $params->{duedate};
1229 # Check if we can check out
1230 my ( $error, $confirm, $alerts, $messages ) =
1231 C4::Circulation::CanBookBeIssued(@issue_args);
1233 # If we got anything back saying we can't check out,
1234 # return it to the template
1236 if ( $error && %{$error} ) { $problems->{error} = $error };
1237 if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
1238 if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
1239 if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
1243 method => 'check_out',
1247 itemtypes => $itemtypes,
1248 libraries => $libraries,
1249 statistical => $statistical_patrons,
1252 check_out_errors => $problems
1257 # We can allegedly check out, so make it so
1258 # For some reason, AddIssue requires an unblessed Patron
1259 $issue_args[0] = $patron->unblessed;
1260 my $issue = C4::Circulation::AddIssue(@issue_args);
1263 # Update the request status
1264 $self->status('CHK')->store;
1266 method => 'check_out',
1267 stage => 'done_check_out',
1276 method => 'check_out',
1280 itemtypes => $itemtypes,
1281 libraries => $libraries,
1282 errors => { item_check_out => 1 }
1290 =head3 generic_confirm
1292 my $stage_summary = $illRequest->generic_confirm;
1294 Handle the generic_confirm extended method. The first stage involves creating
1295 a template email for the end user to edit in the browser. The second stage
1296 attempts to submit the email.
1300 sub generic_confirm {
1301 my ( $self, $params ) = @_;
1302 my $branch = Koha::Libraries->find($params->{current_branchcode})
1303 || die "Invalid current branchcode. Are you logged in as the database user?";
1304 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
1305 # Get the message body from the notice definition
1306 my $letter = $self->get_notice({
1307 notice_code => 'ILL_PARTNER_REQ',
1308 transport => 'email'
1311 my $partners = Koha::Patrons->search({
1312 categorycode => $self->_config->partner_code
1318 method => 'generic_confirm',
1322 subject => $letter->{title},
1323 body => $letter->{content}
1325 partners => $partners,
1329 } elsif ( 'draft' eq $params->{stage} ) {
1330 # Create the to header
1331 my $to = $params->{partners};
1332 if ( defined $to ) {
1333 $to =~ s/^\x00//; # Strip leading NULLs
1334 $to =~ s/\x00/; /; # Replace others with '; '
1336 Koha::Exceptions::Ill::NoTargetEmail->throw(
1337 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
1339 # Create the from, replyto and sender headers
1340 my $from = $branch->from_email_address;
1341 my $replyto = $branch->inbound_ill_address;
1342 Koha::Exceptions::Ill::NoLibraryEmail->throw(
1343 "Your library has no usable email address. Please set it.")
1346 # So we get a notice hashref, then substitute the possibly
1347 # modified title and body from the draft stage
1348 my $letter = $self->get_notice({
1349 notice_code => 'ILL_PARTNER_REQ',
1350 transport => 'email'
1352 $letter->{title} = $params->{subject};
1353 $letter->{content} = $params->{body};
1358 borrowernumber => $self->borrowernumber,
1359 message_transport_type => 'email',
1361 from_address => $from,
1362 reply_address => $replyto
1366 my $result = C4::Letters::EnqueueLetter($params);
1368 $self->status("GENREQ")->store;
1369 $self->_backend_capability(
1370 'set_requested_partners',
1380 method => 'generic_confirm',
1388 status => 'email_failed',
1389 message => 'Email queueing failed',
1390 method => 'generic_confirm',
1394 die "Unknown stage, should not have happened."
1398 =head3 send_patron_notice
1400 my $result = $request->send_patron_notice($notice_code);
1402 Send a specified notice regarding this request to a patron
1406 sub send_patron_notice {
1407 my ( $self, $notice_code ) = @_;
1409 # We need a notice code
1410 if (!$notice_code) {
1412 error => 'notice_no_type'
1416 # Map from the notice code to the messaging preference
1417 my %message_name = (
1418 ILL_PICKUP_READY => 'Ill_ready',
1419 ILL_REQUEST_UNAVAIL => 'Ill_unavailable'
1422 # Get the patron's messaging preferences
1423 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
1424 borrowernumber => $self->borrowernumber,
1425 message_name => $message_name{$notice_code}
1427 my @transports = keys %{ $borrower_preferences->{transports} };
1429 # Notice should come from the library where the request was placed,
1430 # not the patrons home library
1431 my $branch = Koha::Libraries->find($self->branchcode);
1432 my $from_address = $branch->from_email_address;
1433 my $reply_address = $branch->inbound_ill_address;
1435 # Send the notice to the patron via the chosen transport methods
1436 # and record the results
1439 for my $transport (@transports) {
1440 my $letter = $self->get_notice({
1441 notice_code => $notice_code,
1442 transport => $transport
1445 my $result = C4::Letters::EnqueueLetter({
1447 borrowernumber => $self->borrowernumber,
1448 message_transport_type => $transport,
1449 from_address => $from_address,
1450 reply_address => $reply_address
1453 push @success, $transport;
1455 push @fail, $transport;
1458 push @fail, $transport;
1461 if (scalar @success > 0) {
1462 my $logger = Koha::Illrequest::Logger->new;
1463 $logger->log_patron_notice({
1465 notice_code => $notice_code
1470 success => \@success,
1476 =head3 send_staff_notice
1478 my $result = $request->send_staff_notice($notice_code);
1480 Send a specified notice regarding this request to staff
1484 sub send_staff_notice {
1485 my ( $self, $notice_code ) = @_;
1487 # We need a notice code
1488 if (!$notice_code) {
1490 error => 'notice_no_type'
1494 # Get the staff notices that have been assigned for sending in
1496 my $staff_to_send = C4::Context->preference('ILLSendStaffNotices') // q{};
1498 # If it hasn't been enabled in the syspref, we don't want to send it
1499 if ($staff_to_send !~ /\b$notice_code\b/) {
1501 error => 'notice_not_enabled'
1505 my $letter = $self->get_notice({
1506 notice_code => $notice_code,
1507 transport => 'email'
1510 # Try and get an address to which to send staff notices
1511 my $branch = Koha::Libraries->find($self->branchcode);
1512 my $to_address = $branch->inbound_ill_address;
1513 my $from_address = $branch->inbound_ill_address;
1517 borrowernumber => $self->borrowernumber,
1518 message_transport_type => 'email',
1519 from_address => $from_address
1523 $params->{to_address} = $to_address;
1526 error => 'notice_no_create'
1531 C4::Letters::EnqueueLetter($params)
1532 or warn "can't enqueue letter $letter";
1534 success => 'notice_queued'
1538 error => 'notice_no_create'
1545 my $notice = $request->get_notice($params);
1547 Return a compiled notice hashref for the passed notice code
1553 my ( $self, $params ) = @_;
1555 my $title = $self->illrequestattributes->find(
1558 my $author = $self->illrequestattributes->find(
1559 { type => 'author' }
1561 my $metahash = $self->metadata;
1563 while (my($key, $value) = each %{$metahash}) {
1564 push @metaarray, "- $key: $value" if $value;
1566 my $metastring = join("\n", @metaarray);
1567 my $letter = C4::Letters::GetPreparedLetter(
1569 letter_code => $params->{notice_code},
1570 branchcode => $self->branchcode,
1571 message_transport_type => $params->{transport},
1572 lang => $self->patron->lang,
1574 illrequests => $self->illrequest_id,
1575 borrowers => $self->borrowernumber,
1576 biblio => $self->biblio_id,
1577 branches => $self->branchcode,
1580 ill_bib_title => $title ? $title->value : '',
1581 ill_bib_author => $author ? $author->value : '',
1582 ill_full_metadata => $metastring
1591 my $prefix = $record->id_prefix;
1593 Return the prefix appropriate for the current Illrequest as derived from the
1594 borrower and branch associated with this request's Status, and the config
1601 my $prefix = $self->getPrefix( {
1602 branch => $self->branchcode,
1604 $prefix .= "-" if ( $prefix );
1610 my $params = $illRequest->_censor($params);
1612 Return $params, modified to reflect our censorship requirements.
1617 my ( $self, $params ) = @_;
1618 my $censorship = $self->_config->censorship;
1619 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1620 if ( $params->{opac} );
1621 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1630 Overloaded I<store> method that, in addition to performing the 'store',
1631 possibly records the fact that something happened
1636 my ( $self, $attrs ) = @_;
1638 my $ret = $self->SUPER::store;
1640 $attrs->{log_origin} = 'core';
1642 if ($ret && defined $attrs) {
1643 my $logger = Koha::Illrequest::Logger->new;
1644 $logger->log_maybe({
1653 =head3 requested_partners
1655 my $partners_string = $illRequest->requested_partners;
1657 Return the string representing the email addresses of the partners to
1658 whom a request has been sent
1662 sub requested_partners {
1664 return $self->_backend_capability(
1665 'get_requested_partners',
1666 { request => $self }
1672 $json = $illrequest->TO_JSON
1674 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1675 into the unblessed representation of the object.
1677 TODO: This method does nothing and is not called anywhere. However, bug 74325
1678 touches it, so keeping this for now until both this and bug 74325 are merged,
1679 at which point we can sort it out and remove it completely
1684 my ( $self, $embed ) = @_;
1686 my $object = $self->SUPER::TO_JSON();
1691 =head2 Internal methods
1698 return 'Illrequest';
1703 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1704 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>