diff --git a/Koha/REST/V1/Transfers.pm b/Koha/REST/V1/Transfers.pm index f9c406a988..b9583c288a 100644 --- a/Koha/REST/V1/Transfers.pm +++ b/Koha/REST/V1/Transfers.pm @@ -19,7 +19,9 @@ use Modern::Perl; use Mojo::Base 'Mojolicious::Controller'; +use C4::Context; use Koha::Item::Transfers; +use Koha::Recalls; use Try::Tiny qw( catch try ); @@ -44,4 +46,35 @@ sub list { }; } +=head3 delete + +Controller function that handles cancelling an item transfer + +=cut + +sub delete { + my $c = shift->openapi->valid_input or return; + + my $transfer = Koha::Item::Transfers->find( $c->param('transfer_id') ); + + return $c->render_resource_not_found("Transfer") + unless $transfer; + + return try { + my $body = $c->req->json; + + $transfer->cancel( { reason => $body->{cancellation_reason}, force => 1 } ); + + # Revert an in transit recall tied to this item, as returns.pl does + if ( C4::Context->preference('UseRecalls') ) { + my $recall = Koha::Recalls->find( { item_id => $transfer->itemnumber, status => 'in_transit' } ); + $recall->revert_transfer if $recall; + } + + return $c->render_resource_deleted; + } catch { + $c->unhandled_exception($_); + }; +} + 1; diff --git a/api/v1/swagger/paths/transfers.yaml b/api/v1/swagger/paths/transfers.yaml index 1b8fa3ba67..84cd2483b5 100644 --- a/api/v1/swagger/paths/transfers.yaml +++ b/api/v1/swagger/paths/transfers.yaml @@ -85,3 +85,60 @@ x-koha-authorization: permissions: circulate: circulate_remaining_permissions +"/transfers/{transfer_id}": + delete: + x-mojo-to: Transfers#delete + operationId: deleteTransfer + tags: + - transfers + summary: Cancel transfer + parameters: + - $ref: "../swagger.yaml#/parameters/transfer_id_pp" + - name: body + in: body + description: A JSON object describing how to cancel the transfer + required: true + schema: + type: object + properties: + cancellation_reason: + type: string + description: The reason the transfer is being cancelled + required: + - cancellation_reason + additionalProperties: false + produces: + - application/json + responses: + "204": + description: Transfer cancelled + "400": + description: Bad request + schema: + $ref: "../swagger.yaml#/definitions/error" + "401": + description: Authentication required + schema: + $ref: "../swagger.yaml#/definitions/error" + "403": + description: Access forbidden + schema: + $ref: "../swagger.yaml#/definitions/error" + "404": + description: Transfer not found + schema: + $ref: "../swagger.yaml#/definitions/error" + "500": + description: | + Internal server error. Possible `error_code` attribute values: + + * `internal_server_error` + schema: + $ref: "../swagger.yaml#/definitions/error" + "503": + description: Under maintenance + schema: + $ref: "../swagger.yaml#/definitions/error" + x-koha-authorization: + permissions: + circulate: circulate_remaining_permissions diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index 42fe6aefa5..e999a2c4fd 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -699,6 +699,8 @@ paths: $ref: "./paths/transfer_limits.yaml#/~1transfer_limits~1{limit_id}" /transfers: $ref: ./paths/transfers.yaml#/~1transfers + "/transfers/{transfer_id}": + $ref: "./paths/transfers.yaml#/~1transfers~1{transfer_id}" /status/version: $ref: ./paths/status.yaml#/~1status~1version parameters: @@ -1129,6 +1131,12 @@ parameters: name: ticket_id required: true type: integer + transfer_id_pp: + description: Internal transfer identifier + in: path + name: transfer_id + required: true + type: integer transfer_limit_id_pp: description: Internal transfer limit identifier in: path diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/transferstoreceive.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/transferstoreceive.tt index 221a214c91..fa248a7872 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/transferstoreceive.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/transferstoreceive.tt @@ -74,7 +74,7 @@ cancellation_date: { "=": null }, }; - $("#transferstoreceive").kohaTable( + var transfers_table = $("#transferstoreceive").kohaTable( { ajax: { url: "/api/v1/transfers", @@ -190,8 +190,8 @@ searchable: false, orderable: false, render: function (data, type, row, meta) { - var itemnumber = encodeURIComponent(row.item_id); - return ` ${_("Cancel transfer")}`; + var transfer_id = encodeURIComponent(row.transfer_id); + return ` ${_("Cancel transfer")}`; }, }, ], @@ -200,6 +200,29 @@ 1, additional_filters ); + + $("#transferstoreceive").on("click", ".cancel-transfer", function (e) { + e.preventDefault(); + var transfer_id = $(this).data("transfer-id"); + $.ajax({ + method: "DELETE", + url: `/api/v1/transfers/${transfer_id}`, + contentType: "application/json", + data: JSON.stringify({ cancellation_reason: "Manual" }), + }) + .done(function () { + transfers_table.DataTable().ajax.reload(null, false); + }) + .fail(function (xhr) { + if (xhr.status === 404) { + // The transfer was already received or cancelled elsewhere; refresh so the row drops off + alert(_("This transfer no longer exists. It may have already been received or cancelled.")); + transfers_table.DataTable().ajax.reload(null, false); + } else { + alert(_("Something went wrong, the transfer could not be cancelled")); + } + }); + }); }); [% END %] diff --git a/t/db_dependent/api/v1/transfers.t b/t/db_dependent/api/v1/transfers.t index 923952d988..4f480aef32 100755 --- a/t/db_dependent/api/v1/transfers.t +++ b/t/db_dependent/api/v1/transfers.t @@ -18,7 +18,7 @@ use Modern::Perl; use Test::NoWarnings; -use Test::More tests => 2; +use Test::More tests => 3; use Test::Mojo; use t::lib::TestBuilder; @@ -114,3 +114,65 @@ subtest 'list() tests' => sub { $schema->storage->txn_rollback; }; + +subtest 'delete() tests' => sub { + + plan tests => 10; + + $schema->storage->txn_begin; + + my $librarian = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 2**1 } # circulate flag = 1 + } + ); + my $password = 'thePassword123'; + $librarian->set_password( { password => $password, skip_validation => 1 } ); + my $userid = $librarian->userid; + + my $patron = $builder->build_object( + { + class => 'Koha::Patrons', + value => { flags => 0 } + } + ); + $patron->set_password( { password => $password, skip_validation => 1 } ); + my $unauth_userid = $patron->userid; + + my $item = $builder->build_sample_item; + my $transfer = $builder->build_object( + { + class => 'Koha::Item::Transfers', + value => { + itemnumber => $item->itemnumber, + datesent => \'NOW()', # in transit + datearrived => undef, + datecancelled => undef, + } + } + ); + + # Unauthorized access + $t->delete_ok( + "//$unauth_userid:$password@/api/v1/transfers/" . $transfer->id => json => { cancellation_reason => 'Manual' } ) + ->status_is(403); + + # A cancellation reason is mandatory + $t->delete_ok( "//$userid:$password@/api/v1/transfers/" . $transfer->id )->status_is(400); + + # Not found + $t->delete_ok( "//$userid:$password@/api/v1/transfers/" + . ( $transfer->id + 1000 ) => json => { cancellation_reason => 'Manual' } )->status_is(404); + + # Cancel an in-transit transfer with the supplied reason + $t->delete_ok( + "//$userid:$password@/api/v1/transfers/" . $transfer->id => json => { cancellation_reason => 'WrongTransfer' } ) + ->status_is(204); + + $transfer->discard_changes; + ok( defined $transfer->datecancelled, 'The transfer has been cancelled' ); + is( $transfer->cancellation_reason, 'WrongTransfer', 'The supplied cancellation reason is recorded' ); + + $schema->storage->txn_rollback; +};