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 $status_graph->{$prev_action}->{next_actions}
580 # Update all core methods' prev_actions
581 foreach my $next_action ( @{$backend_status->{next_actions}} ) {
582 if ( grep { $next_action eq $_ } @core_status_ids ) {
584 @{$status_graph->{$next_action}->{prev_actions}};
585 push @prev_actions, $backend_status_key;
586 $status_graph->{$next_action}->{prev_actions}
592 return $status_graph;
599 my $capabilities = $illrequest->capabilities;
601 Return a hashref mapping methods to operation names supported by the queried
604 Example return value:
606 { create => "Create Request", confirm => "Progress Request" }
608 NOTE: this module suffers from a confusion in termninology:
610 in _backend_capability, the notion of capability refers to an optional feature
611 that is implemented in core, but might not be supported by a given backend.
613 in capabilities & custom_capability, capability refers to entries in the
614 status_graph (after union between backend and core).
616 The easiest way to fix this would be to fix the terminology in
617 capabilities & custom_capability and their callers.
622 my ( $self, $status ) = @_;
623 # Generate up to date status_graph
624 my $status_graph = $self->_status_graph_union(
625 $self->_core_status_graph,
626 $self->_backend->status_graph({
631 # Extract available actions from graph.
632 return $status_graph->{$status} if $status;
633 # Or return entire graph.
634 return $status_graph;
637 =head3 custom_capability
639 Return the result of invoking $CANDIDATE on this request's backend with
640 $PARAMS, or 0 if $CANDIDATE is an unknown method on backend.
642 NOTE: this module suffers from a confusion in termninology:
644 in _backend_capability, the notion of capability refers to an optional feature
645 that is implemented in core, but might not be supported by a given backend.
647 in capabilities & custom_capability, capability refers to entries in the
648 status_graph (after union between backend and core).
650 The easiest way to fix this would be to fix the terminology in
651 capabilities & custom_capability and their callers.
655 sub custom_capability {
656 my ( $self, $candidate, $params ) = @_;
657 foreach my $capability ( values %{$self->capabilities} ) {
658 if ( $candidate eq $capability->{method} ) {
660 $self->_backend->$candidate({
664 return $self->expandTemplate($response);
670 =head3 available_backends
672 Return a list of available backends.
676 sub available_backends {
677 my ( $self, $reduced ) = @_;
678 my $backends = $self->_config->available_backends($reduced);
682 =head3 available_actions
684 Return a list of available actions.
688 sub available_actions {
690 my $current_action = $self->capabilities($self->status);
691 my @available_actions = map { $self->capabilities($_) }
692 @{$current_action->{next_actions}};
693 return \@available_actions;
696 =head3 mark_completed
698 Mark a request as completed (status = COMP).
704 $self->status('COMP')->store;
705 $self->completed(dt_from_string())->store;
710 method => 'mark_completed',
716 =head2 backend_migrate
718 Migrate a request from one backend to another.
722 sub backend_migrate {
723 my ( $self, $params ) = @_;
725 my $response = $self->_backend_capability('migrate',{
729 return $self->expandTemplate($response) if $response;
733 =head2 backend_confirm
735 Confirm a request. The backend handles setting of mandatory fields in the commit stage:
741 =item * accessurl, cost (if available).
747 sub backend_confirm {
748 my ( $self, $params ) = @_;
750 my $response = $self->_backend->confirm({
754 return $self->expandTemplate($response);
757 =head3 backend_update_status
761 sub backend_update_status {
762 my ( $self, $params ) = @_;
763 return $self->expandTemplate($self->_backend->update_status($params));
766 =head3 backend_cancel
768 my $ILLResponse = $illRequest->backend_cancel;
770 The standard interface method allowing for request cancellation.
775 my ( $self, $params ) = @_;
777 my $result = $self->_backend->cancel({
782 return $self->expandTemplate($result);
787 my $renew_response = $illRequest->backend_renew;
789 The standard interface method allowing for request renewal queries.
795 return $self->expandTemplate(
796 $self->_backend->renew({
802 =head3 backend_create
804 my $create_response = $abstractILL->backend_create($params);
806 Return an array of Record objects created by querying our backend with
809 In the context of the other ILL methods, this is a special method: we only
810 pass it $params, as it does not yet have any other data associated with it.
815 my ( $self, $params ) = @_;
817 # Establish whether we need to do a generic copyright clearance.
818 if ($params->{opac}) {
819 if ( ( !$params->{stage} || $params->{stage} eq 'init' )
820 && C4::Context->preference("ILLModuleCopyrightClearance") ) {
826 stage => 'copyrightclearance',
829 backend => $self->_backend->name
832 } elsif ( defined $params->{stage}
833 && $params->{stage} eq 'copyrightclearance' ) {
834 $params->{stage} = 'init';
837 # First perform API action, then...
842 my $result = $self->_backend->create($args);
844 # ... simple case: we're not at 'commit' stage.
845 my $stage = $result->{stage};
846 return $self->expandTemplate($result)
847 unless ( 'commit' eq $stage );
849 # ... complex case: commit!
851 # Do we still have space for an ILL or should we queue?
852 my $permitted = $self->check_limits(
853 { patron => $self->patron }, { librarycode => $self->branchcode }
856 # Now augment our committed request.
858 $result->{permitted} = $permitted; # Queue request?
862 # ...Updating status!
863 $self->status('QUEUED')->store unless ( $permitted );
865 ## Handle Unmediated ILLs
867 # For the unmediated workflow we only need to delegate to our backend. If
868 # that backend supports unmediateld_ill, it will do its thing and return a
869 # proper response. If it doesn't then _backend_capability returns 0, so
870 # we keep the current result.
871 if ( C4::Context->preference("ILLModuleUnmediated") && $permitted ) {
872 my $unmediated_result = $self->_backend_capability(
876 $result = $unmediated_result if $unmediated_result;
879 return $self->expandTemplate($result);
882 =head3 expandTemplate
884 my $params = $abstract->expandTemplate($params);
886 Return a version of $PARAMS augmented with our required template path.
891 my ( $self, $params ) = @_;
892 my $backend = $self->_backend->name;
893 # Generate path to file to load
894 my $backend_dir = $self->_config->backend_dir;
895 my $backend_tmpl = join "/", $backend_dir, $backend;
896 my $intra_tmpl = join "/", $backend_tmpl, "intra-includes",
897 ( $params->{method}//q{} ) . ".inc";
898 my $opac_tmpl = join "/", $backend_tmpl, "opac-includes",
899 ( $params->{method}//q{} ) . ".inc";
901 $params->{template} = $intra_tmpl;
902 $params->{opac_template} = $opac_tmpl;
906 #### Abstract Imports
910 my $limit_rules = $abstract->getLimits( {
911 type => 'brw_cat' | 'branch',
915 Return the ILL limit rules for the supplied combination of type / value.
917 As the config may have no rules for this particular type / value combination,
918 or for the default, we must define fall-back values here.
923 my ( $self, $params ) = @_;
924 my $limits = $self->_config->getLimitRules($params->{type});
926 if ( defined $params->{value}
927 && defined $limits->{$params->{value}} ) {
928 return $limits->{$params->{value}};
931 return $limits->{default} || { count => -1, method => 'active' };
937 my $prefix = $abstract->getPrefix( {
938 branch => $branch_code
941 Return the ILL prefix as defined by our $params: either per borrower category,
942 per branch or the default.
947 my ( $self, $params ) = @_;
948 my $brn_prefixes = $self->_config->getPrefixes();
949 return $brn_prefixes->{$params->{branch}} || ""; # "the empty prefix"
954 my $type = $abstract->get_type();
956 Return a string representing the material type of this request or undef
962 my $attr = $self->illrequestattributes->find({ type => 'type'});
967 #### Illrequests Imports
971 my $ok = $illRequests->check_limits( {
972 borrower => $borrower,
973 branchcode => 'branchcode' | undef,
976 Given $PARAMS, a hashref containing a $borrower object and a $branchcode,
977 see whether we are still able to place ILLs.
979 LimitRules are derived from koha-conf.xml:
980 + default limit counts, and counting method
981 + branch specific limit counts & counting method
982 + borrower category specific limit counts & counting method
983 + err on the side of caution: a counting fail will cause fail, even if
984 the other counts passes.
989 my ( $self, $params ) = @_;
990 my $patron = $params->{patron};
991 my $branchcode = $params->{librarycode} || $patron->branchcode;
993 # Establish maximum number of allowed requests
994 my ( $branch_rules, $brw_rules ) = (
1001 value => $patron->categorycode,
1004 my ( $branch_limit, $brw_limit )
1005 = ( $branch_rules->{count}, $brw_rules->{count} );
1006 # Establish currently existing requests
1007 my ( $branch_count, $brw_count ) = (
1008 $self->_limit_counter(
1009 $branch_rules->{method}, { branchcode => $branchcode }
1011 $self->_limit_counter(
1012 $brw_rules->{method}, { borrowernumber => $patron->borrowernumber }
1016 # Compare and return
1017 # A limit of -1 means no limit exists.
1018 # We return blocked if either branch limit or brw limit is reached.
1019 if ( ( $branch_limit != -1 && $branch_limit <= $branch_count )
1020 || ( $brw_limit != -1 && $brw_limit <= $brw_count ) ) {
1027 sub _limit_counter {
1028 my ( $self, $method, $target ) = @_;
1030 # Establish parameters of counts
1032 if ($method && $method eq 'annual') {
1033 $resultset = Koha::Illrequests->search({
1036 \"YEAR(placed) = YEAR(NOW())"
1039 } else { # assume 'active'
1040 # XXX: This status list is ugly. There should be a method in config
1042 my $where = { status => { -not_in => [ 'QUEUED', 'COMP' ] } };
1043 $resultset = Koha::Illrequests->search({ %{$target}, %{$where} });
1047 return $resultset->count;
1050 =head3 requires_moderation
1052 my $status = $illRequest->requires_moderation;
1054 Return the name of the status if moderation by staff is required; or 0
1059 sub requires_moderation {
1061 my $require_moderation = {
1062 'CANCREQ' => 'CANCREQ',
1064 return $require_moderation->{$self->status};
1069 my $biblio = $request->biblio;
1071 For a given request, return the biblio associated with it,
1072 or undef if none exists
1079 return if !$self->biblio_id;
1081 return Koha::Biblios->find({
1082 biblionumber => $self->biblio_id
1088 my $stage_summary = $request->check_out;
1090 Handle the check_out method. The first stage involves gathering the required
1091 data from the user via a form, the second stage creates an item and tries to
1092 issue it to the patron. If successful, it notifies the patron, then it
1093 returns a summary of how things went
1098 my ( $self, $params ) = @_;
1100 # Objects required by the template
1101 my $itemtypes = Koha::ItemTypes->search(
1103 { order_by => ['description'] }
1105 my $libraries = Koha::Libraries->search(
1107 { order_by => ['branchcode'] }
1109 my $biblio = $self->biblio;
1111 # Find all statistical patrons
1112 my $statistical_patrons = Koha::Patrons->search(
1113 { 'category_type' => 'x' },
1114 { join => { 'categorycode' => 'borrowers' } }
1117 if (!$params->{stage} || $params->{stage} eq 'init') {
1118 # Present a form to gather the required data
1120 # We may be viewing this page having previously tried to issue
1121 # the item (in which case, we may already have created an item)
1122 # so we pass the biblio for this request
1124 method => 'check_out',
1127 itemtypes => $itemtypes,
1128 libraries => $libraries,
1129 statistical => $statistical_patrons,
1133 } elsif ($params->{stage} eq 'form') {
1134 # Validate what we've got and return with an error if we fail
1136 if (!$params->{item_type} || length $params->{item_type} == 0) {
1137 $errors->{item_type} = 1;
1139 if ($params->{inhouse} && length $params->{inhouse} > 0) {
1140 my $patron_count = Koha::Patrons->search({
1141 cardnumber => $params->{inhouse}
1143 if ($patron_count != 1) {
1144 $errors->{inhouse} = 1;
1148 # Check we don't have more than one item for this bib,
1149 # if we do, something very odd is going on
1150 # Having 1 is OK, it means we're likely trying to issue
1151 # following a previously failed attempt, the item exists
1153 my @items = $biblio->items->as_list;
1154 my $item_count = scalar @items;
1155 if ($item_count > 1) {
1156 $errors->{itemcount} = 1;
1159 # Failed validation, go back to the form
1162 method => 'check_out',
1166 statistical => $statistical_patrons,
1167 itemtypes => $itemtypes,
1168 libraries => $libraries,
1177 # Create an item if one doesn't already exist,
1178 # if one does, use that
1180 if ($item_count == 0) {
1182 biblionumber => $self->biblio_id,
1183 homebranch => $params->{branchcode},
1184 holdingbranch => $params->{branchcode},
1185 location => $params->{branchcode},
1186 itype => $params->{item_type},
1187 barcode => 'ILL-' . $self->illrequest_id
1190 my $item = Koha::Item->new($item_hash)->store;
1191 $itemnumber = $item->itemnumber;
1194 $itemnumber = $items[0]->itemnumber;
1196 # Check we have an item before going forward
1199 method => 'check_out',
1203 itemtypes => $itemtypes,
1204 libraries => $libraries,
1205 statistical => $statistical_patrons,
1206 errors => { item_creation => 1 }
1213 # Gather what we need
1214 my $target_item = Koha::Items->find( $itemnumber );
1215 # Determine who we're issuing to
1216 my $patron = $params->{inhouse} && length $params->{inhouse} > 0 ?
1217 Koha::Patrons->find({ cardnumber => $params->{inhouse} }) :
1222 scalar $target_item->barcode
1224 if ($params->{duedate} && length $params->{duedate} > 0) {
1225 push @issue_args, $params->{duedate};
1227 # Check if we can check out
1228 my ( $error, $confirm, $alerts, $messages ) =
1229 C4::Circulation::CanBookBeIssued(@issue_args);
1231 # If we got anything back saying we can't check out,
1232 # return it to the template
1234 if ( $error && %{$error} ) { $problems->{error} = $error };
1235 if ( $confirm && %{$confirm} ) { $problems->{confirm} = $confirm };
1236 if ( $alerts && %{$alerts} ) { $problems->{alerts} = $alerts };
1237 if ( $messages && %{$messages} ) { $problems->{messages} = $messages };
1241 method => 'check_out',
1245 itemtypes => $itemtypes,
1246 libraries => $libraries,
1247 statistical => $statistical_patrons,
1250 check_out_errors => $problems
1255 # We can allegedly check out, so make it so
1256 # For some reason, AddIssue requires an unblessed Patron
1257 $issue_args[0] = $patron->unblessed;
1258 my $issue = C4::Circulation::AddIssue(@issue_args);
1261 # Update the request status
1262 $self->status('CHK')->store;
1264 method => 'check_out',
1265 stage => 'done_check_out',
1274 method => 'check_out',
1278 itemtypes => $itemtypes,
1279 libraries => $libraries,
1280 errors => { item_check_out => 1 }
1288 =head3 generic_confirm
1290 my $stage_summary = $illRequest->generic_confirm;
1292 Handle the generic_confirm extended method. The first stage involves creating
1293 a template email for the end user to edit in the browser. The second stage
1294 attempts to submit the email.
1298 sub generic_confirm {
1299 my ( $self, $params ) = @_;
1300 my $branch = Koha::Libraries->find($params->{current_branchcode})
1301 || die "Invalid current branchcode. Are you logged in as the database user?";
1302 if ( !$params->{stage}|| $params->{stage} eq 'init' ) {
1303 # Get the message body from the notice definition
1304 my $letter = $self->get_notice({
1305 notice_code => 'ILL_PARTNER_REQ',
1306 transport => 'email'
1309 my $partners = Koha::Patrons->search({
1310 categorycode => $self->_config->partner_code
1316 method => 'generic_confirm',
1320 subject => $letter->{title},
1321 body => $letter->{content}
1323 partners => $partners,
1327 } elsif ( 'draft' eq $params->{stage} ) {
1328 # Create the to header
1329 my $to = $params->{partners};
1330 if ( defined $to ) {
1331 $to =~ s/^\x00//; # Strip leading NULLs
1332 $to =~ s/\x00/; /; # Replace others with '; '
1334 Koha::Exceptions::Ill::NoTargetEmail->throw(
1335 "No target email addresses found. Either select at least one partner or check your ILL partner library records.")
1337 # Create the from, replyto and sender headers
1338 my $from = $branch->from_email_address;
1339 my $replyto = $branch->inbound_ill_address;
1340 Koha::Exceptions::Ill::NoLibraryEmail->throw(
1341 "Your library has no usable email address. Please set it.")
1344 # So we get a notice hashref, then substitute the possibly
1345 # modified title and body from the draft stage
1346 my $letter = $self->get_notice({
1347 notice_code => 'ILL_PARTNER_REQ',
1348 transport => 'email'
1350 $letter->{title} = $params->{subject};
1351 $letter->{content} = $params->{body};
1356 borrowernumber => $self->borrowernumber,
1357 message_transport_type => 'email',
1359 from_address => $from,
1360 reply_address => $replyto
1364 my $result = C4::Letters::EnqueueLetter($params);
1366 $self->status("GENREQ")->store;
1367 $self->_backend_capability(
1368 'set_requested_partners',
1378 method => 'generic_confirm',
1386 status => 'email_failed',
1387 message => 'Email queueing failed',
1388 method => 'generic_confirm',
1392 die "Unknown stage, should not have happened."
1396 =head3 send_patron_notice
1398 my $result = $request->send_patron_notice($notice_code);
1400 Send a specified notice regarding this request to a patron
1404 sub send_patron_notice {
1405 my ( $self, $notice_code ) = @_;
1407 # We need a notice code
1408 if (!$notice_code) {
1410 error => 'notice_no_type'
1414 # Map from the notice code to the messaging preference
1415 my %message_name = (
1416 ILL_PICKUP_READY => 'Ill_ready',
1417 ILL_REQUEST_UNAVAIL => 'Ill_unavailable'
1420 # Get the patron's messaging preferences
1421 my $borrower_preferences = C4::Members::Messaging::GetMessagingPreferences({
1422 borrowernumber => $self->borrowernumber,
1423 message_name => $message_name{$notice_code}
1425 my @transports = keys %{ $borrower_preferences->{transports} };
1427 # Notice should come from the library where the request was placed,
1428 # not the patrons home library
1429 my $branch = Koha::Libraries->find($self->branchcode);
1430 my $from_address = $branch->from_email_address;
1431 my $reply_address = $branch->inbound_ill_address;
1433 # Send the notice to the patron via the chosen transport methods
1434 # and record the results
1437 for my $transport (@transports) {
1438 my $letter = $self->get_notice({
1439 notice_code => $notice_code,
1440 transport => $transport
1443 my $result = C4::Letters::EnqueueLetter({
1445 borrowernumber => $self->borrowernumber,
1446 message_transport_type => $transport,
1447 from_address => $from_address,
1448 reply_address => $reply_address
1451 push @success, $transport;
1453 push @fail, $transport;
1456 push @fail, $transport;
1459 if (scalar @success > 0) {
1460 my $logger = Koha::Illrequest::Logger->new;
1461 $logger->log_patron_notice({
1463 notice_code => $notice_code
1468 success => \@success,
1474 =head3 send_staff_notice
1476 my $result = $request->send_staff_notice($notice_code);
1478 Send a specified notice regarding this request to staff
1482 sub send_staff_notice {
1483 my ( $self, $notice_code ) = @_;
1485 # We need a notice code
1486 if (!$notice_code) {
1488 error => 'notice_no_type'
1492 # Get the staff notices that have been assigned for sending in
1494 my $staff_to_send = C4::Context->preference('ILLSendStaffNotices') // q{};
1496 # If it hasn't been enabled in the syspref, we don't want to send it
1497 if ($staff_to_send !~ /\b$notice_code\b/) {
1499 error => 'notice_not_enabled'
1503 my $letter = $self->get_notice({
1504 notice_code => $notice_code,
1505 transport => 'email'
1508 # Try and get an address to which to send staff notices
1509 my $branch = Koha::Libraries->find($self->branchcode);
1510 my $to_address = $branch->inbound_ill_address;
1511 my $from_address = $branch->inbound_ill_address;
1515 borrowernumber => $self->borrowernumber,
1516 message_transport_type => 'email',
1517 from_address => $from_address
1521 $params->{to_address} = $to_address;
1524 error => 'notice_no_create'
1529 C4::Letters::EnqueueLetter($params)
1530 or warn "can't enqueue letter $letter";
1532 success => 'notice_queued'
1536 error => 'notice_no_create'
1543 my $notice = $request->get_notice($params);
1545 Return a compiled notice hashref for the passed notice code
1551 my ( $self, $params ) = @_;
1553 my $title = $self->illrequestattributes->find(
1556 my $author = $self->illrequestattributes->find(
1557 { type => 'author' }
1559 my $metahash = $self->metadata;
1561 while (my($key, $value) = each %{$metahash}) {
1562 push @metaarray, "- $key: $value" if $value;
1564 my $metastring = join("\n", @metaarray);
1565 my $letter = C4::Letters::GetPreparedLetter(
1567 letter_code => $params->{notice_code},
1568 branchcode => $self->branchcode,
1569 message_transport_type => $params->{transport},
1570 lang => $self->patron->lang,
1572 illrequests => $self->illrequest_id,
1573 borrowers => $self->borrowernumber,
1574 biblio => $self->biblio_id,
1575 branches => $self->branchcode,
1578 ill_bib_title => $title ? $title->value : '',
1579 ill_bib_author => $author ? $author->value : '',
1580 ill_full_metadata => $metastring
1589 my $prefix = $record->id_prefix;
1591 Return the prefix appropriate for the current Illrequest as derived from the
1592 borrower and branch associated with this request's Status, and the config
1599 my $prefix = $self->getPrefix( {
1600 branch => $self->branchcode,
1602 $prefix .= "-" if ( $prefix );
1608 my $params = $illRequest->_censor($params);
1610 Return $params, modified to reflect our censorship requirements.
1615 my ( $self, $params ) = @_;
1616 my $censorship = $self->_config->censorship;
1617 $params->{censor_notes_staff} = $censorship->{censor_notes_staff}
1618 if ( $params->{opac} );
1619 $params->{display_reply_date} = ( $censorship->{censor_reply_date} ) ? 0 : 1;
1628 Overloaded I<store> method that, in addition to performing the 'store',
1629 possibly records the fact that something happened
1634 my ( $self, $attrs ) = @_;
1636 my $ret = $self->SUPER::store;
1638 $attrs->{log_origin} = 'core';
1640 if ($ret && defined $attrs) {
1641 my $logger = Koha::Illrequest::Logger->new;
1642 $logger->log_maybe({
1651 =head3 requested_partners
1653 my $partners_string = $illRequest->requested_partners;
1655 Return the string representing the email addresses of the partners to
1656 whom a request has been sent
1660 sub requested_partners {
1662 return $self->_backend_capability(
1663 'get_requested_partners',
1664 { request => $self }
1670 $json = $illrequest->TO_JSON
1672 Overloaded I<TO_JSON> method that takes care of inserting calculated values
1673 into the unblessed representation of the object.
1675 TODO: This method does nothing and is not called anywhere. However, bug 74325
1676 touches it, so keeping this for now until both this and bug 74325 are merged,
1677 at which point we can sort it out and remove it completely
1682 my ( $self, $embed ) = @_;
1684 my $object = $self->SUPER::TO_JSON();
1689 =head2 Internal methods
1696 return 'Illrequest';
1701 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
1702 Andrew Isherwood <andrew.isherwood@ptfs-europe.com>