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;
+};