From 61e83cdf230b3ee9a8b7e9affb82995962e5a1bb Mon Sep 17 00:00:00 2001 From: Agustin Moyano Date: Thu, 19 Aug 2021 23:51:38 -0300 Subject: [PATCH] Bug 27947: Add cancellation reason to article request This bug adds a cancellation reason authorised values to article requests To test: 1. apply this patch 2. updatedatabase 3. in staff interface go to /cgi-bin/koha/admin/authorised_values.pl CHECK => AR_CANCELLATION category should appears 4. place several article requests 5. in staff interface go to /cgi-bin/koha/circ/article-requests.pl 6. select multiple requests, or just one and cancel them SUCCESS => a modal pops up offering to select a cancellation reason CHECK => message_queue table has messages with cancellation reason included 7. repeat steps 4 to 6 but for /cgi-bin/koha/circ/request-article.pl 8. cancelling article requests from opac interface should work just as before Signed-off-by: Marcel de Rooy Signed-off-by: Tomas Cohen Arazi Signed-off-by: Martin Renvoize --- Koha/ArticleRequest.pm | 18 +++- Koha/Exceptions/ArticleRequests.pm | 17 ++++ Koha/REST/V1/ArticleRequests.pm | 76 ++++++++++++++ Koha/REST/V1/Patrons.pm | 49 +++++++++ api/v1/swagger/parameters.json | 9 ++ .../swagger/parameters/article_request.json | 23 +++++ api/v1/swagger/paths.json | 8 +- api/v1/swagger/paths/article_requests.json | 70 +++++++++++++ api/v1/swagger/paths/public_patrons.json | 70 +++++++++++++ .../en/modules/admin/authorised_values.tt | 2 + .../prog/en/modules/circ/article-requests.tt | 99 ++++++++++++++----- .../prog/en/modules/circ/request-article.tt | 88 +++++++++++++---- .../bootstrap/en/modules/opac-user.tt | 45 ++++++--- 13 files changed, 516 insertions(+), 58 deletions(-) create mode 100644 Koha/Exceptions/ArticleRequests.pm create mode 100644 Koha/REST/V1/ArticleRequests.pm create mode 100644 api/v1/swagger/parameters/article_request.json create mode 100644 api/v1/swagger/paths/article_requests.json diff --git a/Koha/ArticleRequest.pm b/Koha/ArticleRequest.pm index d3554132e9..21b3751ac5 100644 --- a/Koha/ArticleRequest.pm +++ b/Koha/ArticleRequest.pm @@ -102,9 +102,10 @@ sub complete { =cut sub cancel { - my ( $self, $notes ) = @_; + my ( $self, $cancellation_reason, $notes ) = @_; $self->status(Koha::ArticleRequest::Status::Canceled); + $self->cancellation_reason($cancellation_reason) if $cancellation_reason; $self->notes($notes) if $notes; $self->store(); $self->notify(); @@ -119,6 +120,16 @@ sub notify { my ($self) = @_; my $status = $self->status; + my $reason = $self->notes; + if ( !defined $reason && $self->cancellation_reason ) { + my $av = Koha::AuthorisedValues->search( + { + category => 'AR_CANCELLATION', + authorised_value => $self->cancellation_reason + } + )->next; + $reason = $av->lib_opac ? $av->lib_opac : $av->lib if $av; + } require C4::Letters; if ( @@ -135,6 +146,9 @@ sub notify { items => $self->itemnumber, branches => $self->branchcode, }, + substitute => { + reason => $reason, + }, ) ) { @@ -144,7 +158,7 @@ sub notify { borrowernumber => $self->borrowernumber, message_transport_type => 'email', } - ) or warn "can't enqueue letter ". $letter->{code}; + ) or warn "can't enqueue letter " . $letter->{code}; } } diff --git a/Koha/Exceptions/ArticleRequests.pm b/Koha/Exceptions/ArticleRequests.pm new file mode 100644 index 0000000000..669529ca47 --- /dev/null +++ b/Koha/Exceptions/ArticleRequests.pm @@ -0,0 +1,17 @@ +package Koha::Exceptions::ArticleRequests; + +use Modern::Perl; + +use Exception::Class ( + + 'Koha::Exceptions::ArticleRequests' => { + description => 'Something went wrong!', + }, + 'Koha::Exceptions::ArticleRequests::FailedCancel' => { + isa => 'Koha::Exceptions::ArticleRequests', + description => 'Failed to cancel article request' + } + +); + +1; \ No newline at end of file diff --git a/Koha/REST/V1/ArticleRequests.pm b/Koha/REST/V1/ArticleRequests.pm new file mode 100644 index 0000000000..a026b8887c --- /dev/null +++ b/Koha/REST/V1/ArticleRequests.pm @@ -0,0 +1,76 @@ +package Koha::REST::V1::ArticleRequests; + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; + +use Mojo::Base 'Mojolicious::Controller'; + +use Koha::Database; +use Koha::ArticleRequests; + +use Scalar::Util qw( blessed ); +use Try::Tiny qw( catch try ); + +=head1 NAME + +Koha::REST::V1::ArticleRequests + +=head1 API + +=head2 Methods + +=head3 cancel + +Controller function that handles cancelling a Koha::ArticleRequest object + +=cut + +sub cancel { + my $c = shift->openapi->valid_input or return; + + my $ar = Koha::ArticleRequests->find( $c->validation->param('ar_id') ); + + unless ( $ar ) { + return $c->render( + status => 404, + openapi => { error => "Article request not found" } + ); + } + + my $reason = $c->validation->param('cancellation_reason'); + my $notes = $c->validation->param('notes'); + + return try { + + $ar->cancel($reason, $notes); + return $c->render( + status => 204, + openapi => q{} + ); + } catch { + if ( blessed $_ && $_->isa('Koha::Exceptions::ArticleRequests::FailedCancel') ) { + return $c->render( + status => 403, + openapi => { error => "Article request cannot be canceled" } + ); + } + + $c->unhandled_exception($_); + }; +} + +1; \ No newline at end of file diff --git a/Koha/REST/V1/Patrons.pm b/Koha/REST/V1/Patrons.pm index 80b64ee963..6790e9f6b8 100644 --- a/Koha/REST/V1/Patrons.pm +++ b/Koha/REST/V1/Patrons.pm @@ -419,4 +419,53 @@ sub guarantors_can_see_checkouts { }; } +=head3 cancel_article_request + +Controller function that handles cancelling a patron's Koha::ArticleRequest object + +=cut + +sub cancel_article_request { + my $c = shift->openapi->valid_input or return; + + my $patron = Koha::Patrons->find( $c->validation->param('patron_id') ); + + unless ( $patron ) { + return $c->render( + status => 404, + openapi => { error => "Patron not found" } + ); + } + + my $ar = $patron->article_requests->find( $c->validation->param('ar_id') ); + + unless ( $ar ) { + return $c->render( + status => 404, + openapi => { error => "Article request not found" } + ); + } + + my $reason = $c->validation->param('cancellation_reason'); + my $notes = $c->validation->param('notes'); + + return try { + + $ar->cancel($reason, $notes); + return $c->render( + status => 204, + openapi => q{} + ); + } catch { + if ( blessed $_ && $_->isa('Koha::Exceptions::ArticleRequests::FailedCancel') ) { + return $c->render( + status => 403, + openapi => { error => "Article request cannot be canceled" } + ); + } + + $c->unhandled_exception($_); + }; +} + 1; diff --git a/api/v1/swagger/parameters.json b/api/v1/swagger/parameters.json index 95464e1461..120b8371f1 100644 --- a/api/v1/swagger/parameters.json +++ b/api/v1/swagger/parameters.json @@ -56,6 +56,15 @@ "cashup_id_pp": { "$ref": "parameters/cashup.json#/cashup_id_pp" }, + "ar_id_pp": { + "$ref": "parameters/article_request.json#/ar_id_pp" + }, + "ar_reason_qp": { + "$ref": "parameters/article_request.json#/ar_reason_qp" + }, + "ar_notes_qp": { + "$ref": "parameters/article_request.json#/ar_notes_qp" + }, "match": { "name": "_match", "in": "query", diff --git a/api/v1/swagger/parameters/article_request.json b/api/v1/swagger/parameters/article_request.json new file mode 100644 index 0000000000..5644936af5 --- /dev/null +++ b/api/v1/swagger/parameters/article_request.json @@ -0,0 +1,23 @@ +{ + "ar_id_pp": { + "name": "ar_id", + "in": "path", + "description": "Article request identifier", + "required": true, + "type": "integer" + }, + "ar_reason_qp": { + "name": "cancellation_reason", + "in": "query", + "description": "Article request cancellation reason", + "required": false, + "type": "string" + }, + "ar_notes_qp": { + "name": "notes", + "in": "query", + "description": "Article request custom cancellation reason", + "required": false, + "type": "string" + } +} diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json index 23f420cc05..1e96c452cb 100644 --- a/api/v1/swagger/paths.json +++ b/api/v1/swagger/paths.json @@ -17,6 +17,9 @@ "/acquisitions/funds": { "$ref": "paths/acquisitions_funds.json#/~1acquisitions~1funds" }, + "/article_requests/{ar_id}": { + "$ref": "paths/article_requests.json#/~1article_requests~1{ar_id}" + }, "/biblios/{biblio_id}": { "$ref": "paths/biblios.json#/~1biblios~1{biblio_id}" }, @@ -134,7 +137,7 @@ "/patrons/{patron_id}/extended_attributes/{extended_attribute_id}": { "$ref": "paths/patrons_extended_attributes.json#/~1patrons~1{patron_id}~1extended_attributes~1{extended_attribute_id}" }, - "/patrons/{patron_id}/holds": { + "/patrons/{patron_id}/holds": { "$ref": "paths/patrons_holds.json#/~1patrons~1{patron_id}~1holds" }, "/patrons/{patron_id}/password": { @@ -170,6 +173,9 @@ "/public/patrons/{patron_id}/guarantors/can_see_checkouts": { "$ref": "paths/public_patrons.json#/~1public~1patrons~1{patron_id}~1guarantors~1can_see_checkouts" }, + "/public/patrons/{patron_id}/article_requests/{ar_id}": { + "$ref": "paths/public_patrons.json#/~1public~1patrons~1{patron_id}~1article_requests~1{ar_id}" + }, "/quotes": { "$ref": "paths/quotes.json#/~1quotes" }, diff --git a/api/v1/swagger/paths/article_requests.json b/api/v1/swagger/paths/article_requests.json new file mode 100644 index 0000000000..22ac503d63 --- /dev/null +++ b/api/v1/swagger/paths/article_requests.json @@ -0,0 +1,70 @@ +{ + "/article_requests/{ar_id}": { + "delete": { + "x-mojo-to": "ArticleRequests#cancel", + "operationId": "cancelArticleRequest", + "tags": [ + "article_requests" + ], + "summary": "Cancel article requests", + "parameters": [ + { + "$ref": "../parameters.json#/ar_id_pp" + }, + { + "$ref": "../parameters.json#/ar_reason_qp" + }, + { + "$ref": "../parameters.json#/ar_notes_qp" + } + ], + "produces": ["application/json"], + "responses": { + "204": { + "description": "Article request canceled" + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Patron not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "permissions": { + "reserveforothers": "1" + } + } + } + } +} \ No newline at end of file diff --git a/api/v1/swagger/paths/public_patrons.json b/api/v1/swagger/paths/public_patrons.json index 90d6cfe4c4..af06a0c7c7 100644 --- a/api/v1/swagger/paths/public_patrons.json +++ b/api/v1/swagger/paths/public_patrons.json @@ -242,5 +242,75 @@ "allow-owner": true } } + }, + "/public/patrons/{patron_id}/article_requests/{ar_id}": { + "delete": { + "x-mojo-to": "Patrons#cancel_article_request", + "operationId": "cancelPatronArticleRequest", + "tags": [ + "patrons", + "article_requests" + ], + "summary": "Cancel patron's article requests", + "parameters": [ + { + "$ref": "../parameters.json#/patron_id_pp" + }, + { + "$ref": "../parameters.json#/ar_id_pp" + }, + { + "$ref": "../parameters.json#/ar_reason_qp" + }, + { + "$ref": "../parameters.json#/ar_notes_qp" + } + ], + "produces": ["application/json"], + "responses": { + "204": { + "description": "Patron's article request canceled" + }, + "400": { + "description": "Bad request", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "401": { + "description": "Authentication required", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "403": { + "description": "Access forbidden", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "404": { + "description": "Patron not found", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "../definitions.json#/error" + } + }, + "503": { + "description": "Under maintenance", + "schema": { + "$ref": "../definitions.json#/error" + } + } + }, + "x-koha-authorization": { + "allow-owner": true + } + } } } diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/authorised_values.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/authorised_values.tt index b8e1f82b71..e6cb47ce5c 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/authorised_values.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/authorised_values.tt @@ -424,6 +424,8 @@ Authorized values › Administration › Koha

