From d0f53ddb2715122071568fd9a9122ca807c4ec78 Mon Sep 17 00:00:00 2001 From: Dirk van der Laarse Date: Thu, 30 Apr 2026 13:00:07 +0000 Subject: [PATCH 1/2] feat: post a journal entry for cash-up variances --- posnext/fixtures/custom_field.json | 545 +++++++++++++++-------- posnext/hooks.py | 4 + posnext/overrides/dashboard_overrides.py | 21 + posnext/overrides/pos_closing_entry.py | 132 +++++- posnext/public/js/pos_profile.js | 11 + posnext/tests/__init__.py | 0 posnext/tests/test_cashup_variance.py | 302 +++++++++++++ 7 files changed, 823 insertions(+), 192 deletions(-) create mode 100644 posnext/overrides/dashboard_overrides.py create mode 100644 posnext/tests/__init__.py create mode 100644 posnext/tests/test_cashup_variance.py diff --git a/posnext/fixtures/custom_field.json b/posnext/fixtures/custom_field.json index db4d695..e42229e 100644 --- a/posnext/fixtures/custom_field.json +++ b/posnext/fixtures/custom_field.json @@ -296,11 +296,11 @@ "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "POS Profile", + "dt": "Sales Order", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_mobile_number_based_customer", - "fieldtype": "Check", + "fieldname": "pos_profile", + "fieldtype": "Link", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -311,19 +311,19 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_held_button", + "insert_after": "delivery_date", "is_system_generated": 0, "is_virtual": 0, - "label": "Mobile Number Based Customer", + "label": "POS Profile", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-08-16 07:44:20.324682", + "modified": "2025-02-18 21:01:56.657675", "module": "Posnext", - "name": "POS Profile-custom_mobile_number_based_customer", + "name": "Sales Order-custom_pos_profile", "no_copy": 0, "non_negative": 0, - "options": null, + "options": "POS Profile", "permlevel": 0, "placeholder": null, "precision": "", @@ -353,11 +353,11 @@ "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "Sales Order", + "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "pos_profile", - "fieldtype": "Link", + "fieldname": "custom_mobile_number_based_customer", + "fieldtype": "Check", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -368,19 +368,19 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "delivery_date", + "insert_after": "custom_show_held_button", "is_system_generated": 0, "is_virtual": 0, - "label": "POS Profile", + "label": "Mobile Number Based Customer", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2025-02-18 21:01:56.657675", + "modified": "2024-08-16 07:44:20.324682", "module": "Posnext", - "name": "Sales Order-custom_pos_profile", + "name": "POS Profile-custom_mobile_number_based_customer", "no_copy": 0, "non_negative": 0, - "options": "POS Profile", + "options": null, "permlevel": 0, "placeholder": null, "precision": "", @@ -482,7 +482,7 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_notification_message_whatsapp", + "insert_after": "custom_mobile_number_based_customer", "is_system_generated": 0, "is_virtual": 0, "label": "Show Checkout Button", @@ -695,10 +695,10 @@ "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "POS Profile", + "dt": "Sales Invoice", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_last_incoming_rate", + "fieldname": "custom_credit_sales", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -710,16 +710,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_posting_date", + "insert_after": "pos_profile", "is_system_generated": 0, "is_virtual": 0, - "label": "Show Last Incoming Rate", + "label": "Credit Sales", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-11-01 05:25:32.110447", + "modified": "2024-11-05 06:17:52.333733", "module": "Posnext", - "name": "POS Profile-custom_show_last_incoming_rate", + "name": "Sales Invoice-custom_credit_sales", "no_copy": 0, "non_negative": 0, "options": null, @@ -755,7 +755,7 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_oem_part_number", + "fieldname": "custom_show_last_incoming_rate", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -767,16 +767,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_last_incoming_rate", + "insert_after": "custom_show_posting_date", "is_system_generated": 0, "is_virtual": 0, - "label": "Show OEM Part Number", + "label": "Show Last Incoming Rate", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-11-01 05:25:32.017621", + "modified": "2024-11-01 05:25:32.110447", "module": "Posnext", - "name": "POS Profile-custom_show_oem_part_number", + "name": "POS Profile-custom_show_last_incoming_rate", "no_copy": 0, "non_negative": 0, "options": null, @@ -812,9 +812,9 @@ "dt": "Sales Invoice", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_credit_sales", - "fieldtype": "Check", - "hidden": 0, + "fieldname": "custom_credit_sales_date", + "fieldtype": "Date", + "hidden": 1, "hide_border": 0, "hide_days": 0, "hide_seconds": 0, @@ -824,16 +824,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "pos_profile", + "insert_after": "custom_credit_sales", "is_system_generated": 0, "is_virtual": 0, - "label": "Credit Sales", + "label": "Credit Sales Date", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-11-05 06:17:52.333733", + "modified": "2024-11-05 06:22:17.172216", "module": "Posnext", - "name": "Sales Invoice-custom_credit_sales", + "name": "Sales Invoice-custom_credit_sales_date", "no_copy": 0, "non_negative": 0, "options": null, @@ -869,7 +869,7 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_logical_rack", + "fieldname": "custom_show_oem_part_number", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -881,16 +881,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_oem_part_number", + "insert_after": "custom_show_last_incoming_rate", "is_system_generated": 0, "is_virtual": 0, - "label": "Show Logical Rack", + "label": "Show OEM Part Number", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-11-07 06:23:09.148261", + "modified": "2024-11-01 05:25:32.017621", "module": "Posnext", - "name": "POS Profile-custom_show_logical_rack", + "name": "POS Profile-custom_show_oem_part_number", "no_copy": 0, "non_negative": 0, "options": null, @@ -923,12 +923,12 @@ "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "Sales Invoice", + "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_credit_sales_date", - "fieldtype": "Date", - "hidden": 1, + "fieldname": "custom_show_logical_rack", + "fieldtype": "Check", + "hidden": 0, "hide_border": 0, "hide_days": 0, "hide_seconds": 0, @@ -938,16 +938,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_credit_sales", + "insert_after": "custom_show_oem_part_number", "is_system_generated": 0, "is_virtual": 0, - "label": "Credit Sales Date", + "label": "Show Logical Rack", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-11-05 06:22:17.172216", + "modified": "2024-11-07 06:23:09.148261", "module": "Posnext", - "name": "Sales Invoice-custom_credit_sales_date", + "name": "POS Profile-custom_show_logical_rack", "no_copy": 0, "non_negative": 0, "options": null, @@ -1040,7 +1040,7 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "show_batch_in_cart", + "fieldname": "custom_show_uom_in_cart", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -1055,13 +1055,13 @@ "insert_after": "custom_edit_rate_and_uom", "is_system_generated": 0, "is_virtual": 0, - "label": "Show Batch No", + "label": "Show UOM in Cart", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2025-03-28 12:03:58.035560", + "modified": "2024-12-08 22:09:04.484772", "module": "Posnext", - "name": "POS Profile-custom_show_batch_no", + "name": "POS Profile-custom_show_uom_in_cart", "no_copy": 0, "non_negative": 0, "options": null, @@ -1097,7 +1097,7 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_uom_in_cart", + "fieldname": "show_batch_in_cart", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -1109,16 +1109,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "show_batch_in_cart", + "insert_after": "custom_edit_rate_and_uom", "is_system_generated": 0, "is_virtual": 0, - "label": "Show UOM in Cart", + "label": "Show Batch No", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-12-08 22:09:04.484772", + "modified": "2025-03-28 12:03:58.035560", "module": "Posnext", - "name": "POS Profile-custom_show_uom_in_cart", + "name": "POS Profile-custom_show_batch_no", "no_copy": 0, "non_negative": 0, "options": null, @@ -1147,15 +1147,15 @@ "collapsible_depends_on": null, "columns": 0, "default": null, - "depends_on": null, + "depends_on": "eval: doc.custom_edit_rate_and_uom;", "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "Item", + "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_alternative_items", - "fieldtype": "Section Break", + "fieldname": "custom_show_incoming_rate", + "fieldtype": "Check", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -1166,16 +1166,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "image", + "insert_after": "show_branch", "is_system_generated": 0, "is_virtual": 0, - "label": "Alternative Items", + "label": "Show Incoming Rate", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-11-01 10:29:14.635688", + "modified": "2024-12-04 00:05:16.953710", "module": "Posnext", - "name": "Item-custom_alternative_items", + "name": "POS Profile-custom_show_incoming_rate", "no_copy": 0, "non_negative": 0, "options": null, @@ -1211,7 +1211,7 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_incoming_rate", + "fieldname": "custom_show_logical_rack_in_cart", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -1223,16 +1223,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "show_branch", + "insert_after": "custom_show_incoming_rate", "is_system_generated": 0, "is_virtual": 0, - "label": "Show Incoming Rate", + "label": "Show Logical Rack in Cart", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-12-04 00:05:16.953710", + "modified": "2024-12-07 00:03:43.974745", "module": "Posnext", - "name": "POS Profile-custom_show_incoming_rate", + "name": "POS Profile-custom_show_logical_rack_in_cart", "no_copy": 0, "non_negative": 0, "options": null, @@ -1268,8 +1268,8 @@ "dt": "Item", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_items", - "fieldtype": "Table", + "fieldname": "custom_alternative_items", + "fieldtype": "Section Break", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -1280,19 +1280,19 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_alternative_items", + "insert_after": "image", "is_system_generated": 0, "is_virtual": 0, - "label": "Items", + "label": "Alternative Items", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-11-01 10:29:14.533231", + "modified": "2024-11-01 10:29:14.635688", "module": "Posnext", - "name": "Item-custom_items", + "name": "Item-custom_alternative_items", "no_copy": 0, "non_negative": 0, - "options": "Alternative Items", + "options": null, "permlevel": 0, "placeholder": null, "precision": "", @@ -1318,14 +1318,14 @@ "collapsible_depends_on": null, "columns": 0, "default": null, - "depends_on": null, + "depends_on": "eval: doc.custom_edit_rate_and_uom", "description": null, "docstatus": 0, "doctype": "Custom Field", "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_item_discription", + "fieldname": "custom_use_discount_percentage", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -1337,16 +1337,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_incoming_rate", + "insert_after": "custom_show_logical_rack_in_cart", "is_system_generated": 0, "is_virtual": 0, - "label": "Show Item Discription", + "label": "Use Discount Percentage", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2025-05-06 23:01:00.706600", + "modified": "2024-11-15 06:30:05.707381", "module": "Posnext", - "name": "POS Profile-custom_show_item_discription", + "name": "POS Profile-custom_use_discount_percentage", "no_copy": 0, "non_negative": 0, "options": null, @@ -1379,11 +1379,11 @@ "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "POS Profile", + "dt": "Item", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_item_barcode", - "fieldtype": "Check", + "fieldname": "custom_items", + "fieldtype": "Table", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -1394,19 +1394,19 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_item_discription", + "insert_after": "custom_alternative_items", "is_system_generated": 0, "is_virtual": 0, - "label": "Show Item Barcode", + "label": "Items", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2025-05-12 19:26:03.816684", + "modified": "2024-11-01 10:29:14.533231", "module": "Posnext", - "name": "POS Profile-custom_show_item_barcode", + "name": "Item-custom_items", "no_copy": 0, "non_negative": 0, - "options": null, + "options": "Alternative Items", "permlevel": 0, "placeholder": null, "precision": "", @@ -1432,14 +1432,14 @@ "collapsible_depends_on": null, "columns": 0, "default": null, - "depends_on": "eval: doc.custom_edit_rate_and_uom;", + "depends_on": null, "description": null, "docstatus": 0, "doctype": "Custom Field", "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_logical_rack_in_cart", + "fieldname": "custom_show_item_discription", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -1451,16 +1451,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_item_barcode", + "insert_after": "custom_show_incoming_rate", "is_system_generated": 0, "is_virtual": 0, - "label": "Show Logical Rack in Cart", + "label": "Show Item Discription", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-12-07 00:03:43.974745", + "modified": "2025-05-06 23:01:00.706600", "module": "Posnext", - "name": "POS Profile-custom_show_logical_rack_in_cart", + "name": "POS Profile-custom_show_item_discription", "no_copy": 0, "non_negative": 0, "options": null, @@ -1496,7 +1496,7 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_use_discount_percentage", + "fieldname": "custom_use_discount_amount", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -1508,16 +1508,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_logical_rack_in_cart", + "insert_after": "custom_use_discount_percentage", "is_system_generated": 0, "is_virtual": 0, - "label": "Use Discount Percentage", + "label": "Use Discount Amount", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-11-15 06:30:05.707381", + "modified": "2024-11-15 06:30:05.558116", "module": "Posnext", - "name": "POS Profile-custom_use_discount_percentage", + "name": "POS Profile-custom_use_discount_amount", "no_copy": 0, "non_negative": 0, "options": null, @@ -1546,14 +1546,14 @@ "collapsible_depends_on": null, "columns": 0, "default": null, - "depends_on": "eval: doc.custom_edit_rate_and_uom", + "depends_on": null, "description": null, "docstatus": 0, "doctype": "Custom Field", "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_use_discount_amount", + "fieldname": "custom_show_item_barcode", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -1565,16 +1565,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_use_discount_percentage", + "insert_after": "custom_show_item_discription", "is_system_generated": 0, "is_virtual": 0, - "label": "Use Discount Amount", + "label": "Show Item Barcode", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-11-15 06:30:05.558116", + "modified": "2025-05-12 19:26:03.816684", "module": "Posnext", - "name": "POS Profile-custom_use_discount_amount", + "name": "POS Profile-custom_show_item_barcode", "no_copy": 0, "non_negative": 0, "options": null, @@ -2006,11 +2006,11 @@ "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "Sales Invoice Item", + "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_valuation_rate", - "fieldtype": "Currency", + "fieldname": "custom_show_sales_man", + "fieldtype": "Check", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -2021,16 +2021,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "item_tax_template", + "insert_after": "custom_show_close_the_pos", "is_system_generated": 0, "is_virtual": 0, - "label": "Valuation Rate", + "label": "Show Sales Man", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-12-04 23:51:32.067475", + "modified": "2024-11-01 05:25:31.790257", "module": "Posnext", - "name": "Sales Invoice Item-custom_valuation_rate", + "name": "POS Profile-custom_show_sales_man", "no_copy": 0, "non_negative": 0, "options": null, @@ -2040,7 +2040,7 @@ "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, - "read_only": 1, + "read_only": 0, "read_only_depends_on": null, "report_hide": 0, "reqd": 0, @@ -2066,7 +2066,7 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_sales_man", + "fieldname": "custom_show_additional_note", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -2078,16 +2078,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_close_the_pos", + "insert_after": "custom_show_sales_man", "is_system_generated": 0, "is_virtual": 0, - "label": "Show Sales Man", + "label": "Show Additional Note", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-11-01 05:25:31.790257", + "modified": "2024-11-01 05:25:31.561160", "module": "Posnext", - "name": "POS Profile-custom_show_sales_man", + "name": "POS Profile-custom_show_additional_note", "no_copy": 0, "non_negative": 0, "options": null, @@ -2120,11 +2120,11 @@ "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "Sales Invoice Item", + "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_item_uoms", - "fieldtype": "Data", + "fieldname": "custom_show_credit_sales", + "fieldtype": "Check", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -2135,16 +2135,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_valuation_rate", + "insert_after": "custom_show_additional_note", "is_system_generated": 0, "is_virtual": 0, - "label": "Item UOMs", + "label": "Show Credit Sales", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-12-05 21:30:02.846405", + "modified": "2024-11-01 05:25:31.674539", "module": "Posnext", - "name": "Sales Invoice Item-custom_item_uoms", + "name": "POS Profile-custom_show_credit_sales", "no_copy": 0, "non_negative": 0, "options": null, @@ -2154,7 +2154,7 @@ "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, - "read_only": 1, + "read_only": 0, "read_only_depends_on": null, "report_hide": 0, "reqd": 0, @@ -2180,7 +2180,7 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_additional_note", + "fieldname": "custom_show_alternative_item_for_pos_search", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -2192,16 +2192,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_sales_man", + "insert_after": "custom_show_credit_sales", "is_system_generated": 0, "is_virtual": 0, - "label": "Show Additional Note", + "label": "Show Alternative Item for POS Search", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-11-01 05:25:31.561160", + "modified": "2024-11-01 05:25:31.924813", "module": "Posnext", - "name": "POS Profile-custom_show_additional_note", + "name": "POS Profile-custom_show_alternative_item_for_pos_search", "no_copy": 0, "non_negative": 0, "options": null, @@ -2237,8 +2237,8 @@ "dt": "Sales Invoice Item", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_logical_rack", - "fieldtype": "Data", + "fieldname": "custom_valuation_rate", + "fieldtype": "Currency", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -2249,16 +2249,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_item_uoms", + "insert_after": "item_tax_template", "is_system_generated": 0, "is_virtual": 0, - "label": "Logical Rack", + "label": "Valuation Rate", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-12-07 00:03:31.531532", + "modified": "2024-12-04 23:51:32.067475", "module": "Posnext", - "name": "Sales Invoice Item-custom_logical_rack", + "name": "Sales Invoice Item-custom_valuation_rate", "no_copy": 0, "non_negative": 0, "options": null, @@ -2268,7 +2268,7 @@ "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, - "read_only": 0, + "read_only": 1, "read_only_depends_on": null, "report_hide": 0, "reqd": 0, @@ -2294,7 +2294,7 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_credit_sales", + "fieldname": "custom_show_save_as_draft", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -2306,16 +2306,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_additional_note", + "insert_after": "custom_show_alternative_item_for_pos_search", "is_system_generated": 0, "is_virtual": 0, - "label": "Show Credit Sales", + "label": "Show Save as Draft", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-11-01 05:25:31.674539", + "modified": "2024-08-16 07:44:20.763682", "module": "Posnext", - "name": "POS Profile-custom_show_credit_sales", + "name": "POS Profile-custom_show_save_as_draft", "no_copy": 0, "non_negative": 0, "options": null, @@ -2348,11 +2348,11 @@ "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "POS Profile", + "dt": "Sales Invoice Item", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_alternative_item_for_pos_search", - "fieldtype": "Check", + "fieldname": "custom_item_uoms", + "fieldtype": "Data", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -2363,16 +2363,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_credit_sales", + "insert_after": "custom_valuation_rate", "is_system_generated": 0, "is_virtual": 0, - "label": "Show Alternative Item for POS Search", + "label": "Item UOMs", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-11-01 05:25:31.924813", + "modified": "2024-12-05 21:30:02.846405", "module": "Posnext", - "name": "POS Profile-custom_show_alternative_item_for_pos_search", + "name": "Sales Invoice Item-custom_item_uoms", "no_copy": 0, "non_negative": 0, "options": null, @@ -2382,7 +2382,7 @@ "print_hide": 0, "print_hide_if_no_value": 0, "print_width": null, - "read_only": 0, + "read_only": 1, "read_only_depends_on": null, "report_hide": 0, "reqd": 0, @@ -2408,7 +2408,7 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_save_as_draft", + "fieldname": "custom_show_open_form_view", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -2420,16 +2420,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_alternative_item_for_pos_search", + "insert_after": "custom_show_save_as_draft", "is_system_generated": 0, "is_virtual": 0, - "label": "Show Save as Draft", + "label": "Show Open Form View", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-08-16 07:44:20.763682", + "modified": "2024-08-16 07:44:20.593668", "module": "Posnext", - "name": "POS Profile-custom_show_save_as_draft", + "name": "POS Profile-custom_show_open_form_view", "no_copy": 0, "non_negative": 0, "options": null, @@ -2462,11 +2462,11 @@ "description": null, "docstatus": 0, "doctype": "Custom Field", - "dt": "POS Profile", + "dt": "Sales Invoice Item", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_show_open_form_view", - "fieldtype": "Check", + "fieldname": "custom_logical_rack", + "fieldtype": "Data", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -2477,16 +2477,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_show_save_as_draft", + "insert_after": "custom_item_uoms", "is_system_generated": 0, "is_virtual": 0, - "label": "Show Open Form View", + "label": "Logical Rack", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-08-16 07:44:20.593668", + "modified": "2024-12-07 00:03:31.531532", "module": "Posnext", - "name": "POS Profile-custom_show_open_form_view", + "name": "Sales Invoice Item-custom_logical_rack", "no_copy": 0, "non_negative": 0, "options": null, @@ -2792,6 +2792,120 @@ "unique": 0, "width": null }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "POS Profile", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_mobile_number_length", + "fieldtype": "Int", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "custom_add_reference_details", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Mobile Number Length", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2024-08-16 07:44:20.913321", + "module": "Posnext", + "name": "POS Profile-custom_mobile_number_length", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "List", + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "POS Profile", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_default_view", + "fieldtype": "Select", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "custom_mobile_number_length", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Default View", + "length": 0, + "link_filters": null, + "mandatory_depends_on": null, + "modified": "2024-08-16 07:44:20.160526", + "module": "Posnext", + "name": "POS Profile-custom_default_view", + "no_copy": 0, + "non_negative": 0, + "options": "\nList\nCard", + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 1, + "unique": 0, + "width": null + }, { "allow_in_quick_entry": 0, "allow_on_submit": 0, @@ -2921,8 +3035,8 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_mobile_number_length", - "fieldtype": "Int", + "fieldname": "custom_skip_stock_transaction_validation", + "fieldtype": "Check", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -2933,16 +3047,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_stock_update", + "insert_after": "validate_stock_on_save", "is_system_generated": 0, "is_virtual": 0, - "label": "Mobile Number Length", + "label": "Skip Stock Transaction Validation", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-08-16 07:44:20.913321", + "modified": "2024-12-08 22:10:15.427468", "module": "Posnext", - "name": "POS Profile-custom_mobile_number_length", + "name": "POS Profile-custom_skip_stock_transaction_validation", "no_copy": 0, "non_negative": 0, "options": null, @@ -2970,7 +3084,7 @@ "collapsible": 0, "collapsible_depends_on": null, "columns": 0, - "default": "List", + "default": null, "depends_on": null, "description": null, "docstatus": 0, @@ -2978,8 +3092,8 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_default_view", - "fieldtype": "Select", + "fieldname": "custom_cashup_variance_section", + "fieldtype": "Section Break", "hidden": 0, "hide_border": 0, "hide_days": 0, @@ -2990,19 +3104,19 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "custom_mobile_number_length", + "insert_after": "allow_partial_payment", "is_system_generated": 0, "is_virtual": 0, - "label": "Default View", + "label": "Cash-up Variance", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-08-16 07:44:20.160526", + "modified": "2026-04-30 00:00:00", "module": "Posnext", - "name": "POS Profile-custom_default_view", + "name": "POS Profile-custom_cashup_variance_section", "no_copy": 0, "non_negative": 0, - "options": "\nList\nCard", + "options": null, "permlevel": 0, "placeholder": null, "precision": "", @@ -3016,7 +3130,7 @@ "search_index": 0, "show_dashboard": 0, "sort_options": 0, - "translatable": 1, + "translatable": 0, "unique": 0, "width": null }, @@ -3027,7 +3141,7 @@ "collapsible": 0, "collapsible_depends_on": null, "columns": 0, - "default": null, + "default": "0", "depends_on": null, "description": null, "docstatus": 0, @@ -3035,7 +3149,7 @@ "dt": "POS Profile", "fetch_from": null, "fetch_if_empty": 0, - "fieldname": "custom_skip_stock_transaction_validation", + "fieldname": "custom_create_cashup_variance_je", "fieldtype": "Check", "hidden": 0, "hide_border": 0, @@ -3047,16 +3161,16 @@ "in_list_view": 0, "in_preview": 0, "in_standard_filter": 0, - "insert_after": "validate_stock_on_save", + "insert_after": "custom_cashup_variance_section", "is_system_generated": 0, "is_virtual": 0, - "label": "Skip Stock Transaction Validation", + "label": "Create Journal Entry for Cash-up Variance", "length": 0, "link_filters": null, "mandatory_depends_on": null, - "modified": "2024-12-08 22:10:15.427468", + "modified": "2026-04-30 00:00:00", "module": "Posnext", - "name": "POS Profile-custom_skip_stock_transaction_validation", + "name": "POS Profile-custom_create_cashup_variance_je", "no_copy": 0, "non_negative": 0, "options": null, @@ -3077,6 +3191,63 @@ "unique": 0, "width": null }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": "eval:doc.custom_create_cashup_variance_je", + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "POS Profile", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "custom_cashup_variance_account", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "custom_create_cashup_variance_je", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Cash-up Variance Account", + "length": 0, + "link_filters": "[[\"Account\", \"root_type\", \"=\", \"Expense\"]]", + "mandatory_depends_on": "eval:doc.custom_create_cashup_variance_je", + "modified": "2026-04-30 00:00:00", + "module": "Posnext", + "name": "POS Profile-custom_cashup_variance_account", + "no_copy": 0, + "non_negative": 0, + "options": "Account", + "permlevel": 0, + "placeholder": null, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "show_dashboard": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0, + "width": null + }, { "allow_in_quick_entry": 0, "allow_on_submit": 0, diff --git a/posnext/hooks.py b/posnext/hooks.py index 11ebf70..e4c37f1 100644 --- a/posnext/hooks.py +++ b/posnext/hooks.py @@ -253,6 +253,10 @@ "POS Invoice Merge Log": "posnext.overrides.pos_invoice_merge_log.PosnextPOSInvoiceMergeLog", } +override_doctype_dashboards = { + "POS Closing Entry": "posnext.overrides.dashboard_overrides.get_dashboard_for_pos_closing_entry", +} + fixtures = [ {"doctype": "Custom Field", "filters": [["module", "in", ["Posnext"]]]}, diff --git a/posnext/overrides/dashboard_overrides.py b/posnext/overrides/dashboard_overrides.py new file mode 100644 index 0000000..8d48c61 --- /dev/null +++ b/posnext/overrides/dashboard_overrides.py @@ -0,0 +1,21 @@ +from frappe import _ + + +def get_dashboard_for_pos_closing_entry(data: dict) -> dict: + """Extend POS Closing Entry dashboard to show linked cash-up variance Journal Entries.""" + data["transactions"].append( + { + "label": _("Accounting"), + "items": ["Journal Entry"], + } + ) + + # Journal Entry links back via cheque_no (a parent-level Data field), + # not via a standard Link field — must tell Frappe which field to query. + data["non_standard_fieldnames"].update( + { + "Journal Entry": "cheque_no", + } + ) + + return data diff --git a/posnext/overrides/pos_closing_entry.py b/posnext/overrides/pos_closing_entry.py index ec88f60..e23f678 100644 --- a/posnext/overrides/pos_closing_entry.py +++ b/posnext/overrides/pos_closing_entry.py @@ -1,7 +1,7 @@ import frappe from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import POSClosingEntry from frappe import _ -from frappe.utils import get_datetime +from frappe.utils import flt, get_datetime from posnext.overrides.pos_invoice_merge_log import ( consolidate_pos_invoices, @@ -34,17 +34,19 @@ def get_pos_invoices(start: str, end: str, pos_profile: str, user: str) -> list: class PosnextPOSClosingEntry(POSClosingEntry): - def on_submit(self): + def on_submit(self) -> None: consolidate_pos_invoices(closing_entry=self) + self._maybe_create_cashup_variance_je() - def on_cancel(self): + def on_cancel(self) -> None: unconsolidate_pos_invoices(closing_entry=self) + self._maybe_cancel_cashup_variance_je() @frappe.whitelist() - def retry(self): + def retry(self) -> None: consolidate_pos_invoices(closing_entry=self) - def validate_pos_invoices(self): + def validate_pos_invoices(self) -> None: invalid_rows = [] for d in self.pos_transactions: invalid_row = {"idx": d.idx} @@ -92,3 +94,123 @@ def validate_pos_invoices(self): error_list.append(_("Row #{}: {}").format(row.get("idx"), msg)) frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True) + + # ------------------------------------------------------------------ + # Cash-up Variance JE + # ------------------------------------------------------------------ + + def _maybe_create_cashup_variance_je(self) -> None: + enabled, variance_account = frappe.db.get_value( + "POS Profile", + self.pos_profile, + ["custom_create_cashup_variance_je", "custom_cashup_variance_account"], + ) + + if not enabled or not variance_account: + return + + variance_rows = [ + row for row in self.payment_reconciliation if flt(row.difference) != 0 + ] + + if not variance_rows: + return + + je = self._build_cashup_variance_je(variance_account, variance_rows) + je.flags.ignore_permissions = True + je.insert() + je.submit() + + frappe.msgprint( + _("Cash-up Variance Journal Entry {0} created for {1}.").format( + frappe.bold(je.name), + frappe.bold(self.name), + ), + title=_("Cash-up Variance Posted"), + indicator="green", + ) + + def _build_cashup_variance_je( + self, variance_account: str, variance_rows: list + ) -> "frappe.Document": + net_variance = sum(flt(row.difference) for row in variance_rows) + + je = frappe.new_doc("Journal Entry") + je.voucher_type = "Journal Entry" + je.posting_date = self.posting_date + je.company = self.company + je.user_remark = ( + f"POS Closing Entry {self.name} overs and unders/cash-up variance" + ) + je.cheque_no = self.name + je.cheque_date = self.posting_date + + # Single net entry for the variance account + if net_variance > 0: + je.append( + "accounts", + { + "account": variance_account, + "credit_in_account_currency": flt(net_variance), + }, + ) + else: + je.append( + "accounts", + { + "account": variance_account, + "debit_in_account_currency": flt(abs(net_variance)), + }, + ) + + # One row per payment method with a non-zero difference + for row in variance_rows: + payment_account = self._get_mop_default_account(row.mode_of_payment) + if not payment_account: + frappe.throw( + _( + "No default account found for Mode of Payment {0} in company {1}. " + "Please configure it before closing the POS." + ).format( + frappe.bold(row.mode_of_payment), frappe.bold(self.company) + ) + ) + + diff = flt(row.difference) + if diff > 0: + je.append( + "accounts", + { + "account": payment_account, + "debit_in_account_currency": diff, + }, + ) + else: + je.append( + "accounts", + { + "account": payment_account, + "credit_in_account_currency": abs(diff), + }, + ) + + return je + + def _get_mop_default_account(self, mode_of_payment: str) -> str | None: + return frappe.db.get_value( + "Mode of Payment Account", + {"parent": mode_of_payment, "company": self.company}, + "default_account", + ) + + def _maybe_cancel_cashup_variance_je(self) -> None: + je_names = frappe.get_all( + "Journal Entry", + filters={"cheque_no": self.name, "docstatus": 1, "company": self.company}, + pluck="name", + ) + + for je_name in je_names: + je = frappe.get_doc("Journal Entry", je_name) + je.flags.ignore_permissions = True + je.cancel() diff --git a/posnext/public/js/pos_profile.js b/posnext/public/js/pos_profile.js index 678dc0d..cef6dfc 100644 --- a/posnext/public/js/pos_profile.js +++ b/posnext/public/js/pos_profile.js @@ -1,4 +1,15 @@ frappe.ui.form.on("POS Profile", { + refresh(frm) { + frm.set_query("custom_cashup_variance_account", function () { + return { + filters: { + root_type: "Expense", + company: frm.doc.company, + is_group: 0, + }, + }; + }); + }, custom_show_only_list_view: function (frm) { if (frm.doc.custom_show_only_list_view) { frm.doc.custom_show_only_card_view = 0; diff --git a/posnext/tests/__init__.py b/posnext/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/posnext/tests/test_cashup_variance.py b/posnext/tests/test_cashup_variance.py new file mode 100644 index 0000000..19d817d --- /dev/null +++ b/posnext/tests/test_cashup_variance.py @@ -0,0 +1,302 @@ +import unittest + +import frappe +from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile +from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry +from frappe.utils import today + +from posnext.overrides.pos_closing_entry import PosnextPOSClosingEntry + +COMPANY = "_Test Company" +CASH_MOP = "Cash" +_COUNTER = 0 + + +def _next_fake_name() -> str: + global _COUNTER + _COUNTER += 1 + return f"TEST-PCE-{_COUNTER:04d}" + + +def _get_expense_account() -> str: + account = frappe.db.get_value( + "Account", + {"company": COMPANY, "root_type": "Expense", "is_group": 0}, + "name", + order_by="name asc", + ) + if not account: + frappe.throw(f"No Expense account found in {COMPANY} — cannot run tests.") + return account + + +def _get_cash_account() -> str: + account = frappe.db.get_value( + "Mode of Payment Account", + {"parent": CASH_MOP, "company": COMPANY}, + "default_account", + ) + if not account: + frappe.throw( + f"No default account for Mode of Payment '{CASH_MOP}' in {COMPANY}." + ) + return account + + +def _make_pos_profile_with_variance(variance_account: str) -> "frappe.Document": + """POS Profile with variance JE enabled, ready for use.""" + pos_profile = make_pos_profile(do_not_insert=True) + pos_profile.custom_create_cashup_variance_je = 1 + pos_profile.custom_cashup_variance_account = variance_account + if not frappe.db.exists("POS Profile", pos_profile.name): + pos_profile.insert() + else: + pos_profile.save() + return pos_profile + + +def _make_pos_profile_disabled() -> "frappe.Document": + """POS Profile with variance JE disabled (the default).""" + pos_profile = make_pos_profile(do_not_insert=True) + pos_profile.custom_create_cashup_variance_je = 0 + pos_profile.custom_cashup_variance_account = None + if not frappe.db.exists("POS Profile", pos_profile.name): + pos_profile.insert() + else: + pos_profile.save() + return pos_profile + + +def _make_ce( + pos_profile_name: str, + variance_rows: list[dict], + fake_name: str | None = None, +) -> "PosnextPOSClosingEntry": + """ + Build a PosnextPOSClosingEntry with only the fields the variance JE + methods need. The document is NOT saved to the database. + """ + ce = frappe.new_doc("POS Closing Entry") + ce.pos_profile = pos_profile_name + ce.company = COMPANY + ce.posting_date = today() + ce.name = fake_name or _next_fake_name() + for row in variance_rows: + ce.append("payment_reconciliation", row) + return ce + + +class TestCashupVarianceJE(unittest.TestCase): + @classmethod + def setUpClass(cls): + make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) + cls.expense_account = _get_expense_account() + cls.cash_account = _get_cash_account() + + def setUp(self): + frappe.set_user("Administrator") + + def tearDown(self): + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabPOS Profile`") + + # ------------------------------------------------------------------ + # 8.2 — feature disabled + # ------------------------------------------------------------------ + + def test_no_je_when_disabled(self): + pos_profile = _make_pos_profile_disabled() + ce = _make_ce( + pos_profile.name, + [ + { + "mode_of_payment": CASH_MOP, + "expected_amount": 1000, + "closing_amount": 900, + "difference": -100, + } + ], + ) + ce._maybe_create_cashup_variance_je() + + count = frappe.db.count( + "Journal Entry", {"cheque_no": ce.name, "docstatus": ["!=", 2]} + ) + self.assertEqual(count, 0) + + # ------------------------------------------------------------------ + # 8.3 — all differences zero + # ------------------------------------------------------------------ + + def test_no_je_when_all_balanced(self): + pos_profile = _make_pos_profile_with_variance(self.expense_account) + ce = _make_ce( + pos_profile.name, + [ + { + "mode_of_payment": CASH_MOP, + "expected_amount": 1000, + "closing_amount": 1000, + "difference": 0, + } + ], + ) + ce._maybe_create_cashup_variance_je() + + count = frappe.db.count( + "Journal Entry", {"cheque_no": ce.name, "docstatus": ["!=", 2]} + ) + self.assertEqual(count, 0) + + # ------------------------------------------------------------------ + # 8.4 — under (cashier short) + # ------------------------------------------------------------------ + + def test_je_created_for_under(self): + pos_profile = _make_pos_profile_with_variance(self.expense_account) + ce = _make_ce( + pos_profile.name, + [ + { + "mode_of_payment": CASH_MOP, + "expected_amount": 1000, + "closing_amount": 900, + "difference": -100, + } + ], + ) + ce._maybe_create_cashup_variance_je() + + je_name = frappe.db.get_value( + "Journal Entry", {"cheque_no": ce.name, "docstatus": 1}, "name" + ) + self.assertIsNotNone(je_name, "JE should be created for an under") + + je = frappe.get_doc("Journal Entry", je_name) + total_debit = sum(r.debit_in_account_currency for r in je.accounts) + total_credit = sum(r.credit_in_account_currency for r in je.accounts) + self.assertAlmostEqual( + total_debit, total_credit, places=2, msg="JE must balance" + ) + self.assertAlmostEqual(total_debit, 100, places=2) + + # Variance account debited (recording the loss) + var_rows = [r for r in je.accounts if r.account == self.expense_account] + self.assertTrue(var_rows, "Variance account row missing from JE") + self.assertAlmostEqual(var_rows[0].debit_in_account_currency, 100, places=2) + + # ------------------------------------------------------------------ + # 8.5 — over (cashier surplus) + # ------------------------------------------------------------------ + + def test_je_created_for_over(self): + pos_profile = _make_pos_profile_with_variance(self.expense_account) + ce = _make_ce( + pos_profile.name, + [ + { + "mode_of_payment": CASH_MOP, + "expected_amount": 1000, + "closing_amount": 1050, + "difference": 50, + } + ], + ) + ce._maybe_create_cashup_variance_je() + + je_name = frappe.db.get_value( + "Journal Entry", {"cheque_no": ce.name, "docstatus": 1}, "name" + ) + self.assertIsNotNone(je_name, "JE should be created for an over") + + je = frappe.get_doc("Journal Entry", je_name) + total_debit = sum(r.debit_in_account_currency for r in je.accounts) + total_credit = sum(r.credit_in_account_currency for r in je.accounts) + self.assertAlmostEqual( + total_debit, total_credit, places=2, msg="JE must balance" + ) + self.assertAlmostEqual(total_credit, 50, places=2) + + # Variance account credited (recording the gain) + var_rows = [r for r in je.accounts if r.account == self.expense_account] + self.assertTrue(var_rows, "Variance account row missing from JE") + self.assertAlmostEqual(var_rows[0].credit_in_account_currency, 50, places=2) + + # ------------------------------------------------------------------ + # 8.6 — JE balances for mixed over/under + # ------------------------------------------------------------------ + + def test_je_balanced_for_mixed(self): + """Net under of 50 (cash -100, cash +50) → JE must balance.""" + pos_profile = _make_pos_profile_with_variance(self.expense_account) + ce = _make_ce( + pos_profile.name, + [ + { + "mode_of_payment": CASH_MOP, + "expected_amount": 1000, + "closing_amount": 900, + "difference": -100, + }, + { + "mode_of_payment": CASH_MOP, + "expected_amount": 500, + "closing_amount": 550, + "difference": 50, + }, + ], + ) + variance_rows = [ + row + for row in ce.payment_reconciliation + if frappe.utils.flt(row.difference) != 0 + ] + je = ce._build_cashup_variance_je(self.expense_account, variance_rows) + + total_debit = sum(r.debit_in_account_currency or 0 for r in je.accounts) + total_credit = sum(r.credit_in_account_currency or 0 for r in je.accounts) + self.assertAlmostEqual( + total_debit, total_credit, places=2, msg="Mixed JE must balance" + ) + + # ------------------------------------------------------------------ + # 8.7 — cancel cancels the JE + # ------------------------------------------------------------------ + + def test_cancel_cancels_je(self): + pos_profile = _make_pos_profile_with_variance(self.expense_account) + ce = _make_ce( + pos_profile.name, + [ + { + "mode_of_payment": CASH_MOP, + "expected_amount": 1000, + "closing_amount": 900, + "difference": -100, + } + ], + ) + ce._maybe_create_cashup_variance_je() + + je_name = frappe.db.get_value( + "Journal Entry", {"cheque_no": ce.name, "docstatus": 1}, "name" + ) + self.assertIsNotNone(je_name, "JE must exist before cancel") + + ce._maybe_cancel_cashup_variance_je() + + docstatus = frappe.db.get_value("Journal Entry", je_name, "docstatus") + self.assertEqual(docstatus, 2, "JE should be cancelled") + + # ------------------------------------------------------------------ + # 8.8 — missing MOP account raises + # ------------------------------------------------------------------ + + def test_throws_on_missing_mop_account(self): + pos_profile = _make_pos_profile_with_variance(self.expense_account) + ce = _make_ce(pos_profile.name, []) + + # "Cheque" MOP has no default_account configured for _Test Company + variance_rows = [frappe._dict({"mode_of_payment": "Cheque", "difference": -50})] + with self.assertRaises(frappe.ValidationError): + ce._build_cashup_variance_je(self.expense_account, variance_rows) From c7888f56b2b4d889e05754ed2e00f6660023723a Mon Sep 17 00:00:00 2001 From: Dirk van der Laarse Date: Thu, 30 Apr 2026 13:01:04 +0000 Subject: [PATCH 2/2] chore: bump to v0.2.0 --- package.json | 2 +- posnext/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a3a2152..448293e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posnext", - "version": "0.0.1", + "version": "0.2.0", "author": "Starktail (Pty) Ltd ", "main": "index.js", "devDependencies": { diff --git a/posnext/__init__.py b/posnext/__init__.py index 1276d02..d3ec452 100644 --- a/posnext/__init__.py +++ b/posnext/__init__.py @@ -1 +1 @@ -__version__ = "0.1.5" +__version__ = "0.2.0"