Bug 42443: Convert transfers to receive page to ajax datatable

Updates circ/transferstoreceive.pl to use the new REST API with
an ajax DataTable to display transfer to receive.

Test Plan:
1) Apply this patch
2) Restart all the things!
3) Set an item in transit to your logged in library ( check it in at another
   branch so it is sent home, or place a hold to trigger a transfer )
4) Place a hold on a second transferred item, and a recall on a third
5) Browse to /cgi-bin/koha/circ/transferstoreceive.pl
6) Note the "On hold for" column shows the hold patron, the recall patron or
   "None"
7) Set TransfersMaxDaysWarning low and note the "days late" badge appears
8) Click "Cancel transfer" and note the transfer is cancelled
9) A library with no incoming transfers will get "No transfers to receive"

Signed-off-by: Alex Carver [Acerock7] <alex@rcls.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Tomás Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Pedro Amorim <pedro.amorim@openfifth.co.uk>
This commit is contained in:
Kyle M Hall 2026-06-02 14:55:37 -04:00 committed by Pedro Amorim
parent 4fc5617615
commit 560d7b7ff7
No known key found for this signature in database
GPG Key ID: 06B75BA7F3894D55
3 changed files with 181 additions and 189 deletions

View File

@ -2087,6 +2087,26 @@ modules:
cannot_be_toggled: 1 cannot_be_toggled: 1
cannot_be_modified: 1 cannot_be_modified: 1
transferstoreceive:
transferstoreceive:
columns:
-
columnname: sentfrom
-
columnname: datesent
-
columnname: title
-
columnname: holdfor
-
columnname: homelibrary
-
columnname: callnumber
-
columnname: action
cannot_be_toggled: 1
cannot_be_modified: 1
view_holdsqueue: view_holdsqueue:
holds-table: holds-table:
default_sort_order: 3 default_sort_order: 3

View File

@ -22,22 +22,11 @@ use CGI qw ( -utf8 );
use C4::Context; use C4::Context;
use C4::Output qw( output_html_with_http_headers ); use C4::Output qw( output_html_with_http_headers );
use C4::Auth qw( get_template_and_user ); use C4::Auth qw( get_template_and_user );
use C4::Circulation qw( GetTransfersFromTo );
use C4::Members;
use Date::Calc qw( Add_Delta_Days Date_to_Days Today );
use C4::Reserves;
use Koha::Items;
use Koha::ItemTypes;
use Koha::Libraries;
use Koha::DateUtils qw( dt_from_string ); use Koha::DateUtils qw( dt_from_string );
use Koha::BiblioFrameworks;
use Koha::Patrons;
my $input = CGI->new; my $input = CGI->new;
my $itemnumber = $input->param('itemnumber');
my ( $template, $loggedinuser, $cookie, $flags ) = get_template_and_user( my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
{ {
template_name => "circ/transferstoreceive.tt", template_name => "circ/transferstoreceive.tt",
query => $input, query => $input,
@ -46,96 +35,10 @@ my ( $template, $loggedinuser, $cookie, $flags ) = get_template_and_user(
} }
); );
# set the userenv branch
my $default = C4::Context->userenv->{'branch'};
# get the all the branches for reference
my $libraries = Koha::Libraries->search( {}, { order_by => 'branchname' } );
my @branchesloop;
my $latetransfers;
while ( my $library = $libraries->next ) {
my @transferloop;
my %branchloop;
my @gettransfers = GetTransfersFromTo( $library->branchcode, $default );
if (@gettransfers) {
$branchloop{'branchname'} = $library->branchname;
$branchloop{'branchcode'} = $library->branchcode;
foreach my $num (@gettransfers) {
my %getransf;
my ( $sent_year, $sent_month, $sent_day ) = split "-",
$num->{'datesent'};
$sent_day = ( split " ", $sent_day )[0];
( $sent_year, $sent_month, $sent_day ) = Add_Delta_Days(
$sent_year, $sent_month, $sent_day,
C4::Context->preference('TransfersMaxDaysWarning')
);
my $calcDate = Date_to_Days( $sent_year, $sent_month, $sent_day );
my $today = Date_to_Days(&Today);
my $diff = $today - $calcDate;
if ( $today > $calcDate ) {
$latetransfers = 1;
$getransf{'messcompa'} = 1;
$getransf{'diff'} = $diff;
}
my $item = Koha::Items->find( $num->{itemnumber} );
my $biblio = $item->biblio;
my $itemtype = Koha::ItemTypes->find( $item->effective_itemtype );
$getransf{'datetransfer'} = $num->{'datesent'};
$getransf{'itemtype'} = $itemtype->description; # FIXME Should not it be translated_description?
%getransf = (
%getransf,
title => $biblio->title,
subtitle => $biblio->subtitle,
medium => $biblio->medium,
part_number => $biblio->part_number,
part_name => $biblio->part_name,
author => $biblio->author,
biblionumber => $biblio->biblionumber,
itemnumber => $item->itemnumber,
barcode => $item->barcode,
ccode => $item->ccode,
homebranch => $item->homebranch,
holdingbranch => $item->holdingbranch,
itemcallnumber => $item->itemcallnumber,
);
# we check if we have a reserve for this transfer
my $holds = $item->current_holds;
if ( my $first_hold = $holds->next ) {
$getransf{patron} = Koha::Patrons->find( $first_hold->borrowernumber );
}
# check for a recall for this transfer
if ( C4::Context->preference('UseRecalls') ) {
my $recall = $item->recall;
if ( defined $recall ) {
$getransf{recall} = $recall;
}
}
push( @transferloop, \%getransf );
}
# If we have a return of reservloop we put it in the branchloop sequence
$branchloop{'reserve'} = \@transferloop;
}
push( @branchesloop, \%branchloop ) if %branchloop;
}
$template->param( $template->param(
branchesloop => \@branchesloop, branchcode => C4::Context->userenv->{'branch'},
show_date => dt_from_string, show_date => dt_from_string,
TransfersMaxDaysWarning => C4::Context->preference('TransfersMaxDaysWarning'), TransfersMaxDaysWarning => C4::Context->preference('TransfersMaxDaysWarning'),
latetransfers => $latetransfers ? 1 : 0,
); );
# Checking if there is a Fast Cataloging Framework
$template->param( fast_cataloging => 1 ) if Koha::BiblioFrameworks->find('FA');
output_html_with_http_headers $input, $cookie, $template->output; output_html_with_http_headers $input, $cookie, $template->output;