General holdings: type of unit designator

[% CASE 'HOLD_CANCELLATION' %]

Reasons why a hold might have been cancelled

+ [% CASE 'AR_CANCELLATION' %] +

Reasons why an article request might have been cancelled

[% CASE 'HSBND_FREQ' %]

Frequencies used by the housebound module. They are displayed on the housebound tab in the patron account in staff.

[% CASE 'ITEMTYPECAT' %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt index f93baf6d5d..0e4301e860 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt @@ -42,7 +42,7 @@ Complete request - + Cancel request @@ -82,6 +82,44 @@ [% END %] +[% BLOCK cancel_modal %] + +[% END %] [% INCLUDE 'header.inc' %] @@ -465,6 +503,7 @@ + [% PROCESS cancel_modal %] [% MACRO jsinclude BLOCK %] [% INCLUDE 'datatables.inc' %] @@ -573,30 +612,44 @@ window.open(link, 'popup', 'width=600,height=400,resizable=1,toolbar=0,scrollbars=1,top'); } + $('#modal-cancellation-reason').on('change', function(e) { + let reason = $(this).val(); + $('#modal-notes').attr('disabled', !!reason); + }) + + // Confirm cancellation of article requests + let cancel_id; + let cancel_a; + $("#cancelModalConfirmBtn").on("click",function(e) { + let reason = $("#modal-cancellation-reason").val(); + let notes = $("#modal-notes").val(); + let query = '?'+(reason?'cancellation_reason='+reason:'notes='+notes) + + HandleMulti(function(id, a) { + var table_row = a.closest('tr'); + table_row.find('.ar-process-request').remove(); + table_row.find('input[type="checkbox"]').prop('checked', false); + + + a.closest('td').prepend('').find('div.dropdown').hide(); + + $.ajax({ + type: "DELETE", + url: '/api/v1/article_requests/'+id+query, + success: function( data ) { + active_datatable.row( a.closest('tr') ).remove().draw(); + UpdateTabCounts(); + activateBatchActions( active_tab ); + } + }); + }, cancel_id, cancel_a) + }); + function Cancel( id, a ) { - // last_cancel_reason: undefined means 'prompt for new reason' - // a null value indicates that prompt was cancelled - if( last_cancel_reason === undefined ) last_cancel_reason = prompt(_("Please specify the reason for cancelling selected item(s):")); - if ( last_cancel_reason === null ) { - return; - } + cancel_id = id; + cancel_a = a; - a.closest('td').prepend('').find('div.dropdown').hide(); - $.ajax({ - type: "POST", - url: '/cgi-bin/koha/svc/article_request', - data: { - action: 'cancel', - id: id, - notes: last_cancel_reason - }, - success: function( data ) { - active_datatable.row( a.closest('tr') ).remove().draw(); - UpdateTabCounts(); - activateBatchActions( active_tab ); - }, - dataType: 'json' - }); + $('#cancelModal').modal(); } function SetPending( id, a ) { diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/request-article.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/request-article.tt index f26a10318d..d47549db90 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/request-article.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/request-article.tt @@ -3,6 +3,7 @@ [% USE KohaDates %] [% USE Branches %] [% USE ItemTypes %] +[% USE AuthorisedValues %] [% SET footerjs = 1 %] [% SET article_requests_view = 1 %] [% SET biblionumber = biblio.biblionumber %] @@ -11,6 +12,45 @@ [% INCLUDE 'doc-head-close.inc' %] +[% BLOCK cancel_modal %] + +[% END %] + [% INCLUDE 'header.inc' %] [% INCLUDE 'circ-search.inc' %] @@ -317,6 +357,7 @@ [% END %] + [% PROCESS cancel_modal %] [% END %] @@ -401,29 +442,34 @@ } }); - $(".ar-cancel-request").on("click", function(){ - var a = $(this); - var notes = prompt(_("Reason for cancellation:")); + $('#modal-cancellation-reason').on('change', function(e) { + let reason = $(this).val(); + $('#modal-notes').attr('disabled', !!reason); + }) + + let cancel_a; + $("#cancelModalConfirmBtn").on("click",function(e) { + var id = cancel_a.attr('id').split("cancel-")[1]; + $("#cancel-processing-" + id ).hide('slow'); + $("#cancel-processing-spinner-" + id ).show('slow'); + + let reason = $("#modal-cancellation-reason").val(); + let notes = $("#modal-notes").val(); + let query = '?'+(reason?'cancellation_reason='+reason:'notes='+notes) + + $.ajax({ + type: "DELETE", + url: '/api/v1/article_requests/'+id+query, + success: function( data ) { + cancel_a.parents('tr').hide('slow'); + } + }); + }); - if ( notes != null ) { - var id = this.id.split("cancel-")[1]; - $("#cancel-processing-" + id ).hide('slow'); - $("#cancel-processing-spinner-" + id ).show('slow'); - $.ajax({ - type: "POST", - url: '/cgi-bin/koha/svc/article_request', - data: { - action: 'cancel', - id: id, - notes: notes - }, - success: function( data ) { - a.parents('tr').hide('slow'); - }, - dataType: 'json' - }); - } + $(".ar-cancel-request").on("click", function(){ + cancel_a = $(this); + $('#cancelModal').modal(); }); // Initialize format(s) diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt index 578b6ccfbf..1fdc32b0e5 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt @@ -5,6 +5,7 @@ [% USE Branches %] [% USE ItemTypes %] [% USE Price %] +[% USE AuthorisedValues %] [% SET AdlibrisEnabled = Koha.Preference('AdlibrisCoversEnabled') %] [% SET AdlibrisURL = Koha.Preference('AdlibrisCoversURL') %] @@ -715,7 +716,7 @@
[% IF logged_in_user.article_requests_current.count %] - + @@ -803,12 +804,7 @@ [% END %] @@ -845,6 +841,9 @@ [% INCLUDE 'calendar.inc' %] [% INCLUDE 'datatables.inc' %]
Article requests ([% logged_in_user.article_requests_current.count | html %] total)Article requests
Record title - Cancel: -
- Cancel article request - - -
+