Bug 28530: Implement float limits

Signed-off-by: Kris Becker <kbecker@jcls.org>
Signed-off-by: Martin Renvoize <martin.renvoize@openfifth.co.uk>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Lucas Gass 2025-07-31 20:58:42 +00:00
parent 83e6aecd07
commit 1f63d8c13f
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
8 changed files with 193 additions and 60 deletions

View File

@ -2346,6 +2346,19 @@ sub AddReturn {
# if $hbr was "noreturn" or any other non-item table value, then it should 'float' (i.e. stay at this branch)
my $transfer_trigger = $hbr eq 'homebranch' ? 'ReturnToHome' : $hbr eq 'holdingbranch' ? 'ReturnToHolding' : undef;
# check library float limits if enabled if the item isn't being transferred away
if ( ( $returnbranch eq $branch ) && C4::Context->preference('UseLibraryFloatLimits') ) {
my $effective_itemtype = $item->effective_itemtype;
my $limit = Koha::Library::FloatLimits->find( { itemtype => $effective_itemtype, branchcode => $branch } );
if ($limit) {
my $transfer_library = Koha::Library::FloatLimits->lowest_ratio_library( $item, $branch );
if ( $transfer_library && $transfer_library->branchcode ne $branch ) {
$returnbranch = $transfer_library->branchcode;
$transfer_trigger = 'LibraryFloatLimit';
}
}
}
my $borrowernumber = $patron ? $patron->borrowernumber : undef; # we don't know if we had a borrower or not
my $patron_unblessed = $patron ? $patron->unblessed : {};

View File

@ -22,6 +22,7 @@ use Modern::Perl;
use Koha::Database;
use Koha::Library::FloatLimit;
use Koha::Libraries;
use C4::Circulation qw(IsBranchTransferAllowed);
use base qw(Koha::Objects);
@ -41,40 +42,141 @@ Koha::Library::FloatLimits - Koha Library::FloatLimit object set class
sub lowest_ratio_library {
my ( $self, $item, $branchcode ) = @_;
my $field = C4::Context->preference('item-level_itypes') ? 'items.itype' : 'biblioitems.itemtype';
my $query = qq{
SELECT branchcode
FROM library_float_limits
LEFT JOIN items ON ( items.itype = library_float_limits.itemtype AND items.holdingbranch = library_float_limits.branchcode )
LEFT JOIN biblioitems ON ( items.biblioitemnumber = biblioitems.biblioitemnumber )
WHERE library_float_limits.itemtype = ?
AND ( $field = ? OR $field IS NULL )
AND library_float_limits.float_limit != 0
GROUP BY branchcode
ORDER BY COUNT(items.holdingbranch)/library_float_limits.float_limit ASC, library_float_limits.float_limit DESC;
};
my $schema = Koha::Database->new->schema;
my $results = C4::Context->dbh->selectall_arrayref(
$query, { Slice => {} }, $item->effective_itemtype,
$item->effective_itemtype
);
my @float_limits = $schema->resultset('LibraryFloatLimit')->search(
{
itemtype => $item->effective_itemtype,
float_limit => { '!=' => 0 }
}
)->all;
return unless @float_limits;
my @candidates;
my $use_item_level = C4::Context->preference('item-level_itypes');
foreach my $limit (@float_limits) {
my $branch = $limit->get_column('branchcode');
my $float_limit_val = $limit->get_column('float_limit');
my $item_count;
if ($use_item_level) {
my $at_branch_count = Koha::Items->search(
{
itype => $item->effective_itemtype,
holdingbranch => $branch,
onloan => undef
},
)->count;
# Count items in transit TO this branch
my $in_transit_to_count = Koha::Items->search(
{
itype => $item->effective_itemtype,
# Join with active transfers where this branch is the destination
},
{
join => 'branchtransfers',
where => {
'branchtransfers.tobranch' => $branch,
'branchtransfers.datearrived' => undef, # Still in transit
'branchtransfers.datecancelled' => undef, #Not cancelled
},
distinct => 1
}
)->count;
my $in_transit_from_count = Koha::Items->search(
{ itype => $item->effective_itemtype },
{
join => 'branchtransfers',
where => {
'branchtransfers.frombranch' => $branch,
'branchtransfers.datearrived' => undef, # Still in transit
'branchtransfers.datecancelled' => undef, #Not cancelled
},
distinct => 1
}
)->count;
$item_count = $at_branch_count + $in_transit_to_count - $in_transit_from_count;
} else {
my $at_branch_count = Koha::Items->search(
{
holdingbranch => $branch,
'biblioitem.itemtype' => $item->effective_itemtype,
onloan => undef
},
)->count;
# Count items in transit TO this branch
my $in_transit_to_count = Koha::Items->search(
{
itype => $item->effective_itemtype,
},
{
join => 'branchtransfers',
where => {
'branchtransfers.tobranch' => $branch,
'branchtransfers.datearrived' => undef, # Still in transit
'branchtransfers.datecancelled' => undef, #Not cancelled
},
distinct => 1
}
)->count;
my $in_transit_from_count = Koha::Items->search(
{
itype => $item->effective_itemtype,
},
{
join => 'branchtransfers',
where => {
'branchtransfers.frombranch' => $branch,
'branchtransfers.datearrived' => undef, # Still in transit
'branchtransfers.datecancelled' => undef, #Not cancelled
},
distinct => 1
}
)->count;
$item_count = $at_branch_count + $in_transit_to_count - $in_transit_from_count;
}
my $ratio = $item_count / $float_limit_val;
push @candidates, {
branchcode => $branch,
ratio => $ratio,
float_limit => $float_limit_val
};
}
# sort the branches by lowest ratio in the event of a tie choose a random branch
@candidates = sort { $a->{ratio} <=> $b->{ratio} || rand() <=> rand() } @candidates;
my $UseBranchTransferLimits = C4::Context->preference("UseBranchTransferLimits");
my $BranchTransferLimitsType =
C4::Context->preference("BranchTransferLimitsType") eq 'itemtype' ? 'effective_itemtype' : 'ccode';
foreach my $r (@$results) {
my $transfer_branch;
for my $candidate (@candidates) {
if ($UseBranchTransferLimits) {
my $allowed = C4::Circulation::IsBranchTransferAllowed(
$r->{branchcode}, # to
$branchcode, # from
$candidate->{branchcode},
$branchcode,
$item->$BranchTransferLimitsType
);
return $r->{branchcode} if $allowed;
$transfer_branch = Koha::Libraries->find( $candidate->{branchcode} ) if $allowed;
last;
} else {
return $r->{branchcode};
$transfer_branch = Koha::Libraries->find( $candidate->{branchcode} );
last;
}
}
return $transfer_branch;
}
=head3 _type

View File

@ -41,7 +41,7 @@ my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
my $op = $input->param('op');
if ( $op eq 'set_float_limits' ) {
if ( $op eq 'cud-set_float_limits' ) {
my $schema = Koha::Database->new()->schema();
my @branches = Koha::Libraries->search()->as_list;
my @itemtypes = Koha::ItemTypes->search()->as_list;

View File

@ -313,7 +313,6 @@ if ( $barcode && ( $op eq 'cud-checkin' || $op eq 'cud-affect_reserve' ) ) {
my $biblio = $item->biblio;
$template->param(
title => $biblio->title,
returnbranch => $returnbranch,
author => $biblio->author,
itembiblionumber => $biblio->biblionumber,
biblionumber => $biblio->biblionumber,
@ -428,6 +427,8 @@ if ( $barcode && ( $op eq 'cud-checkin' || $op eq 'cud-affect_reserve' ) ) {
);
}
$returnbranch = $messages->{NeedsTransfer} if $messages->{NeedsTransfer};
# Mark missing bundle items as lost and report unexpected items
if ( $item
&& $item->is_bundle
@ -524,6 +525,13 @@ my $recalled = 0;
# new op dev : we check if the document must be returned to his homebranch directly,
# if the document is transferred, we have warning message .
if ( $messages->{'LibraryFloatLimitTransferRequest'} ) {
$template->param(
LibraryFloatLimitTransferRequest => 1,
itemnumber => $itemnumber,
);
}
if ( $messages->{'WasTransfered'} ) {
$template->param(
found => 1,
@ -756,6 +764,8 @@ foreach my $code ( keys %$messages ) {
;
} elsif ( $code eq 'TransferredRecall' ) {
;
} elsif ( $code eq 'LibraryFloatLimitTransferRequest' ) {
;
} elsif ( $code eq 'InBundle' ) {
$template->param( InBundle => $messages->{InBundle} );
} elsif ( $code eq 'UpdateLastSeenError' ) {
@ -834,7 +844,10 @@ if ($barcode) {
}
}
$template->param( itemnumber => $itemnumber );
$template->param(
itemnumber => $itemnumber,
returnbranch => $returnbranch,
);
# Checking if there is a Fast Cataloging Framework
$template->param( fast_cataloging => 1 ) if Koha::BiblioFrameworks->find('FA');

View File

@ -24,40 +24,40 @@
</ul>
[% END %]
[% IF ( CAN_user_parameters_manage_patron_categories || CAN_user_parameters_manage_circ_rules || CAN_user_parameters_manage_patron_attributes || CAN_user_parameters_manage_transfers || CAN_user_parameters_manage_item_circ_alerts || CAN_user_parameters_manage_cities || CAN_user_parameters_manage_curbside_pickups || CAN_user_parameters_manage_patron_restrictions ) %]
<h5>Patrons and circulation</h5>
<ul>
[% IF ( CAN_user_parameters_manage_patron_categories ) %]
<li><a href="/cgi-bin/koha/admin/categories.pl">Patron categories</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_circ_rules ) %]
<li><a href="/cgi-bin/koha/admin/smart-rules.pl">Circulation and fine rules</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_patron_attributes ) %]
<li><a href="/cgi-bin/koha/admin/patron-attr-types.pl">Patron attribute types</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_transfers ) %]
<li><a href="/cgi-bin/koha/admin/branch_transfer_limits.pl">Library transfer limits</a></li>
<li><a href="/cgi-bin/koha/admin/transport-cost-matrix.pl">Transport cost matrix</a></li>
<li><a href="/cgi-bin/koha/admin/float_limits.pl">Library float limits</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_item_circ_alerts ) %]
<li><a href="/cgi-bin/koha/admin/item_circulation_alerts.pl">Item circulation alerts</a></li>
[% END %]
[% IF ( Koha.Preference('UseCirculationDesks') && CAN_user_parameters_manage_libraries ) %]
<li><a href="/cgi-bin/koha/admin/desks.pl">Desks</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_cities ) %]
<li><a href="/cgi-bin/koha/admin/cities.pl">Cities and towns</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_curbside_pickups ) %]
<li><a href="/cgi-bin/koha/admin/curbside_pickup.pl">Curbside pickup</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_patron_restrictions ) %]
<li><a href="/cgi-bin/koha/admin/restrictions.pl">Patron restriction types</a></li>
[% END %]
</ul>
[% END %]
[% IF ( CAN_user_parameters_manage_patron_categories || CAN_user_parameters_manage_circ_rules || CAN_user_parameters_manage_patron_attributes || CAN_user_parameters_manage_transfers || CAN_user_parameters_manage_item_circ_alerts || CAN_user_parameters_manage_cities || CAN_user_parameters_manage_curbside_pickups || CAN_user_parameters_manage_patron_restrictions ) %]
<h5>Patrons and circulation</h5>
<ul>
[% IF ( CAN_user_parameters_manage_patron_categories ) %]
<li><a href="/cgi-bin/koha/admin/categories.pl">Patron categories</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_circ_rules ) %]
<li><a href="/cgi-bin/koha/admin/smart-rules.pl">Circulation and fine rules</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_patron_attributes ) %]
<li><a href="/cgi-bin/koha/admin/patron-attr-types.pl">Patron attribute types</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_transfers ) %]
<li><a href="/cgi-bin/koha/admin/branch_transfer_limits.pl">Library transfer limits</a></li>
<li><a href="/cgi-bin/koha/admin/transport-cost-matrix.pl">Transport cost matrix</a></li>
<li><a href="/cgi-bin/koha/admin/float_limits.pl">Library float limits</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_item_circ_alerts ) %]
<li><a href="/cgi-bin/koha/admin/item_circulation_alerts.pl">Item circulation alerts</a></li>
[% END %]
[% IF ( Koha.Preference('UseCirculationDesks') && CAN_user_parameters_manage_libraries ) %]
<li><a href="/cgi-bin/koha/admin/desks.pl">Desks</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_cities ) %]
<li><a href="/cgi-bin/koha/admin/cities.pl">Cities and towns</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_curbside_pickups ) %]
<li><a href="/cgi-bin/koha/admin/curbside_pickup.pl">Curbside pickup</a></li>
[% END %]
[% IF ( CAN_user_parameters_manage_patron_restrictions ) %]
<li><a href="/cgi-bin/koha/admin/restrictions.pl">Patron restriction types</a></li>
[% END %]
</ul>
[% END %]
[% IF ( CAN_user_parameters_manage_accounts || ( Koha.Preference('UseCashRegisters') && CAN_user_parameters_manage_cash_registers ) ) %]
<h5>Accounting</h5>

