From 3f01282d2923e37e0104a54f98a1fee86a62002b Mon Sep 17 00:00:00 2001 From: Martin Renvoize Date: Sat, 24 Jan 2026 15:36:18 +0000 Subject: [PATCH] Bug 41705: Upgrade POS receipt printing to use PRG pattern This commit implements a POST-Redirect-GET (PRG) pattern for the Point of Sale. We also reverse the receipt printing workflow to make it popup-safe when using FinePaymentAutoPopup. - When FinePaymentAutoPopup is enabled, the POS form submits to a new tab - The server processes the payment and redirects to the receipt print page - The receipt page auto-prints and attempts to close itself - When FinePaymentAutoPopup is disabled, the original in-page flow is preserved - Added error handling for missing receipts - Modernized JavaScript printing logic - Added manual print and close buttons to receipt page Test Plan: 1. Enable FinePaymentAutoPopup system preference 2. Go to Point of Sale page and complete a sale 3. Confirm the receipt opens in a new tab and auto-prints 4. Close the receipt tab and verify the POS page is ready for next transaction 5. Disable FinePaymentAutoPopup system preference 6. Complete another sale 7. Verify the POS page reloads with 'Print receipt' and 'Email receipt' buttons 8. Click 'Print receipt' and verify it opens in a new tab 9. Test error case: manually modify receipt URL and verify error message displays --- .../prog/css/printreceiptinvoice.css | 430 +++++++++--------- .../prog/en/includes/slip-print.inc | 3 +- .../intranet-tmpl/prog/en/modules/pos/pay.tt | 17 +- .../prog/en/modules/pos/printreceipt.tt | 38 +- pos/pay.pl | 23 +- pos/printreceipt.pl | 11 +- 6 files changed, 292 insertions(+), 230 deletions(-) diff --git a/koha-tmpl/intranet-tmpl/prog/css/printreceiptinvoice.css b/koha-tmpl/intranet-tmpl/prog/css/printreceiptinvoice.css index ab9d2549e1d..f2016845f19 100644 --- a/koha-tmpl/intranet-tmpl/prog/css/printreceiptinvoice.css +++ b/koha-tmpl/intranet-tmpl/prog/css/printreceiptinvoice.css @@ -1,322 +1,331 @@ a:link { - color : #000000; - text-decoration: none; + color: #000000; + text-decoration: none; } a:visited { - color : #000000; - text-decoration: none; + color: #000000; + text-decoration: none; } a:hover { - color : #993300; - text-decoration: none; + color: #993300; + text-decoration: none; } body { - background-color : #FFF; - color : #333333; - font-family : arial, geneva, sans-serif; - font-size : 14px; - margin : 0px 0px 0px 0px; + background-color: #fff; + color: #333333; + font-family: arial, geneva, sans-serif; + font-size: 14px; + margin: 0px 0px 0px 0px; } caption { - color : #000000; - font-size : 18px; - font-weight : bold; - margin-top : 5px; - text-align : left; + color: #000000; + font-size: 18px; + font-weight: bold; + margin-top: 5px; + text-align: left; } form { - margin : 0px; - padding : 0px; + margin: 0px; + padding: 0px; } -h1 - { - color : #000000; - font-size : 22px; - font-weight : bold; - margin-bottom : 3px; - margin-top : 3px; +h1 { + color: #000000; + font-size: 22px; + font-weight: bold; + margin-bottom: 3px; + margin-top: 3px; } h1#logo { - display: none; + display: none; } h2 { - color : #000000; - font-size : 20px; - font-weight : bold; - margin-bottom : 3px; - margin-top : 3px; + color: #000000; + font-size: 20px; + font-weight: bold; + margin-bottom: 3px; + margin-top: 3px; } h3 { - color : #000000; - font-size : 18px; - font-weight : bold; - margin-bottom : 3px; - margin-top : 3px; + color: #000000; + font-size: 18px; + font-weight: bold; + margin-bottom: 3px; + margin-top: 3px; } h4 { - color : #000000; - font-size : 16px; - font-weight : bold; - margin-bottom : 3px; - margin-top : 3px; + color: #000000; + font-size: 16px; + font-weight: bold; + margin-bottom: 3px; + margin-top: 3px; } h5 { - color : #000000; - font-size : 15px; - font-weight : bold; - margin-bottom : 1px; - margin-top : 1px; + color: #000000; + font-size: 15px; + font-weight: bold; + margin-bottom: 1px; + margin-top: 1px; } h6 { - color : #000000; - font-size : 14px; - font-weight : bold; - margin-bottom : 1px; - margin-top : 1px; + color: #000000; + font-size: 14px; + font-weight: bold; + margin-bottom: 1px; + margin-top: 1px; } table { - background-color : #FFFFFF; - border-bottom : 1px solid #CCCCCC; - border-collapse : collapse; - border-left : 1px solid #CCCCCC; - margin : 3px 0px 5px 0px; - padding : 0px; - width : 99%; + background-color: #ffffff; + border-bottom: 1px solid #cccccc; + border-collapse: collapse; + border-left: 1px solid #cccccc; + margin: 3px 0px 5px 0px; + padding: 0px; + width: 99%; } table.list { - background-color : #FFFFFF; - border : 0; - border-collapse : collapse; - margin : 3px 0px 5px 0px; - padding : 0px; - width : 99%; + background-color: #ffffff; + border: 0; + border-collapse: collapse; + margin: 3px 0px 5px 0px; + padding: 0px; + width: 99%; } td { - background-color : #FFF; - border-bottom : 1px solid #CCCCCC; - border-right : 1px solid #CCCCCC; - border-top : 0; - border-left : 0; - font-size : 12px; - padding : 5px 5px 5px 5px; + background-color: #fff; + border-bottom: 1px solid #cccccc; + border-right: 1px solid #cccccc; + border-top: 0; + border-left: 0; + font-size: 12px; + padding: 5px 5px 5px 5px; } td:last-child { - background-color : #FFF; - border-bottom : 1px solid #CCCCCC; - border-right : 0px solid #CCCCCC; - border-top : 0; - border-left : 0; - font-size : 12px; - padding : 5px 5px 5px 5px; + background-color: #fff; + border-bottom: 1px solid #cccccc; + border-right: 0px solid #cccccc; + border-top: 0; + border-left: 0; + font-size: 12px; + padding: 5px 5px 5px 5px; } table.list th { - background-color : #FFFFFF; - border-top : 0; - border-left : 0; - text-align: left; - font-size: 12px; - font-weight: bold; - padding: 5px; - width : 25%; + background-color: #ffffff; + border-top: 0; + border-left: 0; + text-align: left; + font-size: 12px; + font-weight: bold; + padding: 5px; + width: 25%; } th { - background-color : #E9E9E9; - border-bottom : 1px solid #CCCCCC; - border-right : 1px solid #CCCCCC; - border-top : 0; - border-left : 0; - font-size : 14px; - font-weight : bold; - padding : 5px 5px 5px 5px; + background-color: #e9e9e9; + border-bottom: 1px solid #cccccc; + border-right: 1px solid #cccccc; + border-top: 0; + border-left: 0; + font-size: 14px; + font-weight: bold; + padding: 5px 5px 5px 5px; } th:last-child { - background-color : #E9E9E9; - border-bottom : 1px solid #CCCCCC; - border-right : 0px solid #CCCCCC; - border-top : 0; - border-left : 0; - font-size : 14px; - font-weight : bold; - padding : 5px 5px 5px 5px; -} - -td.input, div.form td, div.details td { - border-bottom : 1px solid #CCCCCC; - border-left : 1px solid #CCCCCC; - border-right : 1px solid #CCCCCC; - border-top : 1px solid #CCCCCC; - color : #000000; - line-height : 1.5em; - padding : 4px; - text-align : left; -} - -td.label, div.form th, div.details th { - background-color : #FFF; - border-bottom : 1px solid #CCCCCC; - border-left : 1px solid #CCCCCC; - border-right : 0px; - border-top : 0px; - color : #000088; - font-weight : bold; - line-height : 1.5em; - padding : 4px; - text-align : right; - vertical-align : top; + background-color: #e9e9e9; + border-bottom: 1px solid #cccccc; + border-right: 0px solid #cccccc; + border-top: 0; + border-left: 0; + font-size: 14px; + font-weight: bold; + padding: 5px 5px 5px 5px; +} + +td.input, +div.form td, +div.details td { + border-bottom: 1px solid #cccccc; + border-left: 1px solid #cccccc; + border-right: 1px solid #cccccc; + border-top: 1px solid #cccccc; + color: #000000; + line-height: 1.5em; + padding: 4px; + text-align: left; +} + +td.label, +div.form th, +div.details th { + background-color: #fff; + border-bottom: 1px solid #cccccc; + border-left: 1px solid #cccccc; + border-right: 0px; + border-top: 0px; + color: #000088; + font-weight: bold; + line-height: 1.5em; + padding: 4px; + text-align: right; + vertical-align: top; } tr.highlight { - background-color: #CCCCCC; - border-bottom : 1px solid #CCCCCC; - border-left : 1px solid #CCCCCC; - border-right : 1px solid #CCCCCC; - border-top : 1px solid #CCCCCC; + background-color: #cccccc; + border-bottom: 1px solid #cccccc; + border-left: 1px solid #cccccc; + border-right: 1px solid #cccccc; + border-top: 1px solid #cccccc; } .error { - font-weight: bold; + font-weight: bold; } .ex { - font-family : "Courier New", Courier, monospace; + font-family: "Courier New", Courier, monospace; } .inline { - display : inline; + display: inline; } tr.overdue { - font-weight : bold; + font-weight: bold; } tfoot { - font-weight : bold; + font-weight: bold; } -tfoot td, tfoot td:last-child { - background-color : #EEEEEE; - border-right : 1px solid #666666; - border-bottom : 1px solid #666666; +tfoot td, +tfoot td:last-child { + background-color: #eeeeee; + border-right: 1px solid #666666; + border-bottom: 1px solid #666666; } -td.credit, td.debit, td.sum { - font-family : "Courier New", Courier, monospace; - text-align : right; - border-bottom : 1px solid #CCCCCC; - border-left : 1px solid #CCCCCC; - border-right : 1px solid #CCCCCC; - border-top : 1px solid #CCCCCC; +td.credit, +td.debit, +td.sum { + font-family: "Courier New", Courier, monospace; + text-align: right; + border-bottom: 1px solid #cccccc; + border-left: 1px solid #cccccc; + border-right: 1px solid #cccccc; + border-top: 1px solid #cccccc; } td.debit { - color : #990000; - border-bottom : 1px solid #CCCCCC; - border-left : 1px solid #CCCCCC; - border-right : 1px solid #CCCCCC; - border-top : 1px solid #CCCCCC; + color: #990000; + border-bottom: 1px solid #cccccc; + border-left: 1px solid #cccccc; + border-right: 1px solid #cccccc; + border-top: 1px solid #cccccc; } #receipt { - background-color : #FFFFFF; - font-family : serif; - font-size : .9em; - margin : 0 0px 0px 0; - padding : 0px; - position : relative; + background-color: #ffffff; + font-family: serif; + font-size: 0.9em; + margin: 0 0px 0px 0; + padding: 0px; + position: relative; } #receipt a:link { - color : #000000; - text-decoration: none; + color: #000000; + text-decoration: none; } #receipt a:visited { - color : #000000; - text-decoration: none; + color: #000000; + text-decoration: none; } #receipt a:hover { - color : #993300; - text-decoration: none; + color: #993300; + text-decoration: none; } #receipt caption { - color : #000000; - font-weight : bold; - margin-top : 5px; - text-align : left; + color: #000000; + font-weight: bold; + margin-top: 5px; + text-align: left; } #receipt div.table { - background-color : transparent; - font-weight: normal; + background-color: transparent; + font-weight: normal; } -#receipt h3, #receipt caption, #receipt h4 { - background-color : transparent; - color : #000000; - font-size : 1em; - font-weight : bold; - margin-bottom : 3px; - margin-top : 3px; +#receipt h3, +#receipt caption, +#receipt h4 { + background-color: transparent; + color: #000000; + font-size: 1em; + font-weight: bold; + margin-bottom: 3px; + margin-top: 3px; } #receipt h2 { - background-color : transparent; - color : #000000; - font-weight : bold; - margin-bottom : 3px; - margin-top : 3px; + background-color: transparent; + color: #000000; + font-weight: bold; + margin-bottom: 3px; + margin-top: 3px; } #receipt table { - background-color : transparent; - border : 0; - border-collapse : collapse; - width : auto; -} - -#receipt td, #receipt th { - background-color : transparent; - border : 0; - font-size : 1em; - font-weight : normal; - padding : 1px; - border-bottom : 1px solid #000000; - border-left : 1px solid #000000; - border-right : 1px solid #000000; - border-top : 1px solid #000000; - } + background-color: transparent; + border: 0; + border-collapse: collapse; + width: auto; +} + +#receipt td, +#receipt th { + background-color: transparent; + border: 0; + font-size: 1em; + font-weight: normal; + padding: 1px; + border-bottom: 1px solid #000000; + border-left: 1px solid #000000; + border-right: 1px solid #000000; + border-top: 1px solid #000000; +} #receipt th { - font-weight : bold; - text-align : left; - } + font-weight: bold; + text-align: left; +} #receipt th.centerednames { - font-weight : bold; - text-align: center; - color: red; - border-top : 1px solid #000000; - border-bottom : 0px; + font-weight: bold; + text-align: center; + color: red; + border-top: 1px solid #000000; + border-bottom: 0px; } div#breadcrumbs, @@ -327,6 +336,7 @@ div#toolbar, div#changelanguage, div#menu, .noprint, +.no-print, form#sortbyform, #cartDetails, fieldset.action, @@ -336,8 +346,8 @@ fieldset.action, } fieldset { - border : 0; + border: 0; } #cataloguing_additem_itemlist { - overflow : visible !important; + overflow: visible !important; } diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/slip-print.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/slip-print.inc index 4a39a40a1d2..c6e354db112 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/slip-print.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/slip-print.inc @@ -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 %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/pay.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/pos/pay.tt index f5cb9b2dd03..d5e02ccdf09 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/pay.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/pos/pay.tt @@ -59,7 +59,11 @@ [% END %] + [% IF Koha.Preference('FinePaymentAutoPopup') %] +
+ [% ELSE %] + [% END %] [% INCLUDE 'csrf-token.inc' %]
@@ -240,10 +244,7 @@
-[% IF payment_id && Koha.Preference('FinePaymentAutoPopup') %] - - -[% END %] + [% MACRO jsinclude BLOCK %] [% INCLUDE 'format_price.inc' %] @@ -519,13 +520,7 @@ } }); - [% 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 %] + }); [% END %] diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/printreceipt.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/pos/printreceipt.tt index 53c8b56a851..a2be9ba6b3c 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/pos/printreceipt.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/pos/printreceipt.tt @@ -38,8 +38,44 @@ [% END %] +[% IF error_msg %] +
+

