Bug 41705: Use PRG pattern for POS receipt printing to avoid popup blockers

The previous approach triggered popup blockers because the receipt window
was opened via JavaScript after a page reload — browsers treat this as a
non-user-initiated action. This patch reverses the flow: when
FinePaymentAutoPopup is enabled, the payment form submits directly to a
new tab via target="_blank" (a user-initiated action that browsers allow),
the server processes the payment and redirects (303) to the receipt page,
which auto-prints and closes itself, returning focus to the POS tab.

When FinePaymentAutoPopup is disabled, the server redirects back to the
POS page with payment details in the URL (standard PRG), displaying the
existing success banner with print/email receipt buttons.

Changes:
- pos/pay.pl: Implement PRG redirect for both popup and non-popup flows,
  URI-encode redirect parameters, remove session-based state management
- pos/pay.tt: Add target="_blank" when FinePaymentAutoPopup is enabled,
  reset form state after submission via same-scope setTimeout
- pos/printreceipt.pl: Add error handling for missing receipts, pass
  autoprint parameter to template
- pos/printreceipt.tt: Add conditional auto-print, manual print/close
  buttons, error display, and IntranetSlipPrinterJS preference support
- slip-print.inc: Modernize jQuery to vanilla JS, remove obsolete
  Chrome setTimeout hack

Test plan:
1. Enable FinePaymentAutoPopup system preference
2. Go to Point of Sale, add items, and complete a sale
3. Verify receipt opens in a new tab without popup blocker warnings
4. Verify receipt auto-prints and tab closes after printing
5. Verify original POS tab form is reset for next transaction
6. Disable FinePaymentAutoPopup
7. Complete another sale
8. Verify POS page shows success with Print/Email receipt buttons
9. Test receipt printing from other pages (circulation, fees) to
   confirm slip-print.inc change causes no regressions

Sponsored-by: OpenFifth <https://openfifth.co.uk/>
Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Andrew Fuerste Henry <andrew@bywatersolutions.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
(cherry picked from commit a5a1eeaa5b)
Signed-off-by: Baptiste Wojtkowski <baptiste.wojtkowski@biblibre.com>
This commit is contained in:
Martin Renvoize 2026-04-14 16:18:07 +01:00 committed by Baptiste Wojtkowski
parent b378ee94cb
commit 3f5ede8832
No known key found for this signature in database
GPG Key ID: 47A8A13E10BC0D40
5 changed files with 93 additions and 31 deletions

View File

@ -6,12 +6,11 @@
[% Koha.Preference('IntranetSlipPrinterJS') | $raw %]
[% ELSE %]
let autoprint = true;
$( window ).load(function() {
window.addEventListener('load', function() {
window.print();
window.onafterprint = function () {
window.close();
}
setTimeout('window.close()', 1000); //Hack from Chrome < 63
});
[% END %]
</script>

View File

@ -59,7 +59,7 @@
</div>
[% END %]
<form name="payForm" id="payForm" method="post" action="/cgi-bin/koha/pos/pay.pl">
<form name="payForm" id="payForm" method="post" action="/cgi-bin/koha/pos/pay.pl" [% IF Koha.Preference('FinePaymentAutoPopup') %]target="_blank"[% END %]>
[% INCLUDE 'csrf-token.inc' %]
<input type="hidden" name="op" value="cud-pay" />
<div class="row">
@ -240,11 +240,6 @@
</div>
</div>
[% IF payment_id && Koha.Preference('FinePaymentAutoPopup') %]
<!-- Automatic Print Receipt -->
<a id="printReceipt" style="display: none" href="#"></a>
[% END %]
[% MACRO jsinclude BLOCK %]
[% INCLUDE 'format_price.inc' %]
[% INCLUDE 'datatables.inc' %]
@ -515,17 +510,22 @@
value: JSON.stringify(sale)
}).appendTo('#payForm');
});
[% IF Koha.Preference('FinePaymentAutoPopup') %]
// Form submits to new tab; reset this page for the next transaction
setTimeout(function() {
sale_kohaTable.api().clear().draw();
$('#tendered').val('');
$('#paid').val(zero_formatted);
$('#change').html(zero_formatted);
$(':input[name="change"]').val(zero_formatted);
checked = false;
}, 0);
[% END %]
return true;
}
});
[% IF payment_id && Koha.Preference('FinePaymentAutoPopup') %]
$("#printReceipt").click(function() {
var win = window.open('/cgi-bin/koha/pos/printreceipt.pl?action=print&accountlines_id=[% payment_id | uri %]&tendered=[% tendered | uri %]&change=[% change | uri %]', '_blank');
win.focus();
});
$("#printReceipt").click();
[% END %]
});
</script>
[% END %]

View File