View File

@ -108,6 +108,8 @@
>
<dt><a href="/cgi-bin/koha/admin/transport-cost-matrix.pl">Transport cost matrix</a></dt>
<dd>Define transport costs between branches</dd>
<dt><a href="/cgi-bin/koha/admin/float_limits.pl">Library float limits</a></dt>
<dd>Define when items should be transferred to other libraries on checkin to balance floating collections</dd>
[% END %]
[% IF ( CAN_user_sip2 ) %]
<dt><a href="/cgi-bin/koha/sip2/sip2.pl">Self-service circulation (SIP2)</a></dt>

View File

@ -38,7 +38,7 @@
<div class="main container-fluid">
<div class="row">
<div class="col-sm-10 col-sm-push-2">
<div class="col-sm-10 order-sm-1 col-sm-push-2">
<main>
<h1 class="parameters"> Library float limits </h1>
@ -53,7 +53,8 @@
[% END %]
<form method="post" action="/cgi-bin/koha/admin/float_limits.pl" id="float_limit_form">
<input type="hidden" name="op" value="set_float_limits" />
[% INCLUDE 'csrf-token.inc' %]
<input type="hidden" name="op" value="cud-set_float_limits" />
<fieldset id="float_limits">
<div class="help">
<p>Specify the total number of items per itemtype that are allowed to float at each library.</p>

View File

@ -120,6 +120,8 @@
<p>
[%- SWITCH trigger -%]
[%- CASE 'LibraryFloatLimit' -%]
<span>Library has exceeded float limit for this item type</span>
[%- CASE 'Manual' -%]
<span>Manual</span>
[%- CASE 'StockrotationAdvance' -%]