From b8224693c49a21ddfa4a57a0a5ed2a8a963e06aa Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 21 Jul 2025 14:22:05 +0000 Subject: [PATCH 1/9] fix(regional-uae): mark export items as zero rated --- erpnext/patches.txt | 1 + .../patches/v15_0/update_uae_zero_rated_fetch.py | 9 +++++++++ erpnext/regional/report/uae_vat_201/uae_vat_201.py | 2 +- erpnext/regional/united_arab_emirates/setup.py | 1 + erpnext/regional/united_arab_emirates/utils.py | 13 +++++++++---- 5 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 erpnext/patches/v15_0/update_uae_zero_rated_fetch.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9fea3cb1b16..e9d6d14cbd9 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -428,3 +428,4 @@ erpnext.patches.v15_0.set_company_on_pos_inv_merge_log erpnext.patches.v15_0.rename_price_list_to_buying_price_list erpnext.patches.v15_0.patch_missing_buying_price_list_in_material_request erpnext.patches.v15_0.remove_sales_partner_from_consolidated_sales_invoice +erpnext.patches.v15_0.update_uae_zero_rated_fetch diff --git a/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py new file mode 100644 index 00000000000..a769cc0a85e --- /dev/null +++ b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py @@ -0,0 +1,9 @@ +import frappe +from frappe.custom.doctype.property_setter.property_setter import make_property_setter + + +def execute(): + if not frappe.db.get_value("Company", {"country": "United Arab Emirates"}): + return + + make_property_setter("Sales Invoice Item", "is_zero_rated", "fetch_if_empty", 1, "Check") diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py index 1a68d7dec6a..7cf86adbe01 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -143,7 +143,7 @@ def get_total_emiratewise(filters): on i.parent = s.name where - s.docstatus = 1 and i.is_exempt != 1 and i.is_zero_rated != 1 + s.docstatus = 1 and i.is_exempt != 1 and i.is_zero_rated != 1 {conditions} group by s.vat_emirate; diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 36a079546e5..6a8c7b9438b 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -20,6 +20,7 @@ def make_custom_fields(): label="Is Zero Rated", fieldtype="Check", fetch_from="item_code.is_zero_rated", + fetch_if_empty=1, insert_after="description", print_hide=1, ) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 8265c27831f..8e23fe9e81b 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -7,10 +7,6 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax def update_itemised_tax_data(doc): - # maybe this should be a standard function rather than a regional one - if not doc.taxes: - return - if not doc.items: return @@ -19,6 +15,14 @@ def update_itemised_tax_data(doc): return itemised_tax = get_itemised_tax(doc.taxes) + is_export = 0 + + if doc.customer_address and doc.company_address: + company_country = frappe.get_cached_value("Address", doc.company_address, "country") + customer_country = frappe.db.get_value("Address", doc.customer_address, "country") + + if company_country != customer_country: + is_export = 1 for row in doc.items: tax_rate, tax_amount = 0.0, 0.0 @@ -30,6 +34,7 @@ def update_itemised_tax_data(doc): tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate + row.is_zero_rated = is_export row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt(tax_amount, row.precision("tax_amount")) row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) From dc72e6cf36f5b144b092efc60b2ebb4947a0d197 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 21 Jul 2025 14:47:14 +0000 Subject: [PATCH 2/9] fix(regional-uae): split export determination --- .../regional/united_arab_emirates/utils.py | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 8e23fe9e81b..511a8b03ab4 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -15,14 +15,25 @@ def update_itemised_tax_data(doc): return itemised_tax = get_itemised_tax(doc.taxes) - is_export = 0 + is_export = False - if doc.customer_address and doc.company_address: - company_country = frappe.get_cached_value("Address", doc.company_address, "country") - customer_country = frappe.db.get_value("Address", doc.customer_address, "country") + def determine_if_export(doc): + if doc.flags.export_determined is not None: + return doc.flags.export_determined - if company_country != customer_country: - is_export = 1 + doc.flags.export_determined = False + if doc.customer_address and doc.company_address: + company_country = frappe.get_cached_value("Address", doc.company_address, "country") + customer_country = frappe.db.get_value("Address", doc.customer_address, "country") + + if company_country != customer_country: + doc.flags.export_determined = True + return doc.flags.export_determined + else: + frappe.msgprint( + _("Please set Customer and Company Address to determine if the transaction is an export."), + alert=True, + ) for row in doc.items: tax_rate, tax_amount = 0.0, 0.0 @@ -34,11 +45,16 @@ def update_itemised_tax_data(doc): tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate - row.is_zero_rated = is_export + if not row.is_zero_rated and not tax_rate: + is_export = is_export or determine_if_export(doc) + row.is_zero_rated = is_export + row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt(tax_amount, row.precision("tax_amount")) row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) + doc.flags.export_determined = None + def get_account_currency(account): """Helper function to get account currency.""" From 1170e4fb2c067f7c44a06cac89f80b667ffe0e49 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 28 Jul 2025 05:32:04 +0000 Subject: [PATCH 3/9] fix(regional-uae): restrict zero rated export to invoice --- erpnext/regional/united_arab_emirates/utils.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 511a8b03ab4..d60d941faf2 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -10,14 +10,22 @@ def update_itemised_tax_data(doc): if not doc.items: return + has_zero_rated_field = False meta = frappe.get_meta(doc.items[0].doctype) if not meta.has_field("tax_rate"): return + if meta.has_field("is_zero_rated"): + has_zero_rated_field = True + itemised_tax = get_itemised_tax(doc.taxes) + doc.flags.export_determined = None is_export = False def determine_if_export(doc): + if doc.doctype != "Sales Invoice": + return False + if doc.flags.export_determined is not None: return doc.flags.export_determined @@ -45,7 +53,7 @@ def update_itemised_tax_data(doc): tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate - if not row.is_zero_rated and not tax_rate: + if not tax_rate and has_zero_rated_field and not row.is_zero_rated: is_export = is_export or determine_if_export(doc) row.is_zero_rated = is_export From d25846f383affe7ca4dfeba444cb5cc9daaec5ea Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 4 Aug 2025 15:45:43 +0530 Subject: [PATCH 4/9] fix: simplify export determination logic --- .../regional/united_arab_emirates/utils.py | 39 +++++++------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index d60d941faf2..b2ee96eedfc 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -10,38 +10,32 @@ def update_itemised_tax_data(doc): if not doc.items: return - has_zero_rated_field = False meta = frappe.get_meta(doc.items[0].doctype) if not meta.has_field("tax_rate"): return - if meta.has_field("is_zero_rated"): - has_zero_rated_field = True - itemised_tax = get_itemised_tax(doc.taxes) - doc.flags.export_determined = None - is_export = False def determine_if_export(doc): if doc.doctype != "Sales Invoice": return False - if doc.flags.export_determined is not None: - return doc.flags.export_determined - - doc.flags.export_determined = False - if doc.customer_address and doc.company_address: - company_country = frappe.get_cached_value("Address", doc.company_address, "country") - customer_country = frappe.db.get_value("Address", doc.customer_address, "country") - - if company_country != customer_country: - doc.flags.export_determined = True - return doc.flags.export_determined - else: + if not doc.customer_address: frappe.msgprint( - _("Please set Customer and Company Address to determine if the transaction is an export."), + _("Please set Customer Address to determine if the transaction is an export."), alert=True, ) + return False + + company_country = frappe.get_cached_value("Company", doc.company, "country") + customer_country = frappe.db.get_value("Address", doc.customer_address, "country") + + if company_country != customer_country: + return True + + return False + + is_export = determine_if_export(doc) for row in doc.items: tax_rate, tax_amount = 0.0, 0.0 @@ -53,16 +47,13 @@ def update_itemised_tax_data(doc): tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate - if not tax_rate and has_zero_rated_field and not row.is_zero_rated: - is_export = is_export or determine_if_export(doc) - row.is_zero_rated = is_export + if not tax_rate and is_export: + row.is_zero_rated = 1 row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt(tax_amount, row.precision("tax_amount")) row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount")) - doc.flags.export_determined = None - def get_account_currency(account): """Helper function to get account currency.""" From 0c15b65756b2c3dac09a705cf2a08bc8cd459c20 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Mon, 4 Aug 2025 15:52:22 +0530 Subject: [PATCH 5/9] fix: avoid property setter for custom field --- erpnext/patches/v15_0/update_uae_zero_rated_fetch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py index a769cc0a85e..4ccdbb4229f 100644 --- a/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py +++ b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py @@ -1,9 +1,9 @@ import frappe -from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from erpnext.regional.united_arab_emirates.setup import make_custom_fields def execute(): if not frappe.db.get_value("Company", {"country": "United Arab Emirates"}): return - make_property_setter("Sales Invoice Item", "is_zero_rated", "fetch_if_empty", 1, "Check") + make_custom_fields() From eb6c8d8938226f12d517a59e2c9777f479001888 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Wed, 6 Aug 2025 11:40:33 +0530 Subject: [PATCH 6/9] chore: linters --- erpnext/patches/v15_0/update_uae_zero_rated_fetch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py index 4ccdbb4229f..57b8db59f97 100644 --- a/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py +++ b/erpnext/patches/v15_0/update_uae_zero_rated_fetch.py @@ -1,4 +1,5 @@ import frappe + from erpnext.regional.united_arab_emirates.setup import make_custom_fields From 38471995e76313b31cc37a52c8b2834744e2c46f Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Fri, 8 Aug 2025 09:58:16 +0530 Subject: [PATCH 7/9] fix: show message only if no tax is applied --- erpnext/regional/united_arab_emirates/utils.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index b2ee96eedfc..3cc34866f3c 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -21,10 +21,12 @@ def update_itemised_tax_data(doc): return False if not doc.customer_address: - frappe.msgprint( - _("Please set Customer Address to determine if the transaction is an export."), - alert=True, - ) + if not doc.total_taxes_and_charges: + frappe.msgprint( + _("Please set Customer Address to determine if the transaction is an export."), + alert=True, + ) + return False company_country = frappe.get_cached_value("Company", doc.company, "country") @@ -47,8 +49,10 @@ def update_itemised_tax_data(doc): tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate - if not tax_rate and is_export: - row.is_zero_rated = 1 + if not tax_rate: + row.is_zero_rated = ( + is_export or frappe.get_cached_value("Item", row.item_code, "is_zero_rated") + ) row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt(tax_amount, row.precision("tax_amount")) From c8940a39b357836e36095d07ef91f53789d5eff5 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Fri, 8 Aug 2025 10:02:35 +0530 Subject: [PATCH 8/9] chore: code styling --- erpnext/regional/united_arab_emirates/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index 3cc34866f3c..c4b7b033def 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -50,9 +50,7 @@ def update_itemised_tax_data(doc): tax_rate += _tax_rate if not tax_rate: - row.is_zero_rated = ( - is_export or frappe.get_cached_value("Item", row.item_code, "is_zero_rated") - ) + row.is_zero_rated = is_export or frappe.get_cached_value("Item", row.item_code, "is_zero_rated") row.tax_rate = flt(tax_rate, row.precision("tax_rate")) row.tax_amount = flt(tax_amount, row.precision("tax_amount")) From 29c3ef8280a36376cbb4ecc07d8467ecdd85ec41 Mon Sep 17 00:00:00 2001 From: Dany Robert Date: Fri, 8 Aug 2025 11:01:59 +0530 Subject: [PATCH 9/9] fix: handle case where taxes is added invoice changed to non-export later --- erpnext/regional/united_arab_emirates/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py index c4b7b033def..4cc805c8616 100644 --- a/erpnext/regional/united_arab_emirates/utils.py +++ b/erpnext/regional/united_arab_emirates/utils.py @@ -49,7 +49,7 @@ def update_itemised_tax_data(doc): tax_amount += flt((row.net_amount * _tax_rate) / 100, row.precision("tax_amount")) tax_rate += _tax_rate - if not tax_rate: + if not tax_rate or row.get("is_zero_rated"): row.is_zero_rated = is_export or frappe.get_cached_value("Item", row.item_code, "is_zero_rated") row.tax_rate = flt(tax_rate, row.precision("tax_rate"))