@ -38,8 +38,41 @@
[% END %]
</div>
[% IF error_msg %]
<div class="alert alert-danger">
<p>[% error_msg | html %]</p>
<button id="close-window" class="btn btn-secondary" onclick="window.close();"> [% t("Close") | html %] </button>
</div>
[% ELSE %]
<div class="no-print" style="margin-top: 20px; text-align: center;">
<button id="print-receipt" class="btn btn-primary"> <i class="fa fa-print"></i> [% t("Print receipt") | html %] </button>
<button id="close-window" class="btn btn-secondary" onclick="window.close();"> [% t("Close") | html %] </button>
</div>
[% END %]
[% MACRO jsinclude BLOCK %]
[% INCLUDE 'slip-print.inc' #printThenClose %]
<script nonce="[% Koha.CSPNonce | $raw %]">
[% IF ( Koha.Preference('IntranetSlipPrinterJS') ) %]
[% Koha.Preference('IntranetSlipPrinterJS') | $raw %]
[% ELSE %]
function doPrint() {
window.print();
}
window.onafterprint = () => {
window.close();
};
window.addEventListener('load', () => {
if ( [% autoprint | html %] == 1 ) {
doPrint();
}
});
document.getElementById('print-receipt')
.addEventListener('click', doPrint);
[% END %]
</script>
[% END %]
[% INCLUDE 'intranet-bottom.inc' %]

View File

@ -20,9 +20,10 @@
use Modern::Perl;
use CGI;
use JSON qw( from_json );
use JSON qw( from_json );
use URI::Escape qw( uri_escape_utf8 );
use C4::Auth qw( get_session get_template_and_user );
use C4::Auth qw( get_template_and_user );
use C4::Context;
use C4::Letters qw( GetPreparedLetter EnqueueLetter SendQueuedMessages );
use C4::Output qw( output_html_with_http_headers );
@ -34,9 +35,7 @@ use Koha::Charges::Sales;
use Koha::Database;
use Koha::Libraries;
my $input = CGI->new();
my $sessionID = $input->cookie('CGISESSID');
my $session = get_session($sessionID);
my $input = CGI->new();
my ( $template, $loggedinuser, $cookie, $user_flags ) = get_template_and_user(
{
@ -52,6 +51,15 @@ my $library_id = C4::Context->userenv->{'branch'};
my $registerid = $input->param('registerid');
my $op = $input->param('op') || '';
# PRG flow: pick up payment details from redirect URL params
if ( !$op && $input->param('payment_id') ) {
$template->param(
payment_id => scalar $input->param('payment_id'),
tendered => scalar $input->param('tendered'),
change => scalar $input->param('change'),
);
}
my $invoice_types = Koha::Account::DebitTypes->search_with_library_limits(
{ can_be_sold => 1, archived => 0 },
{}, $library_id
@ -78,11 +86,24 @@ if ( $op eq 'cud-pay' ) {
my $payment = $sale->purchase( { payment_type => $payment_type } );
$template->param(
payment_id => $payment->accountlines_id,
tendered => scalar $input->param('tendered'),
change => scalar $input->param('change')
);
my $payment_id = $payment->accountlines_id;
my $tendered = uri_escape_utf8( scalar $input->param('tendered') );
my $change = uri_escape_utf8( scalar $input->param('change') );
my $redirect_url;
if ( C4::Context->preference('FinePaymentAutoPopup') ) {
$redirect_url =
"/cgi-bin/koha/pos/printreceipt.pl"
. "?accountlines_id=$payment_id"
. "&tendered=$tendered"
. "&change=$change"
. "&autoprint=1";
} else {
$redirect_url =
"/cgi-bin/koha/pos/pay.pl" . "?payment_id=$payment_id" . "&tendered=$tendered" . "&change=$change";
}
print $input->redirect( -uri => $redirect_url, -status => 303 );
exit;
}
}

View File

@ -37,7 +37,15 @@ my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
my $payment_id = $input->param('accountlines_id');
my $accountline = Koha::Account::Lines->find($payment_id);
my $patron = $accountline->patron;
if ( !$accountline ) {
$template->param(
error_msg => 'Receipt not found or access denied.',
);
output_html_with_http_headers $input, $cookie, $template->output;
exit;
}
my $patron = $accountline->patron;
my $autoprint = $input->param('autoprint') // 0;
my $logged_in_user = Koha::Patrons->find($loggedinuser) or die "Not logged in";
output_and_exit_if_error(
@ -83,9 +91,10 @@ my $letter = C4::Letters::GetPreparedLetter(
);
$template->param(
slip => $letter->{content},
plain => !$letter->{is_html},
style => $letter->{style},
slip => $letter->{content},
plain => !$letter->{is_html},
style => $letter->{style},
autoprint => $autoprint,
);
output_html_with_http_headers $input, $cookie, $template->output;