@@ -, +, @@ --- Koha/ArticleRequest.pm | 23 ++++- 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, 519 insertions(+), 60 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 --- a/Koha/ArticleRequest.pm +++ a/Koha/ArticleRequest.pm @@ -83,9 +83,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(); @@ -100,12 +101,23 @@ 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 ( my $letter = C4::Letters::GetPreparedLetter( - module => 'circulation', - letter_code => "AR_$status", # AR_PENDING, AR_PROCESSING, AR_COMPLETED, AR_CANCELED + module => 'circulation', + letter_code => "AR_$status" + , # AR_PENDING, AR_PROCESSING, AR_COMPLETED, AR_CANCELED message_transport_type => 'email', lang => $self->borrower->lang, tables => { @@ -116,6 +128,9 @@ sub notify { items => $self->itemnumber, branches => $self->branchcode, }, + substitute => { + reason => $reason, + }, ) ) { @@ -125,7 +140,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}; } } --- a/Koha/Exceptions/ArticleRequests.pm +++ a/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; --- a/Koha/REST/V1/ArticleRequests.pm +++ a/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; --- a/Koha/REST/V1/Patrons.pm +++ a/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; --- a/api/v1/swagger/parameters.json +++ a/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", --- a/api/v1/swagger/parameters/article_request.json +++ a/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" + } +} --- a/api/v1/swagger/paths.json +++ a/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" }, --- a/api/v1/swagger/paths/article_requests.json +++ a/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" + } + } + } + } +} --- a/api/v1/swagger/paths/public_patrons.json +++ a/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 + } + } } } --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/authorised_values.tt +++ a/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' %] --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt +++ a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/article-requests.tt @@ -36,7 +36,7 @@ Complete request - + Cancel request @@ -75,6 +75,44 @@ [% END %] +[% BLOCK cancel_modal %] + +[% END %] [% INCLUDE 'header.inc' %] @@ -346,6 +384,7 @@ + [% PROCESS cancel_modal %] [% MACRO jsinclude BLOCK %] [% INCLUDE 'datatables.inc' %] @@ -442,30 +481,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 Process( id, a ) { --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/request-article.tt +++ a/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' %] @@ -310,6 +350,7 @@ [% END %] + [% PROCESS cancel_modal %] [% END %] @@ -394,29 +435,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) --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt +++ a/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 %] - + @@ -801,12 +802,7 @@ [% END %] @@ -843,6 +839,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 - - -
+