[% error_msg | html %]

+ +
+[% ELSE %] +
+ + +
+[% END %] + [% MACRO jsinclude BLOCK %] - [% INCLUDE 'slip-print.inc' #printThenClose %] + [% END %] [% INCLUDE 'intranet-bottom.inc' %] diff --git a/pos/pay.pl b/pos/pay.pl index 73070c9d44e..069983e2f99 100755 --- a/pos/pay.pl +++ b/pos/pay.pl @@ -78,11 +78,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 $redirect_url; + if ( C4::Context->preference('FinePaymentAutoPopup') ) { + # PRG Flow: Redirect to receipt print page + $redirect_url = C4::Context->uri_for("/cgi-bin/koha/pos/printreceipt.pl") + . "?accountlines_id=" . $payment->accountlines_id + . "&tendered=" . scalar $input->param('tendered') + . "&change=" . scalar $input->param('change') + . "&autoprint=1"; + + } else { + # PRG Flow: Redirect to pos with payment details + $redirect_url = C4::Context->uri_for("/cgi-bin/koha/pos/pay.pl") + . '?payment_id=' . $payment->accountlines_id + . '&tendered=' . scalar $input->param('tendered') + . '&change=' . scalar $input->param('change'); + } + print $input->redirect( -uri => $redirect_url, -status => 303 ); + exit; # Terminate the script immediately after redirect headers are sent } } diff --git a/pos/printreceipt.pl b/pos/printreceipt.pl index 321e3db6040..78b48ab01eb 100755 --- a/pos/printreceipt.pl +++ b/pos/printreceipt.pl @@ -37,7 +37,15 @@ my ( $template, $loggedinuser, $cookie ) = get_template_and_user( my $payment_id = $input->param('accountlines_id'); my $payment = Koha::Account::Lines->find($payment_id); -my $patron = $payment->patron; +if ( !$payment ) { + $template->param( + error_msg => 'Receipt not found or access denied.', + ); + output_html_with_http_headers $input, $cookie, $template->output; + exit; +} +my $patron = $payment->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( @@ -71,6 +79,7 @@ $template->param( slip => $letter->{content}, plain => !$letter->{is_html}, style => $letter->{style}, + autoprint => $autoprint, ); output_html_with_http_headers $input, $cookie, $template->output; -- 2.52.0