From 24445cd6e7efd5a2fc532089427f6e98ea15ef78 Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Thu, 11 Sep 2025 14:24:18 +0100 Subject: [PATCH] Bug 19814: Add batch return functionality for RFID workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This proof-of-concept adds batch return capability to the circulation returns page, designed primarily for RFID pad workflows while maintaining backward compatibility for single scan returns. Key Features: - Automatic RFID batch detection and processing with debouncing - Manual batch mode toggle in checkin settings - Single textarea element that adapts from single-line to multi-line - Batch results display with per-item status and messages Technical Implementation: - Backend auto-detects batch mode via newline presence in barcode input - Debouncing (500ms) prevents premature form submission during scanning for multi-line input - Textarea styling dynamically switches between single-line and multi-line modes - Batch processing reuses existing AddReturn() logic for consistency - All existing circulation rules, messages, and workflows preserved RFID Workflow: 1. RFID pad scans multiple items → deposits newline-delimited barcodes 2. Interface auto-expands to show all scanned items 3. Automatic submission after 0.5s delay 4. Batch results display with status for each item Manual Workflow: 1. Enable "batch mode" in circulation settings 2. Input field expands to textarea for multiple barcodes 3. Submit to see comprehensive batch results This is a proof-of-concept seeking user feedback before full development and refinement. --- circ/returns.pl | 101 ++++++++++- .../prog/en/modules/circ/returns.tt | 157 +++++++++++++++++- 2 files changed, 255 insertions(+), 3 deletions(-) diff --git a/circ/returns.pl b/circ/returns.pl index 29b2d2f7bf7..77eafd22e54 100755 --- a/circ/returns.pl +++ b/circ/returns.pl @@ -211,7 +211,16 @@ my $borrower; my $returned = 0; my $messages; my $issue; -my $barcode = $query->param('barcode'); +my $barcode_input = $query->param('barcode'); +my $batch_barcodes = $query->param('batch_barcodes'); + +# Check if barcode input contains multiple lines (batch mode) +my $is_batch_mode = $barcode_input && $barcode_input =~ /\n/; +if ($is_batch_mode) { + $batch_barcodes = $barcode_input; + $barcode_input = undef; # Clear single barcode when in batch mode +} +my $barcode = $barcode_input; my $exemptfine = $query->param('exemptfine'); if ( $exemptfine && !C4::Auth::haspermission( C4::Context->userenv->{'id'}, { 'updatecharges' => 'writeoff' } ) ) @@ -291,6 +300,96 @@ if ( $transit && $op eq 'cud-transfer' ) { # actually return book and prepare item table..... my $returnbranch; + +# Handle batch returns +my @batch_results; +if ( $batch_barcodes && $op eq 'cud-checkin' ) { + my @barcodes = split /\s*\n\s*/, $batch_barcodes; + @barcodes = grep { $_ && $_ !~ /^\s*$/ } @barcodes; # Remove empty lines + + foreach my $batch_barcode (@barcodes) { + $batch_barcode = barcodedecode($batch_barcode) if $batch_barcode; + next unless $batch_barcode; + + my $batch_item = Koha::Items->find( { barcode => $batch_barcode } ); + my %batch_result = ( + barcode => $batch_barcode, + success => 0, + messages => {}, + borrower => undef, + item => $batch_item + ); + + if ($batch_item) { + my $batch_itemnumber = $batch_item->itemnumber; + + # Check if we should display a checkin message + my $itemtype = Koha::ItemTypes->find( $batch_item->effective_itemtype ); + if ( $itemtype && $itemtype->checkinmsg ) { + $batch_result{checkinmsg} = $itemtype->checkinmsg; + $batch_result{checkinmsgtype} = $itemtype->checkinmsgtype; + } + + # Process the return date + my $return_date = $dropboxmode ? $dropboxdate : dt_from_string($return_date_override); + + # Check for multi-part confirmation requirement + my $needs_confirm = + C4::Context->preference("CircConfirmItemParts") + && $batch_item->materials + && !$query->param('multiple_confirm'); + + # Check for bundle confirmation requirement + my $bundle_confirm = $batch_item->is_bundle + && !$query->param('confirm_items_bundle_return'); + + # Process waiting holds cancellation requests + if ($batch_item) { + my $waiting_holds_to_be_cancelled = $batch_item->holds->waiting->filter_by_has_cancellation_requests; + while ( my $hold = $waiting_holds_to_be_cancelled->next ) { + $hold->cancel; + } + } + + # Do the actual return unless confirmation needed + unless ( $needs_confirm || $bundle_confirm ) { + my ( $batch_returned, $batch_messages, $batch_issue, $batch_borrower ) = + AddReturn( $batch_barcode, $userenv_branch, $exemptfine, $return_date ); + + $batch_result{success} = $batch_returned; + $batch_result{messages} = $batch_messages; + $batch_result{issue} = $batch_issue; + $batch_result{borrower} = $batch_borrower; + + if ($batch_returned) { + unshift @checkins, { + barcode => $batch_barcode, + duedate => $batch_issue ? $batch_issue->date_due : undef, + borrowernumber => $batch_issue ? $batch_issue->borrowernumber : undef, + not_returned => 0, + }; + } elsif ( C4::Context->preference('ShowAllCheckins') && !$batch_messages->{'BadBarcode'} ) { + unshift @checkins, { + barcode => $batch_barcode, + duedate => $batch_issue ? $batch_issue->date_due : undef, + borrowernumber => $batch_issue ? $batch_issue->borrowernumber : undef, + not_returned => 1, + }; + } + } else { + $batch_result{needs_confirm} = $needs_confirm; + $batch_result{bundle_confirm} = $bundle_confirm; + } + } else { + $batch_result{messages}{'BadBarcode'} = $batch_barcode; + } + + push @batch_results, \%batch_result; + } + + $template->param( batch_results => \@batch_results ); +} + if ( $barcode && ( $op eq 'cud-checkin' || $op eq 'cud-affect_reserve' ) ) { $barcode = barcodedecode($barcode) if $barcode; my $item = Koha::Items->find( { barcode => $barcode } ); 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 fec7616ac69..ba9762c8efd 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt @@ -1155,9 +1155,16 @@
[% IF ( exemptfine || dropboxmode ) %] - + [% ELSE %] - + [% END %] @@ -1245,6 +1252,11 @@
+
+ + +
+ [% IF Koha.Preference('ExpireReservesMaxPickUpDelayCharge') %]
[% IF ( forgivemanualholdsexpire ) %] @@ -1263,6 +1275,73 @@ + [% IF ( batch_results.size ) %] +
+

