From 49758c5768e184461034f660f01a11598b5aec8d Mon Sep 17 00:00:00 2001 From: Lucas Gass Date: Thu, 31 Jul 2025 20:58:42 +0000 Subject: [PATCH] Bug 28530: Implement float limits --- C4/Circulation.pm | 14 ++ Koha/Library/FloatLimits.pm | 146 +++++++++++++++--- admin/float_limits.pl | 2 +- circ/returns.pl | 17 +- .../prog/en/includes/admin-menu.inc | 68 ++++---- .../prog/en/modules/admin/admin-home.tt | 2 + .../prog/en/modules/admin/float_limits.tt | 5 +- .../prog/en/modules/circ/returns.tt | 2 + 8 files changed, 195 insertions(+), 61 deletions(-) diff --git a/C4/Circulation.pm b/C4/Circulation.pm index 514eb3ae97a..e9ee3b5ae7e 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -52,6 +52,7 @@ use Koha::Patrons; use Koha::Patron::Debarments qw( DelUniqueDebarment AddUniqueDebarment ); use Koha::Database; use Koha::Libraries; +use Koha::Library::FloatLimits; use Koha::Account::Lines; use Koha::Holds; use Koha::Account::Lines; @@ -2298,6 +2299,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 : {}; diff --git a/Koha/Library/FloatLimits.pm b/Koha/Library/FloatLimits.pm index c491cbf9273..189c67d6cfe 100644 --- a/Koha/Library/FloatLimits.pm +++ b/Koha/Library/FloatLimits.pm @@ -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 $results = C4::Context->dbh->selectall_arrayref( - $query, { Slice => {} }, $item->effective_itemtype, - $item->effective_itemtype - ); + my $schema = Koha::Database->new->schema; + + 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 diff --git a/admin/float_limits.pl b/admin/float_limits.pl index fe882de84d8..1f2477bc05c 100755 --- a/admin/float_limits.pl +++ b/admin/float_limits.pl @@ -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; diff --git a/circ/returns.pl b/circ/returns.pl index 901877ef1e5..276ce641d2e 100755 --- a/circ/returns.pl +++ b/circ/returns.pl @@ -303,7 +303,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, @@ -413,6 +412,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 @@ -509,6 +510,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, @@ -741,6 +749,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' ) { @@ -819,7 +829,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'); diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc index b6a4d1324b4..a72549ac72e 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/admin-menu.inc @@ -24,40 +24,40 @@ [% 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 ) %] -
Patrons and circulation
- - [% 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 ) %] +
Patrons and circulation
+ + [% END %] [% IF ( CAN_user_parameters_manage_accounts || ( Koha.Preference('UseCashRegisters') && CAN_user_parameters_manage_cash_registers ) ) %]
Accounting
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt index a8813575fa2..993178cd290 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/admin-home.tt @@ -108,6 +108,8 @@ >
Transport cost matrix
Define transport costs between branches
+
Library float limits
+
Define when items should be transferred to other libraries on checkin to balance floating collections
[% END %] [% IF ( CAN_user_parameters_manage_item_circ_alerts ) %]
Item circulation alerts
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/float_limits.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/float_limits.tt index 5b051e13a6b..9709c43ad08 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/float_limits.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/float_limits.tt @@ -38,7 +38,7 @@
-
+

Library float limits

@@ -53,7 +53,8 @@ [% END %]
- + [% INCLUDE 'csrf-token.inc' %] +

Specify the total number of items per itemtype that are allowed to float at each library.

diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt index e52eeae7936..d5c4ed28b57 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt @@ -120,6 +120,8 @@

[%- SWITCH trigger -%] + [%- CASE 'LibraryFloatLimit' -%] + Library has exceeded float limit for this item type [%- CASE 'Manual' -%] Manual [%- CASE 'StockrotationAdvance' -%] -- 2.39.5