View File

@ -1,9 +1,9 @@
[% USE raw %] [% USE raw %]
[% USE Asset %] [% USE Asset %]
[% USE AuthorisedValues %]
[% USE Koha %] [% USE Koha %]
[% USE KohaDates %] [% USE KohaDates %]
[% USE Branches %] [% USE Branches %]
[% USE TablesSettings %]
[% PROCESS 'i18n.inc' %] [% PROCESS 'i18n.inc' %]
[% SET footerjs = 1 %] [% SET footerjs = 1 %]
[% INCLUDE 'doc-head-open.inc' %] [% INCLUDE 'doc-head-open.inc' %]
@ -35,102 +35,171 @@
[% SET aside = Koha.Preference('CircSidebar') ? 'circ-nav' : '' %] [% SET aside = Koha.Preference('CircSidebar') ? 'circ-nav' : '' %]
[% WRAPPER 'main-container.inc' aside=aside %] [% WRAPPER 'main-container.inc' aside=aside %]
<h1>Transfers made to your library as of [% show_date | $KohaDates %]</h1> <h1>Transfers made to your library as of [% show_date | $KohaDates %]</h1>
[% IF ( branchesloop ) %] <p>Your library is the destination for the following transfer(s)</p>
<p>Your library is the destination for the following transfer(s)</p> [% IF ( TransfersMaxDaysWarning ) %]<p>Transfers are <span class="error">considered late</span> after [% TransfersMaxDaysWarning | html %] days.</p>[% END %]
[% IF ( latetransfers ) %]<p>Transfers are <span class="error">considered late</span> after [% TransfersMaxDaysWarning | html %] days.</p>[% END %] <div id="resultlist">
<div id="resultlist"> <div class="page-section">
[% FOREACH branchesloo IN branchesloop %] <table id="transferstoreceive"></table>
[% IF ( branchesloo.branchcode ) %]
<div class="page-section">
<table style="width: 100%" id="transferst[% branchesloo.branchcode | html %]">
<caption>Coming from [% branchesloo.branchname | html %]</caption>
<thead
><tr>
<th>Date of transfer</th>
<th class="anti-the">Title</th>
<th>On hold for</th>
<th>Home library</th>
<th>Call number</th>
<th class="no-sort">&nbsp;</th>
</tr></thead
>
<tbody
>[% FOREACH reser IN branchesloo.reserve %]
<tr>
<td data-order="[% reser.datetransfer | html %]">
[% reser.datetransfer | $KohaDates %]
[% IF ( reser.messcompa ) %]
<span class="badge bg-warning text-dark">[% tnx('{count} day late', '{count} days late', reser.diff, { count = reser.diff }) %]</span>
[% END %]
</td>
<td>
[% INCLUDE 'biblio-title.inc' biblio=reser link = 1 %] [% IF ( reser.author ) %]by [% reser.author | html %][% END %]
[% IF ( reser.itemtype ) %](<strong>[% reser.itemtype | html %]</strong>)[% END %]
[% IF ( reser.ccode ) -%]
<span class="collection_code">Collection: [% AuthorisedValues.GetDescriptionByKohaField(kohafield => 'items.ccode', authorised_value => reser.ccode) | html %]</span>
[%- END %]
<br />Barcode: [% reser.barcode | html %]
</td>
<td
>[% IF ( reser.patron ) %]
<a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% reser.patron.borrowernumber | uri %]">
[% reser.patron.surname | html %][% IF ( reser.patron.firstname ) %], [% reser.patron.firstname | html %][% END %]
</a>
[% IF ( reser.patron.phone ) %]<br />[% reser.patron.phone | html %][% END %]
[% IF ( reser.patron.notice_email_address ) %]
<br />
[% BLOCK subject %]Hold:[% END %]
<a href="mailto:[% reser.patron.notice_email_address | uri %]?subject=[% INCLUDE subject %] [% reser.title | uri %]"> [% reser.patron.notice_email_address | html %] </a>
[% END %]
[% IF reser.patron.cardnumber %]
<div class="cardnumber">Card number: [% reser.patron.cardnumber | html %]</div>
[% END %]
[% ELSIF ( reser.recall ) %]
Recall requested by
<a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% reser.recall.patron_id | uri %]"
>[% reser.recall.patron.surname | html %][% IF reser.recall.patron.firstname %], [% reser.recall.patron.firstname | html %][% END %] ([% reser.recall.patron.cardnumber | html %])</a
>
[% ELSE %]
<p>None</p>
[% END %]
</td>
<td>[% IF reser.homebranch %][% Branches.GetName( reser.homebranch ) | html %][% END %]</td>
<td>[% reser.itemcallnumber | html %]</td>
<td class="actions"
><a href="/cgi-bin/koha/circ/returns.pl?itemnumber=[% reser.itemnumber | uri %]&amp;canceltransfer=1&amp;dest=ttr" class="btn btn-default btn-xs"
><i class="fa fa-times"></i> Cancel transfer</a
></td
>
</tr>
[% END %]</tbody
>
</table>
</div>
<!-- /.page-section -->
[% END %]
[% END %]
</div> </div>
[% ELSE %] <!-- /.page-section -->
<p>No transfers to receive</p> </div>
[% END %]
[% END %] [% END %]
[% MACRO jsinclude BLOCK %] [% MACRO jsinclude BLOCK %]
[% INCLUDE 'datatables.inc' %] [% INCLUDE 'datatables.inc' %]
<script nonce="[% Koha.CSPNonce | $raw %]" data-tt-allowed="true"> [% INCLUDE 'js-biblio-format.inc' %]
let library_ids = []; [% INCLUDE 'js-date-format.inc' %]
[% FOR library IN branchesloop %] <script nonce="[% Koha.CSPNonce | $raw %]">
library_ids.push("[% library.branchcode | html %]"); var table_settings = [% TablesSettings.GetTableSettings( 'circ', 'transferstoreceive', 'transferstoreceive', 'json' ) | $raw %];
[% END %] var TransfersMaxDaysWarning = [% TransfersMaxDaysWarning ? TransfersMaxDaysWarning : 0 | html %];
// Mirror the legacy "late transfer" calculation: a transfer is late once it has been
// sent for more than TransfersMaxDaysWarning days without arriving
function days_late(sent_date) {
if (!sent_date || !TransfersMaxDaysWarning) return 0;
var sent = new Date(sent_date);
var threshold = new Date(sent.getFullYear(), sent.getMonth(), sent.getDate() + Number(TransfersMaxDaysWarning));
var now = new Date();
var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
var diff = Math.floor((today - threshold) / 86400000);
return diff > 0 ? diff : 0;
}
</script> </script>
<script nonce="[% Koha.CSPNonce | $raw %]"> <script nonce="[% Koha.CSPNonce | $raw %]">
$(document).ready(function () { $(document).ready(function () {
library_ids.forEach(library_id => { var additional_filters = {
$(`#transferst${library_id}`).kohaTable({ to_library_id: "[% branchcode | html %]",
dom: "t", sent_date: { "!=": null },
paging: false, arrived_date: { "=": null },
}); cancellation_date: { "=": null },
}); };
$("#transferstoreceive").kohaTable(
{
ajax: {
url: "/api/v1/transfers",
},
language: {
emptyTable: _("No transfers to receive"),
},
embed: [
"item+strings",
"item.biblio",
"item.home_library",
"item.item_type",
"item.first_hold.patron",
"item.recall.patron",
"from_library",
],
order: [[1, "asc"]],
columns: [
{
data: "me.from_library_id",
title: _("Sent from"),
searchable: true,
orderable: true,
render: function (data, type, row, meta) {
return escape_str(row.from_library.name);
},
},
{
data: "sent_date",
title: _("Date of transfer"),
type: "date",
searchable: false,
orderable: true,
render: function (data, type, row, meta) {
var html = $date(data);
var late = days_late(data);
if (late > 0) {
var badge = __nx("{count} day late", "{count} days late", late, { count: late });
html += ` <span class="badge bg-warning text-dark">${badge}</span>`;
}
return html;
},
},
{
data: "item.biblio.title",
title: _("Title"),
searchable: true,
orderable: true,
render: function (data, type, row, meta) {
var item = row.item;
var html = $biblio_to_html(item.biblio, { link: 1 });
if (item.biblio.author) {
var author = escape_str(item.biblio.author);
html += ` ${_("by")} ${author}`;
}
if (item.item_type) {
var item_type = escape_str(item.item_type.translated_description || item.item_type.description);
if (item_type) html += ` (<strong>${item_type}</strong>)`;
}
if (item._strings.collection_code && item._strings.collection_code.str) {
var collection_code = escape_str(item._strings.collection_code.str);
html += ` <span class="collection_code">${_("Collection:")} ${collection_code}</span>`;
}
var barcode = escape_str(item.external_id);
html += `<br>${_("Barcode:")} ${barcode}`;
return html;
},
},
{
data: null,
title: _("On hold for"),
searchable: false,
orderable: false,
render: function (data, type, row, meta) {
var item = row.item;
if (item.first_hold) {
return $patron_to_html(item.first_hold.patron, { url: true, display_cardnumber: true });
} else if (item.recall) {
var patron = $patron_to_html(item.recall.patron, { url: true, display_cardnumber: true });
return `${_("Recall requested by")} ${patron}`;
} else {
return _("None");
}
},
},
{
data: "item.home_library_id",
title: _("Home library"),
searchable: true,
orderable: true,
render: function (data, type, row, meta) {
return escape_str(row.item.home_library.name);
},
},
{
data: "item.callnumber",
title: _("Call number"),
searchable: true,
orderable: true,
render: function (data, type, row, meta) {
return escape_str(data);
},
},
{
data: null,
title: _("Actions"),
class: "actions",
searchable: false,
orderable: false,
render: function (data, type, row, meta) {
var itemnumber = encodeURIComponent(row.item_id);
return `<a href="/cgi-bin/koha/circ/returns.pl?itemnumber=${itemnumber}&canceltransfer=1&dest=ttr" class="btn btn-default btn-xs"><i class="fa fa-times"></i> ${_("Cancel transfer")}</a>`;
},
},
],
},
table_settings,
1,
additional_filters
);
}); });
</script> </script>
[% END %] [% END %]