From deedfbb6492cb5f5167b1f7c219796b1cfe21367 Mon Sep 17 00:00:00 2001 From: Lucas Gass Date: Wed, 12 Mar 2025 21:31:35 +0000 Subject: [PATCH] Bug 31698: Add ability to move holds to new items or new records --- Koha/Hold.pm | 61 ++++ .../prog/en/includes/holds_table.inc | 10 +- .../prog/en/modules/reserve/request.tt | 295 ++++++++++++++++++ reserve/request.pl | 58 +++- 4 files changed, 421 insertions(+), 3 deletions(-) diff --git a/Koha/Hold.pm b/Koha/Hold.pm index b3bf4a02b76..f76c0189580 100644 --- a/Koha/Hold.pm +++ b/Koha/Hold.pm @@ -178,6 +178,67 @@ sub delete { return $deleted; } +=head3 move_hold + +$hold->move_hold(); + +=cut + +sub move_hold { + my ( $self, $args ) = @_; + + my $original = $self; + my $original_biblionumber = $self->biblionumber; + + my $patron = Koha::Patrons->find( { borrowernumber => $self->borrowernumber } ); + return { success => 0, error => 'Missing patron or target' } unless $patron; + + my ( $new_biblionumber, $new_itemnumber, $item, $canReserve ); + + if ( $args->{new_itemnumber} ) { + $item = Koha::Items->find( { itemnumber => $args->{new_itemnumber} } ) + or return { success => 0, error => 'Target item not found' }; + + $new_itemnumber = $item->itemnumber; + $new_biblionumber = $item->biblionumber; + + $canReserve = C4::Reserves::CanItemBeReserved( $patron, $item, $self->branchcode, { ignore_hold_counts => 1 } ); + } elsif ( $args->{new_biblionumber} ) { + $new_biblionumber = $args->{new_biblionumber}; + + $canReserve = C4::Reserves::CanBookBeReserved( + $patron->borrowernumber, $new_biblionumber, $self->branchcode, + { ignore_hold_counts => 1 } + ); + } else { + return { success => 0, error => 'Missing itemnumber or biblionumber' }; + } + + return { success => 0, error => $canReserve->{status} } + unless $canReserve->{status} eq 'OK'; + + my $max = Koha::Holds->search( + { biblionumber => $new_biblionumber }, + { order_by => { -desc => 'priority' }, rows => 1 } + )->next; + my $base_priority = $max ? $max->priority : 0; + my $priority = $base_priority + 1; + my $new_priority = C4::Reserves::_ShiftPriority( $new_biblionumber, $priority ); + + $self->update( + { + itemnumber => $new_itemnumber, # undef for biblio-level is fine + biblionumber => $new_biblionumber, + priority => $new_priority, + } + ); + + C4::Log::logaction( 'HOLDS', 'MODIFY', $self->reserve_id, $self, undef, $original ) + if C4::Context->preference('HoldsLog'); + + return { success => 1, hold => $self }; +} + =head3 set_transfer =cut diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/holds_table.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/holds_table.inc index 2d65e83dc1f..0d26304999e 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/holds_table.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/holds_table.inc @@ -49,7 +49,15 @@ [%- END -%] [% SET tr_class = hold.suspend ? 'suspend' : '' %] - + diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt index 558b5e0d9f0..790fe2dd15a 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/reserve/request.tt @@ -105,6 +105,9 @@ :disabled { opacity: 0.5; } + #toolbar { + align-items: center; + } [% END %] @@ -171,6 +174,60 @@ [% END %] + [% IF ( hold_move_successes ) %] +
+

Moved the following holds:

+ +
+ [% END %] + [% IF ( hold_move_failures ) %] +
+ One or more holds were not moved due to following errors: + +
+ [% END %] + [% IF ( failed_holds ) %]
One or more holds were not placed due to following errors: @@ -1235,6 +1292,15 @@ [% END %] + [% IF ( CAN_user_reserveforothers_alter_hold_targets ) %] + + [% END %]
[% FOREACH biblioloo IN biblioloop %] @@ -1416,6 +1482,104 @@
+ +
[% INCLUDE 'csrf-token.inc' %]
@@ -1467,6 +1631,7 @@ } var MSG_CANCEL_SELECTED = _("Cancel selected (%s)"); + var MSG_MOVE_SELECTED = _("Move selected (%s)"); var MSG_CANCEL_ALERT = _("This action will cancel %s hold(s)."); $.fn.select2.defaults.set("width", "100%" ); $.fn.select2.defaults.set("dropdownAutoWidth", true ); @@ -1731,6 +1896,91 @@ } $(document).ready(function() { + $("#itemSearchForm").on("submit", function (event) { + event.preventDefault(); + + let externalID = $("#external_id").val(); + let apiUrl = `/api/v1/items?external_id=${encodeURIComponent(externalID)}`; + + $.ajax({ + url: apiUrl, + method: "GET", + dataType: "json", + success: function (data) { + if (data.length > 0) { + let resultHtml = ""; + $.each(data, function (index, item) { + resultHtml += ` +
+ Biblionumber: ${item.biblio_id}
+ Item: ${item.external_id}
+ + + +
+
+ `; + }); + $("#itemResultMessage").html(resultHtml); + } else { + $("#itemResultMessage").html(` +
No item found with barcode: ${externalID}.
+ `); + } + }, + }); + }); + + $("#biblioSearchForm").on("submit", function (event) { + event.preventDefault(); + + let biblioID = $("#biblio_id").val(); + let apiUrl = `/api/v1/items?q={"biblio_id":"${encodeURIComponent(biblioID)}"}`; + + $.ajax({ + url: apiUrl, + method: "GET", + dataType: "json", + success: function (data) { + if (data.length > 0) { + let resultHtml = ""; + $.each(data, function (index, item) { + resultHtml += ` +
+ Biblionumber: ${item.biblio_id}
+ + +
+
+ `; + }); + $("#biblioResultMessage").html(resultHtml); + } else { + $("#biblioResultMessage").html(` +
No item found with barcode: ${biblioID}.
+ `); + } + }, + }); + }); + + $(document).on("change", 'input[name="new_itemnumber"]', function() { + $('input[name="new_itemnumber"]').not(this).prop("checked", false); + if ( $('input[name="new_itemnumber"]:checked').length ){ + $('#move_hold_item_confirm').show(); + } else { + $('#move_hold_item_confirm').hide(); + } + }); + + $(document).on("change", 'input[name="new_biblionumber"]', function() { + $('input[name="new_biblionumber"]').not(this).prop("checked", false); + if ( $('input[name="new_biblionumber"]:checked').length ){ + $('#move_hold_biblio_confirm').show(); + } else { + $('#move_hold_biblio_confirm').hide(); + } + }); $("#always_show_holds").change(function(){ if( $(this).prop('checked') ){ @@ -1834,6 +2084,7 @@ }); $('.cancel_selected_holds').html(MSG_CANCEL_SELECTED.format($('.holds_table .select_hold:checked').length)); + $('.move_selected_holds').html(MSG_MOVE_SELECTED.format($('.holds_table .select_hold:checked').length)); $('.holds_table .select_hold_all').click(function() { var table = $(this).parents('.holds_table'); @@ -1841,6 +2092,7 @@ $('.select_hold', table).prop('checked', !count); $(this).prop('checked', !count); $('.cancel_selected_holds').html(MSG_CANCEL_SELECTED.format($('.holds_table .select_hold:checked').length)); + $('.move_selected_holds').html(MSG_MOVE_SELECTED.format($('.holds_table .select_hold:checked').length)); $('#cancel_hold_alert').html( MSG_CANCEL_ALERT.format($('.holds_table .select_hold:checked').length)); $('#cancel_hold_alert').show(); localStorage.selectedHolds = $('.holds_table .select_hold:checked').toArray().map(el => $(el).data('id')); @@ -1851,11 +2103,54 @@ var count = $('.select_hold:not(:checked)', table).length; $('.select_hold_all', table).prop('checked', !count); $('.cancel_selected_holds').html(MSG_CANCEL_SELECTED.format($('.holds_table .select_hold:checked').length)); + $('.move_selected_holds').html(MSG_MOVE_SELECTED.format($('.holds_table .select_hold:checked').length)); $('#cancel_hold_alert').html( MSG_CANCEL_ALERT.format($('.holds_table .select_hold:checked').length)); $('#cancel_hold_alert').show(); localStorage.selectedHolds = $('.holds_table .select_hold:checked').toArray().map(el => $(el).data('id')); }); + $('.move_hold_item').click(function(e) { + e.preventDefault(); + if($('.holds_table .select_hold:checked').length) { + $('#itemResultMessage').empty(); + $('#move_hold_item_selection table tbody').empty(); + $('#moveHoldItemModal').modal('show'); + $('.select_hold:checked').each( function() { + let reserve_id = $(this).data('id'); + let reserve_biblionumber = $(this).data('biblionumber'); + let reserve_itemnumber = $(this).data('itemnumber'); + let item_level_hold = $(this).data('item_level_hold'); + let error_message = $(this).data('item_level_hold') ? "" : _("Cannot move a waiting or record level hold"); + let found_status = $(this).data('found'); + $('#move_hold_item_selection table').append(`${reserve_id}Biblionumber: ${reserve_biblionumber} Itemnumber: ${reserve_itemnumber}${error_message}`) + if ( item_level_hold ) { + $('#move_hold_item_form').append(``); + } + }); + } + }); + + $('.move_hold_record').click(function(e) { + e.preventDefault(); + if($('.holds_table .select_hold:checked').length) { + $('#biblioResultMessage').empty(); + $('#move_hold_item_selection table tbody').empty(); + $('#moveHoldBiblioModal').modal('show'); + $('.select_hold:checked').each( function() { + let reserve_id = $(this).data('id'); + let reserve_biblionumber = $(this).data('biblionumber'); + let reserve_itemnumber = $(this).data('itemnumber'); + let item_level_hold = $(this).data('item_level_hold'); + let error_message = $(this).data('item_level_hold') ? _("Cannot move a waiting or item level hold") : ""; + let found_status = $(this).data('found'); + $('#move_hold_item_selection table').append(`${reserve_id}Biblionumber: ${reserve_biblionumber}${error_message}`) + if ( !item_level_hold ) { + $('#move_hold_biblio_form').append(``); + } + }); + } + }); + $('.cancel_selected_holds').click(function(e) { e.preventDefault(); if($('.holds_table .select_hold:checked').length) { diff --git a/reserve/request.pl b/reserve/request.pl index c5e354aa7d2..89c28d1e1dd 100755 --- a/reserve/request.pl +++ b/reserve/request.pl @@ -44,6 +44,7 @@ use C4::Search qw( enabled_staff_search_views ); use Koha::Biblios; use Koha::Checkouts; use Koha::Holds; +use Koha::Hold; use Koha::CirculationRules; use Koha::Items; use Koha::ItemTypes; @@ -90,6 +91,7 @@ my $exceeded_maxreserves; my $exceeded_holds_per_record; my @failed_holds = $input->multi_param('failed_holds'); my $form_submitted = $input->param('form_submitted'); +my @biblionumbers = $input->multi_param('biblionumber'); my $op = $input->param('op') || q{}; @@ -109,6 +111,60 @@ if ( $op eq 'cud-move' ) { $next_priority, $first_priority, $last_priority ); } +} elsif ( $op eq 'cud-move_hold_item' or $op eq 'cud-move_hold_biblio' ) { + my @hold_ids = $input->multi_param('hold_id'); + my $original_biblionumber = $input->param('original_biblionumber'); + my @moving_holds = Koha::Holds->search( + { reserve_id => \@hold_ids }, + { order_by => { -asc => 'priority' } } + )->as_list; + + my $new_biblionumber = $input->param('new_biblionumber'); + my $target_biblio = Koha::Biblios->find($new_biblionumber); + + my %args = ( + new_biblionumber => $new_biblionumber, + ); + + # Only pass new_itemnumber if this is an item level move + if ( $op eq 'cud-move_hold_item' ) { + $args{new_itemnumber} = $input->param('new_itemnumber'); + } + + my @success_messages; + my @error_messages; + + foreach my $hold (@moving_holds) { + my $original_biblio = Koha::Biblios->find( $hold->biblionumber ); + + my $result = $hold->move_hold( \%args ); + + if ( $result->{success} ) { + push @success_messages, { + hold_id => $hold->id, + success => 1, + original_biblio => $original_biblio, + target_biblio => $target_biblio, + }; + } else { + push @error_messages, { + hold_id => $hold->id, + success => 0, + original_biblio => $original_biblio, + target_biblio => $target_biblio, + error => $result->{error}, + }; + } + } + + #Fix the priority on the original record + C4::Reserves::_FixPriority( { biblionumber => $original_biblionumber } ); + + $template->param( + hold_move_successes => \@success_messages, + hold_move_failures => \@error_messages, + ); + } elsif ( $op eq 'cud-cancel' ) { my $reserve_id = $input->param('reserve_id'); my $cancellation_reason = $input->param("cancellation-reason"); @@ -172,8 +228,6 @@ if ($form_submitted) { } } -my @biblionumbers = $input->multi_param('biblionumber'); - my $multi_hold = @biblionumbers > 1; $template->param( multi_hold => $multi_hold, -- 2.39.5