From f687d1093e468d1f1e07228fd83e58a8b8eff652 Mon Sep 17 00:00:00 2001 From: MohamedAliSmk Date: Mon, 25 May 2026 15:01:51 +0300 Subject: [PATCH 1/3] fix(eod): enhance EOD report generation and handle empty item cases - Updated the EOD report print format for improved styling and layout. - Added a check in the item fetching function to return an empty list if no parent targets are found, preventing potential errors during report generation. Co-authored-by: Cursor --- .../print_format/pos_next_eod_report/pos_next_eod_report.json | 2 +- pos_next/pos_next/utils/pos_closing_print.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pos_next/pos_next/print_format/pos_next_eod_report/pos_next_eod_report.json b/pos_next/pos_next/print_format/pos_next_eod_report/pos_next_eod_report.json index adb0d0dae..563ebad7a 100644 --- a/pos_next/pos_next/print_format/pos_next_eod_report/pos_next_eod_report.json +++ b/pos_next/pos_next/print_format/pos_next_eod_report/pos_next_eod_report.json @@ -8,7 +8,7 @@ "docstatus": 0, "doctype": "Print Format", "font_size": 11, - "html": "\n\n{%- set items_sold = get_items_sold(doc) -%}\n{%- set taxes = doc.taxes or [] -%}\n{%- set payments = doc.payment_reconciliation or [] -%}\n{%- set tax_total = taxes | sum(attribute='amount') -%}\n{%- set total_expected = payments | sum(attribute='expected_amount') -%}\n{%- set total_counted = payments | sum(attribute='closing_amount') -%}\n{%- set summary_diff = total_counted - total_expected -%}\n{%- set profile_address = frappe.db.get_value('POS Profile', doc.pos_profile, 'address') -%}\n\n
\n\t
END-OF-DAY REPORT
\n\t
{{ doc.company or 'POS Next' }}
\n\t{%- if profile_address -%}\n\t
{{ profile_address }}
\n\t{%- endif -%}\n\n\t
\n\t\t
Session
\n\t\t
Profile{{ doc.pos_profile }}
\n\t\t
Cashier{{ doc.user }}
\n\t\t
Opened{{ doc.period_start_date }}
\n\t\t
Closed{{ doc.period_end_date }}
\n\t\t
Closing Shift{{ doc.name }}
\n\t
\n\n\t
\n\t\t
Sales Totals
\n\t\t
Invoice Count{{ (doc.pos_transactions or []) | length }}
\n\t\t
Total Qty{{ '%.2f' | format(doc.total_quantity or 0) }}
\n\t\t
Net{{ '%.2f' | format(doc.net_total or 0) }}
\n\t\t
Tax{{ '%.2f' | format(tax_total or 0) }}
\n\t\t
Grand Total{{ '%.2f' | format(doc.grand_total or 0) }}
\n\t
\n\n\t
\n\t\t
Items Sold
\n\t\t{%- if items_sold -%}\n\t\t\t{%- for item in items_sold -%}\n\t\t\t
\n\t\t\t\t
{{ item.item_name or item.item_code }}
\n\t\t\t\t
Qty: {{ '%.2f' | format(item.qty or 0) }}{{ '%.2f' | format(item.amount or 0) }}
\n\t\t\t
\n\t\t\t{%- endfor -%}\n\t\t{%- else -%}\n\t\t\t
No item rows found
\n\t\t{%- endif -%}\n\t
\n\n\t
\n\t\t
Taxes
\n\t\t{%- if taxes -%}\n\t\t\t{%- for tax in taxes -%}\n\t\t\t
{{ tax.account_head }} ({{ '%.2f' | format(tax.rate or 0) }}%){{ '%.2f' | format(tax.amount or 0) }}
\n\t\t\t{%- endfor -%}\n\t\t{%- else -%}\n\t\t\t
No tax rows
\n\t\t{%- endif -%}\n\t
\n\n\t
\n\t\t
Payments
\n\t\t{%- if payments -%}\n\t\t\t{%- for payment in payments -%}\n\t\t\t
\n\t\t\t\t
{{ payment.mode_of_payment }}
\n\t\t\t\t
Opening{{ '%.2f' | format(payment.opening_amount or 0) }}
\n\t\t\t\t
Expected{{ '%.2f' | format(payment.expected_amount or 0) }}
\n\t\t\t\t
Counted{{ '%.2f' | format(payment.closing_amount or 0) }}
\n\t\t\t\t
Difference{{ '%.2f' | format(payment.difference or 0) }} {% if (payment.difference or 0) < 0 %}SHORT{% elif (payment.difference or 0) > 0 %}OVER{% endif %}
\n\t\t\t
\n\t\t\t{%- endfor -%}\n\t\t{%- else -%}\n\t\t\t
No payment rows
\n\t\t{%- endif -%}\n\t
\n\n\t
\n\t\t
Final Summary
\n\t\t
\n\t\t\t
Total Expected{{ '%.2f' | format(total_expected or 0) }}
\n\t\t\t
Total Counted{{ '%.2f' | format(total_counted or 0) }}
\n\t\t\t
Short / Over{{ '%.2f' | format(summary_diff or 0) }}
\n\t\t
\n\t
\n\n\t
\n\t\t
Printed at {{ frappe.utils.now_datetime() }}
\n\t\t
Printed by {{ frappe.session.user }}
\n\t\t
POS Next
\n\t
\n
", + "html": "\n\n{%- set company_doc = frappe.get_cached_doc(\"Company\", doc.company) -%}\n{%- set profile_doc = frappe.get_cached_doc(\"POS Profile\", doc.pos_profile) -%}\n{%- set user_doc = frappe.get_cached_doc(\"User\", doc.user) -%}\n\n{%- set items = get_items_sold(doc) -%}\n\n{% set total_expected = namespace(v=0.0) %}\n{% set total_counted = namespace(v=0.0) %}\n\n{% for p in doc.payment_reconciliation %}\n {% set total_expected.v = total_expected.v + (p.expected_amount or 0) %}\n {% set total_counted.v = total_counted.v + (p.closing_amount or 0) %}\n{% endfor %}\n\n{% set short_over = total_counted.v - total_expected.v %}\n\n
\n\n \n\n
\n End of Day Report\n
\n\n
\n
\n {{ company_doc.company_name }}\n
\n\n {% if profile_doc.company_address %}\n
\n {{ profile_doc.company_address }}\n
\n {% endif %}\n
\n\n \n\n
\n Session Information\n
\n\n
\n POS Profile\n {{ doc.pos_profile }}\n
\n\n
\n Cashier\n \n {{ user_doc.full_name or doc.user }}\n \n
\n\n
\n Opened\n \n {{ frappe.utils.format_datetime(doc.period_start_date) }}\n \n
\n\n
\n Closed\n \n {{ frappe.utils.format_datetime(doc.period_end_date) }}\n \n
\n\n
\n Closing ID\n {{ doc.name }}\n
\n\n \n\n
\n Sales Totals\n
\n\n
\n Invoices\n \n {{ doc.pos_transactions|length }}\n \n
\n\n
\n Items Sold\n \n {{ \"{:.2f}\".format(doc.total_quantity or 0) }}\n \n
\n\n
\n Net Total\n \n {{ \"{:,.2f}\".format(doc.net_total or 0) }}\n \n
\n\n {% set tax_total = doc.taxes|sum(attribute='amount') if doc.taxes else 0 %}\n\n
\n Tax Total\n \n {{ \"{:,.2f}\".format(tax_total) }}\n \n
\n\n
\n Grand Total\n \n {{ \"{:,.2f}\".format(doc.grand_total or 0) }}\n \n
\n\n \n\n {% if items %}\n\n
\n Items Sold\n
\n\n {% for it in items %}\n\n
\n\n
\n {{ it.item_name or it.item_code }}\n
\n\n
\n\n
\n Qty {{ \"{:g}\".format(it.qty | float) }}\n \u00d7\n {{ \"{:,.2f}\".format((it.amount / it.qty) if it.qty else 0) }}\n
\n\n
\n {{ \"{:,.2f}\".format(it.amount | float) }}\n
\n\n
\n\n
\n\n {% endfor %}\n\n {% endif %}\n\n \n\n {% if doc.taxes %}\n\n
\n Taxes\n
\n\n {% for t in doc.taxes %}\n\n
\n\n \n {{ t.account_head }}\n {% if t.rate %}\n ({{ \"{:g}\".format(t.rate | float) }}%)\n {% endif %}\n \n\n \n {{ \"{:,.2f}\".format(t.amount or 0) }}\n \n\n
\n\n {% endfor %}\n\n {% endif %}\n\n \n\n
\n Payments\n
\n\n {% for p in doc.payment_reconciliation %}\n\n {% set diff = (p.closing_amount or 0) - (p.expected_amount or 0) %}\n\n
\n\n
\n {{ p.mode_of_payment }}\n
\n\n
\n Opening\n \n {{ \"{:,.2f}\".format(p.opening_amount or 0) }}\n \n
\n\n
\n Expected\n \n {{ \"{:,.2f}\".format(p.expected_amount or 0) }}\n \n
\n\n
\n Counted\n \n {{ \"{:,.2f}\".format(p.closing_amount or 0) }}\n \n
\n\n
\n\n \n Difference\n \n\n \n\n {{ \"{:,.2f}\".format(diff) }}\n\n {% if diff < 0 %}\n \n SHORT\n \n {% elif diff > 0 %}\n \n OVER\n \n {% endif %}\n\n \n\n
\n\n
\n\n {% endfor %}\n\n \n\n
\n Final Summary\n
\n\n
\n Total Expected\n \n {{ \"{:,.2f}\".format(total_expected.v) }}\n \n
\n\n
\n Total Counted\n \n {{ \"{:,.2f}\".format(total_counted.v) }}\n \n
\n\n
\n\n \n Short / Over\n \n\n \n\n {{ \"{:,.2f}\".format(short_over) }}\n\n {% if short_over < 0 %}\n \n SHORT\n \n {% elif short_over > 0 %}\n \n OVER\n \n {% endif %}\n\n \n\n
\n\n \n\n
\n\n Printed:\n {{ frappe.utils.format_datetime(frappe.utils.now_datetime()) }}\n\n
\n\n by {{ frappe.session.user }}\n\n
\n *** END ***\n
\n\n
\n\n
", "line_breaks": 0, "margin_bottom": 0.0, "margin_left": 0.0, diff --git a/pos_next/pos_next/utils/pos_closing_print.py b/pos_next/pos_next/utils/pos_closing_print.py index 6759d05ad..f5be90b68 100644 --- a/pos_next/pos_next/utils/pos_closing_print.py +++ b/pos_next/pos_next/utils/pos_closing_print.py @@ -87,6 +87,9 @@ def _fetch_items_for_targets(parent_targets: set[tuple[str, str]]) -> list[dict] def get_items_sold(doc) -> list[dict]: closing_doc = _as_closing_doc(doc) parent_targets = _collect_parent_targets(closing_doc.get("pos_transactions")) + if not parent_targets: + return [] + items = _fetch_items_for_targets(parent_targets) return [ From 3ee4e2a8fff45c2901b9168217ffe770c633afe8 Mon Sep 17 00:00:00 2001 From: MohamedAliSmk Date: Mon, 25 May 2026 16:07:50 +0300 Subject: [PATCH 2/3] feat(translations): add "Print EOD Report" translations for Arabic, Indonesian, and Portuguese (Brazil) --- pos_next/translations/ar.csv | 1 + pos_next/translations/id.csv | 1 + pos_next/translations/pt-br.csv | 1 + 3 files changed, 3 insertions(+) diff --git a/pos_next/translations/ar.csv b/pos_next/translations/ar.csv index c99f44df1..b2d72273f 100644 --- a/pos_next/translations/ar.csv +++ b/pos_next/translations/ar.csv @@ -47,6 +47,7 @@ "Item removed from cart","تم الحذف من السلة","" "Open Shift","فتح وردية","" "Close Shift","إغلاق الوردية","" +"Print EOD Report","طباعة تقرير نهاية اليوم","" "Shift Opening","بداية الوردية","" "Shift Closing","نهاية الوردية","" "Opening Amount","عهدة الفتح","" diff --git a/pos_next/translations/id.csv b/pos_next/translations/id.csv index c6012d9bb..39ae1c4e5 100644 --- a/pos_next/translations/id.csv +++ b/pos_next/translations/id.csv @@ -46,6 +46,7 @@ "Item removed from cart","Dihapus dari keranjang","" "Open Shift","Buka Shift","" "Close Shift","Tutup Shift","" +"Print EOD Report","Cetak Laporan Akhir Hari","" "Shift Opening","Pembukaan Shift","" "Shift Closing","Penutupan Shift","" "Opening Amount","Jumlah Pembukaan Shift","" diff --git a/pos_next/translations/pt-br.csv b/pos_next/translations/pt-br.csv index de859ba11..683402896 100644 --- a/pos_next/translations/pt-br.csv +++ b/pos_next/translations/pt-br.csv @@ -44,6 +44,7 @@ "Item removed from cart","Item removido do carrinho","" "Open Shift","Abrir Turno","" "Close Shift","Fechar Turno","" +"Print EOD Report","Imprimir Relatório de Fim de Dia","" "Shift Opening","Abertura do Turno","" "Shift Closing","Fechamento do Turno","" "Opening Amount","Valor de Abertura","" From 59aee7a2ba60069b0d39e9c5be903ec18ff8601b Mon Sep 17 00:00:00 2001 From: MohamedAliSmk Date: Tue, 16 Jun 2026 14:47:20 +0300 Subject: [PATCH 3/3] test(eod): cover empty parent targets in get_items_sold (PN-67) Add regression test for shifts with transaction rows that resolve to no invoice parents. Links Jira PN-67 to PR #275. Co-authored-by: Cursor --- pos_next/pos_next/utils/tests/test_pos_closing_print.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pos_next/pos_next/utils/tests/test_pos_closing_print.py b/pos_next/pos_next/utils/tests/test_pos_closing_print.py index d5f1d0a96..2cc28dccb 100644 --- a/pos_next/pos_next/utils/tests/test_pos_closing_print.py +++ b/pos_next/pos_next/utils/tests/test_pos_closing_print.py @@ -75,3 +75,11 @@ def test_get_items_sold_returns_empty_when_no_transactions(self, mock_fetch): self.assertEqual(result, []) mock_fetch.assert_not_called() + + @patch("pos_next.pos_next.utils.pos_closing_print._fetch_items_for_targets") + def test_get_items_sold_returns_empty_when_no_parent_targets(self, mock_fetch): + doc = {"pos_transactions": [{"customer": "CUST-001"}]} + result = get_items_sold(doc) + + self.assertEqual(result, []) + mock_fetch.assert_not_called()