Batch return results

+ + + + + + + + + + + + [% FOREACH result IN batch_results %] + + + + + + + + [% END %] + +
BarcodeStatusTitlePatronMessages
[% result.barcode | html %] + [% IF result.success %] + Returned + [% ELSIF result.needs_confirm %] + Needs confirmation + [% ELSIF result.bundle_confirm %] + Bundle confirmation required + [% ELSIF result.messages.BadBarcode %] + Invalid barcode + [% ELSE %] + Failed + [% END %] + + [% IF result.item %] + [% result.item.biblio.title | html %] + [% END %] + + [% IF result.borrower %] + [% result.borrower.firstname | html %] [% result.borrower.surname | html %] + [% END %] + + [% IF result.checkinmsg %] +
[% result.checkinmsg | html %]
+ [% END %] + [% FOREACH code IN result.messages.keys %] + [% IF code == 'WasTransfered' %] +
Item transferred to [% result.messages.TransferTo %]
+ [% ELSIF code == 'NeedsTransfer' %] +
Item needs transfer
+ [% ELSIF code == 'NotIssued' %] +
Item was not checked out
+ [% ELSIF code == 'WasLost' %] +
Item was lost, now found
+ [% ELSIF code == 'withdrawn' %] +
Item is withdrawn
+ [% ELSIF code == 'Debarred' %] +
Patron is restricted
+ [% END %] + [% END %] +
+
+ [% END %] + [% IF ( checkins.size ) %]

Checked-in items

@@ -1736,6 +1815,80 @@ ], }); + // Debounce variables for RFID batch input + let debounceTimer; + let isReceivingRFIDInput = false; + + // Batch mode toggle functionality + $('#batch-mode-toggle').on('change', function() { + const $barcodeTextarea = $('#barcode'); + + if ($(this).is(':checked')) { + // Switch to multi-line batch mode + $barcodeTextarea.attr({ + 'placeholder': 'Enter barcodes, one per line', + 'rows': 8 + }).css({ + 'height': 'auto', + 'overflow': 'auto', + 'white-space': 'pre-wrap', + 'resize': 'vertical' + }); + } else { + // Switch back to single-line mode + $barcodeTextarea.attr({ + 'placeholder': 'Enter item barcode', + 'rows': 1 + }).css({ + 'height': '2.2em', + 'overflow': 'hidden', + 'white-space': 'nowrap', + 'resize': 'none' + }); + } + $barcodeTextarea.focus(); + }); + + // Handle RFID input with debouncing + $('#barcode').on('input paste', function(e) { + const currentValue = $(this).val(); + + // Clear any existing timer + clearTimeout(debounceTimer); + + // Check if this looks like RFID batch input (contains newlines) + if (currentValue.includes('\n')) { + isReceivingRFIDInput = true; + + // Auto-enable batch mode if not already enabled + if (!$('#batch-mode-toggle').is(':checked')) { + $('#batch-mode-toggle').prop('checked', true).trigger('change'); + } + + // Set a debounce timer to submit after RFID scanning finishes + debounceTimer = setTimeout(function() { + isReceivingRFIDInput = false; + $('#checkin-form').submit(); + }, 500); // 500ms delay after last input + } + }); + + // Prevent immediate form submission on Enter if receiving RFID input + $('#barcode').on('keydown', function(e) { + if (e.which === 13 && isReceivingRFIDInput) { + e.preventDefault(); + return false; + } + }); + + // Prevent form submission during RFID input + $('#checkin-form').on('submit', function(e) { + if (isReceivingRFIDInput) { + e.preventDefault(); + return false; + } + }); + [% IF ( !(Koha.Preference('TransfersBlockCirc')) && Koha.Preference('AutomaticConfirmTransfer') ) %] $("#wrong-transfer-modal").on('hidden.bs.modal',function(){ $("#wrongtransferform").submit(); -- 2.51.0