From cfaaf43381d2390379417e4d3c68100ef130b8e4 Mon Sep 17 00:00:00 2001 From: Mihir Kandoi Date: Mon, 22 Jun 2026 10:42:57 +0530 Subject: [PATCH] Revert "feat: Enhance UAE VAT Reports, UAE FTA Audit File and Add VAT Register" --- erpnext/patches.txt | 1 - .../v16_0/add_arabic_company_name_field.py | 21 - .../doctype/fta_audit_file/__init__.py | 0 .../doctype/fta_audit_file/fta_audit_file.js | 126 --- .../fta_audit_file/fta_audit_file.json | 221 ----- .../doctype/fta_audit_file/fta_audit_file.py | 807 ------------------ .../fta_audit_file/test_fta_audit_file.py | 238 ------ .../report/uae_vat_201/uae_vat_201.html | 127 ++- .../report/uae_vat_201/uae_vat_201.js | 5 +- .../report/uae_vat_201/uae_vat_201.py | 631 +++++--------- .../report/uae_vat_register/__init__.py | 0 .../uae_vat_register/uae_vat_register.js | 73 -- .../uae_vat_register/uae_vat_register.json | 24 - .../uae_vat_register/uae_vat_register.py | 197 ----- .../regional/united_arab_emirates/setup.py | 8 - 15 files changed, 279 insertions(+), 2200 deletions(-) delete mode 100644 erpnext/patches/v16_0/add_arabic_company_name_field.py delete mode 100644 erpnext/regional/doctype/fta_audit_file/__init__.py delete mode 100644 erpnext/regional/doctype/fta_audit_file/fta_audit_file.js delete mode 100644 erpnext/regional/doctype/fta_audit_file/fta_audit_file.json delete mode 100644 erpnext/regional/doctype/fta_audit_file/fta_audit_file.py delete mode 100644 erpnext/regional/doctype/fta_audit_file/test_fta_audit_file.py delete mode 100644 erpnext/regional/report/uae_vat_register/__init__.py delete mode 100644 erpnext/regional/report/uae_vat_register/uae_vat_register.js delete mode 100644 erpnext/regional/report/uae_vat_register/uae_vat_register.json delete mode 100644 erpnext/regional/report/uae_vat_register/uae_vat_register.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 19f2451576e..5db549806a3 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -489,4 +489,3 @@ erpnext.patches.v16_0.submit_existing_product_bundles #1 erpnext.patches.v16_0.migrate_subscription_generate_invoice_at erpnext.patches.v16_0.rename_subscription_billing_period_fields erpnext.patches.v16_0.drop_redundant_serial_no_index_from_sabb -erpnext.patches.v16_0.add_arabic_company_name_field diff --git a/erpnext/patches/v16_0/add_arabic_company_name_field.py b/erpnext/patches/v16_0/add_arabic_company_name_field.py deleted file mode 100644 index 100bf5502a4..00000000000 --- a/erpnext/patches/v16_0/add_arabic_company_name_field.py +++ /dev/null @@ -1,21 +0,0 @@ -import frappe -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields - - -def execute(): - if not frappe.db.exists("Company", {"country": "United Arab Emirates"}): - return - - create_custom_fields( - { - "Company": [ - { - "fieldname": "company_name_in_arabic", - "label": "Company Name in Arabic", - "fieldtype": "Data", - "insert_after": "company_name", - } - ] - }, - ignore_validate=True, - ) diff --git a/erpnext/regional/doctype/fta_audit_file/__init__.py b/erpnext/regional/doctype/fta_audit_file/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/regional/doctype/fta_audit_file/fta_audit_file.js b/erpnext/regional/doctype/fta_audit_file/fta_audit_file.js deleted file mode 100644 index 233819b190d..00000000000 --- a/erpnext/regional/doctype/fta_audit_file/fta_audit_file.js +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on("FTA Audit File", { - refresh: function (frm) { - // Generate FAF — available from Draft (first generation) and Error - // (retry after a previous attempt failed). Queued/Generating are - // blocked by the server guard; Generated/Submitted are intentionally - // not re-generable. - if (!frm.is_new() && ["Draft", "Error"].includes(frm.doc.status)) { - frm.add_custom_button( - __(frm.doc.status === "Error" ? "Retry FAF Generation" : "Generate FAF"), - function () { - frm.trigger("generate_faf"); - }, - __("Actions") - ); - } - - // Add Mark as Submitted button for Generated status - if (frm.doc.status === "Generated") { - frm.add_custom_button( - __("Mark as Submitted"), - function () { - frm.trigger("mark_submitted"); - }, - __("Actions") - ); - } - - // Add Download button if file exists - if (frm.doc.faf_file) { - frm.add_custom_button( - __("Download FAF"), - function () { - window.open(frm.doc.faf_file); - }, - __("Actions") - ); - } - - // Show status indicator - frm.trigger("set_status_indicator"); - }, - - generate_faf: function (frm) { - frappe.confirm( - __("This will generate the FTA Audit File for the selected period. Continue?"), - function () { - frm.call({ - doc: frm.doc, - method: "generate_faf", - freeze: true, - freeze_message: __("Queuing FTA Audit File generation..."), - }).then((r) => { - if (!r.message) return; - if (r.message.success) { - frappe.show_alert({ - message: r.message.message, - indicator: "green", - }); - } else { - frappe.msgprint({ - title: __("Generation Failed"), - message: r.message.message, - indicator: "red", - }); - } - frm.reload_doc(); - }); - } - ); - }, - - mark_submitted: function (frm) { - frappe.confirm( - __("Mark this FAF as submitted to FTA? This action is for record-keeping only."), - function () { - frm.call({ - doc: frm.doc, - method: "mark_as_submitted", - }).then((r) => { - if (r.message && r.message.success) { - frappe.show_alert({ - message: r.message.message, - indicator: "green", - }); - frm.reload_doc(); - } - }); - } - ); - }, - - set_status_indicator: function (frm) { - const status_colors = { - Draft: "orange", - Queued: "yellow", - Generating: "blue", - Generated: "green", - Submitted: "blue", - Error: "red", - }; - - if (frm.doc.status) { - frm.page.set_indicator(__(frm.doc.status), status_colors[frm.doc.status] || "gray"); - } - }, - - from_date: function (frm) { - frm.trigger("validate_dates"); - }, - - to_date: function (frm) { - frm.trigger("validate_dates"); - }, - - validate_dates: function (frm) { - if (frm.doc.from_date && frm.doc.to_date) { - if (frm.doc.from_date > frm.doc.to_date) { - frappe.msgprint(__("From Date cannot be after To Date")); - frm.set_value("to_date", null); - } - } - }, -}); diff --git a/erpnext/regional/doctype/fta_audit_file/fta_audit_file.json b/erpnext/regional/doctype/fta_audit_file/fta_audit_file.json deleted file mode 100644 index 061b50e219f..00000000000 --- a/erpnext/regional/doctype/fta_audit_file/fta_audit_file.json +++ /dev/null @@ -1,221 +0,0 @@ -{ - "actions": [], - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 1, - "creation": "2025-12-11 00:29:21.891860", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "naming_series", - "company", - "column_break_1", - "from_date", - "to_date", - "section_break_agent", - "tax_agency_name", - "tan", - "column_break_agent", - "tax_agent_name", - "taan", - "section_break_options", - "file_type", - "include_opening_balance", - "column_break_options_2", - "status", - "section_break_output", - "faf_file", - "section_break_logs", - "generation_log", - "error_message" - ], - "fields": [ - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Series", - "options": "FAF-.YYYY.-", - "reqd": 1, - "hidden": 1, - "default": "FAF-.YYYY.-" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Company", - "options": "Company", - "reqd": 1 - }, - { - "fieldname": "column_break_1", - "fieldtype": "Column Break" - }, - { - "fieldname": "from_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "From Date", - "reqd": 1 - }, - { - "fieldname": "to_date", - "fieldtype": "Date", - "in_list_view": 1, - "label": "To Date", - "reqd": 1 - }, - { - "fieldname": "section_break_agent", - "fieldtype": "Section Break", - "label": "Tax Agent (optional)", - "description": "Fill in only if the FAF is being filed through a registered Tax Agency or Tax Agent.", - "collapsible": 1, - "collapsible_depends_on": "eval:!(doc.tax_agency_name || doc.tan || doc.tax_agent_name || doc.taan)" - }, - { - "fieldname": "tax_agency_name", - "fieldtype": "Data", - "label": "Tax Agency Name", - "length": 100 - }, - { - "fieldname": "tan", - "fieldtype": "Data", - "label": "TAN (Tax Agency Number)", - "length": 20 - }, - { - "fieldname": "column_break_agent", - "fieldtype": "Column Break" - }, - { - "fieldname": "tax_agent_name", - "fieldtype": "Data", - "label": "Tax Agent Name", - "length": 100 - }, - { - "fieldname": "taan", - "fieldtype": "Data", - "label": "TAAN (Tax Agent Approval Number)", - "length": 20 - }, - { - "fieldname": "section_break_options", - "fieldtype": "Section Break", - "label": "Options" - }, - { - "fieldname": "file_type", - "fieldtype": "Select", - "label": "File Type", - "options": "VAT", - "default": "VAT", - "reqd": 1 - }, - { - "fieldname": "include_opening_balance", - "fieldtype": "Check", - "label": "Include Opening Balance in GL", - "description": "Carry each account's pre-period balance forward into the General Ledger Balance column. Off by default (period-only running balance), matching the lighter interpretation used by Microsoft Dynamics 365 Finance.", - "default": "0" - }, - { - "fieldname": "column_break_options_2", - "fieldtype": "Column Break" - }, - { - "fieldname": "status", - "fieldtype": "Select", - "label": "Status", - "options": "Draft\nQueued\nGenerating\nGenerated\nSubmitted\nError", - "default": "Draft", - "in_list_view": 1, - "read_only": 1 - }, - { - "fieldname": "section_break_output", - "fieldtype": "Section Break", - "label": "Generated File", - "depends_on": "eval:doc.status === 'Generated' || doc.status === 'Submitted'" - }, - { - "fieldname": "faf_file", - "fieldtype": "Attach", - "label": "FAF File", - "read_only": 1 - }, - { - "fieldname": "section_break_logs", - "fieldtype": "Section Break", - "label": "Generation Logs", - "collapsible": 1 - }, - { - "fieldname": "generation_log", - "fieldtype": "Long Text", - "label": "Generation Log", - "read_only": 1 - }, - { - "fieldname": "error_message", - "fieldtype": "Long Text", - "label": "Error Message", - "read_only": 1, - "depends_on": "eval:doc.status === 'Error'" - } - ], - "index_web_pages_for_search": 1, - "links": [], - "modified": "2025-12-11 00:29:21.891860", - "modified_by": "Administrator", - "module": "Regional", - "name": "FTA Audit File", - "naming_rule": "By \"Naming Series\" field", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [], - "title_field": "company", - "track_changes": 1 -} diff --git a/erpnext/regional/doctype/fta_audit_file/fta_audit_file.py b/erpnext/regional/doctype/fta_audit_file/fta_audit_file.py deleted file mode 100644 index e0bcf2b1635..00000000000 --- a/erpnext/regional/doctype/fta_audit_file/fta_audit_file.py +++ /dev/null @@ -1,807 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -""" -FTA Audit File DocType Controller - -Owns generation of FTA Audit Files (FAF) for UAE VAT compliance per the -FTA "Requirements Document for Tax Accounting Software" (October 2017), -Appendix 5. - -A conformant VAT FAF contains four CSV tables in this order, each -delimited by an explicit start/end marker row: - - 1. Company Information (CompInfoStart .. CompInfoEnd) - 2. Purchase Listing (PurcDataStart .. PurcDataEnd) - 3. Supply Listing (SuppDataStart .. SuppDataEnd) - 4. General Ledger (GLDataStart .. GLDataEnd) - -The footer of each transactional table carries running totals plus a -transaction count. Primary amount columns are in the company's accounting -currency (typically AED for a UAE-registered entity); foreign-currency -mirrors are emitted alongside when the source invoice is in a different -currency. -""" - -import csv -import io - -import frappe -from frappe import _ -from frappe.model.document import Document -from frappe.utils import flt, getdate, today -from frappe.utils.file_manager import save_file - -FAF_VERSION = "FAFv1.0.0" -DEFAULT_COUNTRY = "United Arab Emirates" -DEFAULT_DATE = "31-12-9999" -PRODUCT_VERSION = "ERPNext" - -# Number of GL Entry rows read into memory per batch when streaming the -# General Ledger section. Large UAE companies routinely produce hundreds -# of thousands of GL entries per year; reading them all into a single list -# would push the background worker over its memory limit. -GL_PAGE_SIZE = 5000 - -# Inputs that define what the FAF file represents. Once the file has been -# Generated or Submitted, changing any of these would silently desync the -# attached CSV from the form, so we lock them. -LOCKED_INPUT_FIELDS = ( - "company", - "from_date", - "to_date", - "file_type", - "include_opening_balance", - "tax_agency_name", - "tan", - "tax_agent_name", - "taan", -) -LOCKED_STATUSES = ("Generated", "Submitted") -IN_FLIGHT_STATUSES = ("Queued", "Generating") - - -class FTAAuditFile(Document): - def validate(self): - if getdate(self.from_date) > getdate(self.to_date): - frappe.throw(_("From Date cannot be after To Date")) - - if not frappe.db.get_value("Company", self.company, "tax_id"): - frappe.throw( - _("Company {0} does not have a Tax ID (TRN). Please set the Tax ID in Company.").format( - self.company - ) - ) - - self._guard_locked_fields() - - if self.status != "Error": - self.error_message = None - - def _guard_locked_fields(self): - """Block edits to FAF inputs once the file is Generated or Submitted. - - The status field alone is read-only in the UI, but a user with write - permission can still patch fields via REST or scripts; this enforces - immutability server-side so the attached CSV always matches the form. - """ - if self.is_new(): - return - - previous_status = self.get_db_value("status") - if previous_status not in LOCKED_STATUSES: - return - - changed = [f for f in LOCKED_INPUT_FIELDS if self.has_value_changed(f)] - if changed: - frappe.throw( - _("Cannot modify {0} after the FAF has been {1}.").format(", ".join(changed), previous_status) - ) - - @frappe.whitelist() - def generate_faf(self): - """Queue FAF generation as a background job. - - Returns immediately with status ``Queued``. The actual generation - runs in ``_run_generation`` on the ``long`` queue (Frappe's - ``enqueue_doc`` re-fetches a fresh doc inside the worker) and - updates ``status``, ``faf_file``, ``generation_log``, and - ``error_message`` when complete. - - Under ``frappe.flags.in_test`` the job runs synchronously so tests - can assert on the post-generation state without polling. - """ - # `@frappe.whitelist()` gates network access but not document write - # permission; without this explicit check, a user with read-only - # access could still trigger generation via REST. - self.check_permission("write") - - # Re-read status from DB so two concurrent button clicks can't both - # enqueue a job — the second one sees Queued/Generating and bails. - current_status = frappe.db.get_value(self.doctype, self.name, "status", for_update=True) - if current_status in IN_FLIGHT_STATUSES: - frappe.throw(_("FAF generation is already {0} for this document.").format(current_status)) - if current_status in ("Generated", "Submitted"): - # UI hides the Generate/Retry button for Generated and Submitted; - # enforce the same lifecycle on the REST endpoint so a direct - # call cannot silently overwrite the attached CSV. - frappe.throw(_("FAF is already {0}; create a new document to regenerate.").format(current_status)) - - self.status = "Queued" - self.generation_log = "" - self.error_message = None - self.save() - - frappe.enqueue_doc( - self.doctype, - self.name, - "_run_generation", - queue="long", - timeout=1500, - enqueue_after_commit=True, - now=bool(frappe.flags.in_test), - ) - - return { - "success": True, - "message": _("FAF generation has been queued. The status will update when complete."), - "docname": self.name, - "status": self.status, - } - - @frappe.whitelist() - def mark_as_submitted(self): - """Mark the FAF as submitted to the FTA portal (manual record).""" - self.check_permission("write") - if self.status != "Generated": - frappe.throw(_("Only Generated files can be marked as Submitted")) - self.status = "Submitted" - self.save() - return {"success": True, "message": _("FAF marked as submitted")} - - def _run_generation(self): - """Background entry point. Invoked via ``frappe.enqueue_doc`` from - ``generate_faf`` (or synchronously under ``frappe.flags.in_test``). - - Broad ``except`` is intentional: a long-running batch op records - the failure on the doc itself (status/error_message/log) so the - user sees what went wrong without the request 500'ing. The - exception is re-raised so the queue marks the job as failed and - the traceback is written to the Error Log. - """ - try: - self.status = "Generating" - self.save() - - result = self._build_faf() - self.faf_file = result["file_url"] - self.generation_log = result["log"] - self.status = "Generated" - self.save() - except Exception as e: - try: - err_doc = frappe.get_doc(self.doctype, self.name) - err_doc.status = "Error" - err_doc.error_message = str(e) - err_doc.generation_log = (err_doc.generation_log or "") + f"\n\nError: {e}" - err_doc.save() - except Exception: - # Don't lose the original failure if persisting the Error - # state itself fails (e.g. row lock, validation regression); - # log the secondary failure with context, then re-raise the - # original ``e`` below so the job is still marked failed. - frappe.log_error( - title=_("FAF Error-state persistence failed"), - message=f"{self.doctype} {self.name}\n\n{frappe.get_traceback()}", - ) - frappe.log_error( - title=_("FAF Generation Error"), - message=frappe.get_traceback(), - ) - raise - - def _build_faf(self): - """Build the FAF CSV per Appendix 5 and attach it to this document.""" - log_entries = [] - - def log(msg): - log_entries.append(msg) - - log(f"Starting FAF generation for {self.company}") - log(f"Period: {self.from_date} to {self.to_date}") - log(f"File Type: {self.file_type}") - - output = io.StringIO() - writer = csv.writer(output) - - self._write_company_info(writer) - log("Company Information written") - - purchase_count = self._write_purchase_listing(writer) - log(f"Purchase Listing written: {purchase_count} line items") - - supply_count = self._write_supply_listing(writer) - log(f"Supply Listing written: {supply_count} line items") - - gl_count = self._write_gl_listing(writer) - log(f"General Ledger written: {gl_count} entries") - - csv_content = output.getvalue() - output.close() - - file_name = f"FAF_{self.company}_{self.from_date}_to_{self.to_date}.csv".replace(" ", "_") - file_doc = save_file( - fname=file_name, - content=csv_content.encode("utf-8"), - dt="FTA Audit File", - dn=self.name, - is_private=1, - ) - - log(f"FAF file generated: {file_name}") - log("Generation completed successfully") - - return {"file_url": file_doc.file_url, "log": "\n".join(log_entries)} - - def _write_company_info(self, writer): - """Emit ``CompInfoStart`` + body row + ``CompInfoEnd`` per Appendix 5.""" - writer.writerow(["CompInfoStart"]) - - info = ( - frappe.db.get_value( - "Company", - self.company, - ["company_name", "company_name_in_arabic", "tax_id"], - as_dict=True, - ) - or {} - ) - - writer.writerow( - [ - _clean(info.get("company_name") or self.company), - _clean(info.get("company_name_in_arabic") or ""), - info.get("tax_id") or "", - _clean(self.tax_agency_name or ""), - _clean(self.tan or ""), - _clean(self.tax_agent_name or ""), - _clean(self.taan or ""), - _format_date(self.from_date), - _format_date(self.to_date), - _format_date(today()), - PRODUCT_VERSION, - FAF_VERSION, - ] - ) - - writer.writerow(["CompInfoEnd"]) - - def _write_purchase_listing(self, writer): - """Emit Purchase Listing per Appendix 5 with end-of-table totals row.""" - writer.writerow(["PurcDataStart"]) - - invoices = frappe.get_all( - "Purchase Invoice", - filters={ - "company": self.company, - "posting_date": ["between", [self.from_date, self.to_date]], - "docstatus": 1, - }, - fields=[ - "name", - "supplier", - "supplier_name", - "posting_date", - "permit_no", - "currency", - "conversion_rate", - ], - order_by="posting_date asc, name asc", - ) - if not invoices: - writer.writerow(["PurcDataEnd", _money(0), _money(0), 0]) - return 0 - - invoice_names = [inv.name for inv in invoices] - supplier_names = list({inv.supplier for inv in invoices if inv.supplier}) - - supplier_trn_map = _bulk_party_field("Supplier", supplier_names, "tax_id") - items_by_invoice = _bulk_invoice_items( - "Purchase Invoice Item", - invoice_names, - [ - "parent", - "idx", - "item_name", - "description", - "base_net_amount", - "net_amount", - "tax_amount", - "item_tax_template", - ], - ) - tax_code_bands = _bulk_tax_code_bands( - { - item.item_tax_template - for items in items_by_invoice.values() - for item in items - if item.item_tax_template - } - ) - - company_currency = _company_currency(self.company) - - total_purchase_company = 0.0 - total_vat_company = 0.0 - line_count = 0 - - for inv in invoices: - supplier_trn = supplier_trn_map.get(inv.supplier, "") - fcy_code, fcy_factor = _fcy_for_invoice(inv.currency, inv.conversion_rate, company_currency) - - for item in items_by_invoice.get(inv.name, []): - # base_net_amount is company-currency; tax_amount is a UAE - # custom field with options="currency" and therefore stored - # in the document's invoice currency. Multiply by the - # conversion rate to land in company currency. - net_company = flt(item.base_net_amount, 2) - vat_invoice = flt(item.tax_amount or 0, 2) - vat_company = flt(vat_invoice * fcy_factor, 2) - net_fcy = flt(item.net_amount or 0, 2) if fcy_code != "XXX" else 0.0 - vat_fcy = vat_invoice if fcy_code != "XXX" else 0.0 - - writer.writerow( - [ - _clean(inv.supplier_name), - supplier_trn, - _format_date(inv.posting_date), - inv.name, - inv.permit_no or "", - item.idx, - _clean(item.description or item.item_name or ""), - _money(net_company), - _money(vat_company), - _resolve_tax_code(item.item_tax_template, inv.posting_date, tax_code_bands), - fcy_code, - _money(net_fcy), - _money(vat_fcy), - ] - ) - total_purchase_company += net_company - total_vat_company += vat_company - line_count += 1 - - writer.writerow( - [ - "PurcDataEnd", - _money(total_purchase_company), - _money(total_vat_company), - line_count, - ] - ) - return line_count - - def _write_supply_listing(self, writer): - """Emit Supply Listing per Appendix 5 with end-of-table totals row.""" - writer.writerow(["SuppDataStart"]) - - invoices = frappe.get_all( - "Sales Invoice", - filters={ - "company": self.company, - "posting_date": ["between", [self.from_date, self.to_date]], - "docstatus": 1, - }, - fields=[ - "name", - "customer", - "customer_name", - "posting_date", - "currency", - "conversion_rate", - ], - order_by="posting_date asc, name asc", - ) - if not invoices: - writer.writerow(["SuppDataEnd", _money(0), _money(0), 0]) - return 0 - - invoice_names = [inv.name for inv in invoices] - customer_names = list({inv.customer for inv in invoices if inv.customer}) - - customer_trn_map = _bulk_party_field("Customer", customer_names, "tax_id") - customer_country_map = _bulk_party_country("Customer", customer_names) - items_by_invoice = _bulk_invoice_items( - "Sales Invoice Item", - invoice_names, - [ - "parent", - "idx", - "item_name", - "description", - "base_net_amount", - "net_amount", - "tax_amount", - "item_tax_template", - "is_zero_rated", - "is_exempt", - ], - ) - tax_code_bands = _bulk_tax_code_bands( - { - item.item_tax_template - for items in items_by_invoice.values() - for item in items - if item.item_tax_template - } - ) - - company_currency = _company_currency(self.company) - - total_supply_company = 0.0 - total_vat_company = 0.0 - line_count = 0 - - for inv in invoices: - customer_trn = customer_trn_map.get(inv.customer, "") - customer_country = customer_country_map.get(inv.customer) or DEFAULT_COUNTRY - fcy_code, fcy_factor = _fcy_for_invoice(inv.currency, inv.conversion_rate, company_currency) - - for item in items_by_invoice.get(inv.name, []): - # See _write_purchase_listing for the currency convention: - # tax_amount is invoice-currency, base_net_amount is - # company-currency, and fcy_factor converts invoice → company. - net_company = flt(item.base_net_amount, 2) - vat_invoice = flt(item.tax_amount or 0, 2) - vat_company = flt(vat_invoice * fcy_factor, 2) - net_fcy = flt(item.net_amount or 0, 2) if fcy_code != "XXX" else 0.0 - vat_fcy = vat_invoice if fcy_code != "XXX" else 0.0 - - if item.is_zero_rated: - tax_code = "ZR" - elif item.is_exempt: - tax_code = "EX" - else: - tax_code = _resolve_tax_code(item.item_tax_template, inv.posting_date, tax_code_bands) - - writer.writerow( - [ - _clean(inv.customer_name), - customer_trn, - _format_date(inv.posting_date), - inv.name, - item.idx, - _clean(item.description or item.item_name or ""), - _money(net_company), - _money(vat_company), - tax_code, - _clean(customer_country), - fcy_code, - _money(net_fcy), - _money(vat_fcy), - ] - ) - total_supply_company += net_company - total_vat_company += vat_company - line_count += 1 - - writer.writerow( - [ - "SuppDataEnd", - _money(total_supply_company), - _money(total_vat_company), - line_count, - ] - ) - return line_count - - def _write_gl_listing(self, writer): - """Emit General Ledger per Appendix 5 with end-of-table totals row. - - GL Entry rows are streamed in pages of ``GL_PAGE_SIZE`` to keep - memory bounded for multi-year exports on large companies; the - running-balance, account-name, and totals state survives across - pages so the output is identical to a single-fetch implementation. - """ - writer.writerow(["GLDataStart"]) - - company_currency = _company_currency(self.company) - base_filters = { - "company": self.company, - "posting_date": ["between", [self.from_date, self.to_date]], - "is_cancelled": 0, - } - - # Opening balances need every account that posts in the period up - # front; without that flag we cache account names lazily as we - # encounter them in each batch. - if self.include_opening_balance: - accounts_in_period = frappe.get_all( - "GL Entry", filters=base_filters, pluck="account", distinct=True - ) - running_balance = _opening_balances_by_account(self.company, self.from_date, accounts_in_period) - account_name_map = _bulk_party_field("Account", accounts_in_period, "account_name") - else: - running_balance = {} - account_name_map = {} - - source_type_map = { - "Sales Invoice": "AR", - "Purchase Invoice": "AP", - "Journal Entry": "General Journal", - "Payment Entry": "Cash Receipt", - "Stock Entry": "Inventory", - "Delivery Note": "Inventory Sale", - "Purchase Receipt": "Purchases", - } - - total_debit = 0.0 - total_credit = 0.0 - count = 0 - start = 0 - - while True: - batch = frappe.get_all( - "GL Entry", - filters=base_filters, - fields=[ - "name", - "posting_date", - "account", - "remarks", - "against", - "voucher_no", - "voucher_type", - "debit", - "credit", - ], - order_by="posting_date asc, creation asc", - limit_start=start, - limit_page_length=GL_PAGE_SIZE, - ) - if not batch: - break - - # Backfill the account-name cache for accounts new to this batch. - new_accounts = [e.account for e in batch if e.account and e.account not in account_name_map] - if new_accounts: - account_name_map.update(_bulk_party_field("Account", new_accounts, "account_name")) - - for entry in batch: - account_name = account_name_map.get(entry.account) or entry.account - source_type = source_type_map.get(entry.voucher_type, entry.voucher_type or "") - debit = flt(entry.debit, 2) - credit = flt(entry.credit, 2) - - running_balance[entry.account] = running_balance.get(entry.account, 0.0) + debit - credit - balance = flt(running_balance[entry.account], 2) - - writer.writerow( - [ - _format_date(entry.posting_date), - entry.account, - _clean(account_name), - _clean(entry.remarks or ""), - _clean(entry.against or ""), - entry.voucher_no, - entry.voucher_no, - source_type, - _money(debit), - _money(credit), - _money(balance), - ] - ) - total_debit += debit - total_credit += credit - count += 1 - - if len(batch) < GL_PAGE_SIZE: - break - start += GL_PAGE_SIZE - - writer.writerow( - [ - "GLDataEnd", - _money(total_debit), - _money(total_credit), - count, - company_currency, - ] - ) - return count - - -def _clean(value): - """Sanitize a string for FAF CSV. - - The spec mandates that the delimiter (``,``) must not appear inside any - field. We follow Microsoft Dynamics 365's UAE FAF convention and - substitute ``;`` so the original separator stays visible in the data. - Embedded newlines are stripped because they would otherwise break the - CSV row structure. - """ - if value is None: - return "" - return str(value).replace(",", ";").replace("\n", " ").replace("\r", " ").strip() - - -def _format_date(d): - """Format a date as DD-MM-YYYY per FTA spec; missing values become 31-12-9999.""" - if not d: - return DEFAULT_DATE - return getdate(d).strftime("%d-%m-%Y") - - -def _money(value): - """Format a numeric field as ``Decimal[14,2]`` per FTA spec. - - Python's ``csv.writer`` calls ``str()`` on numeric values, which - strips trailing zeros (``0.00`` → ``"0.0"``). The spec mandates two - decimal places everywhere a Decimal[14,2] field is emitted, so we - pre-format to a string here. - """ - return f"{flt(value):.2f}" - - -def _company_currency(company): - return frappe.db.get_value("Company", company, "default_currency") or "AED" - - -def _fcy_for_invoice(invoice_currency, conversion_rate, company_currency): - """Resolve foreign-currency code + conversion factor for an invoice. - - Returns ``("XXX", 1.0)`` when the invoice is in the company's home - currency (no FCY columns to populate); otherwise the ISO 4217 code - plus the conversion factor (rate to company currency). - """ - if not invoice_currency or invoice_currency == company_currency: - return ("XXX", 1.0) - return (invoice_currency, flt(conversion_rate) or 1.0) - - -def _bulk_party_field(doctype, names, field): - """Return ``{name: field_value}`` for the given names. Empty input → empty dict.""" - if not names: - return {} - rows = frappe.get_all( - doctype, - filters={"name": ["in", names]}, - fields=["name", field], - ) - return {r["name"]: (r.get(field) or "") for r in rows} - - -def _bulk_party_country(party_doctype, party_names): - """Return ``{party_name: country}`` for each party. - - Picks deterministically when a party has multiple addresses: prefer the - one flagged ``is_primary_address``, then ``is_shipping_address``, then - the lowest address name. Without this ordering, MariaDB would return - rows in storage-engine order and the FAF would be non-reproducible - across runs. - """ - if not party_names: - return {} - - dl = frappe.qb.DocType("Dynamic Link") - addr = frappe.qb.DocType("Address") - rows = ( - frappe.qb.from_(dl) - .inner_join(addr) - .on(addr.name == dl.parent) - .where(dl.link_doctype == party_doctype) - .where(dl.parenttype == "Address") - .where(dl.link_name.isin(party_names)) - .where(addr.country.isnotnull()) - .where(addr.country != "") - .select(dl.link_name, addr.country) - .orderby(addr.is_primary_address, order=frappe.qb.desc) - .orderby(addr.is_shipping_address, order=frappe.qb.desc) - .orderby(addr.name) - .run(as_dict=True) - ) - out = {} - for r in rows: - out.setdefault(r["link_name"], r["country"]) - return out - - -def _opening_balances_by_account(company, period_start, accounts): - """Net pre-period balance per account: sum(debit) - sum(credit) before ``period_start``. - - Returns ``{account: net}`` where net is debit-positive (positive for - asset/expense accounts that carry a debit balance, negative for - liability/equity/revenue accounts that carry a credit balance). - Single aggregated SQL via ``frappe.qb`` — one query regardless of the - number of accounts. Cancelled GL entries are excluded. - """ - if not accounts: - return {} - - from frappe.query_builder.functions import Sum - - gle = frappe.qb.DocType("GL Entry") - rows = ( - frappe.qb.from_(gle) - .where(gle.company == company) - .where(gle.posting_date < period_start) - .where(gle.is_cancelled == 0) - .where(gle.account.isin(accounts)) - .groupby(gle.account) - .select( - gle.account, - Sum(gle.debit).as_("debit"), - Sum(gle.credit).as_("credit"), - ) - .run(as_dict=True) - ) - return {r["account"]: flt(r.get("debit") or 0) - flt(r.get("credit") or 0) for r in rows} - - -def _bulk_invoice_items(child_doctype, invoice_names, fields): - """Return ``{parent_invoice: [items...]}`` for the given invoice names.""" - if not invoice_names: - return {} - items = frappe.get_all( - child_doctype, - filters={"parent": ["in", invoice_names]}, - fields=fields, - order_by="parent asc, idx asc", - ) - out = {} - for item in items: - out.setdefault(item["parent"], []).append(item) - return out - - -_FTA_TAX_CODES = ("SR", "ZR", "EX", "RC", "IG", "OA", "IA") - - -def _bulk_tax_code_bands(item_tax_templates): - """Return ``{template: [(valid_from, tax_category), ...]}`` sorted desc by valid_from. - - One ``Item Tax Template`` can have multiple ``Item Tax`` rows with - different ``valid_from`` dates (e.g. tax code changing on a regulator - cutover). Fetching them all up-front lets ``_resolve_tax_code`` pick - the row that was in force on each invoice's posting date without an - extra DB hit per line item. - """ - if not item_tax_templates: - return {} - rows = frappe.get_all( - "Item Tax", - filters={"item_tax_template": ["in", list(item_tax_templates)]}, - fields=["item_tax_template", "tax_category", "valid_from"], - order_by="valid_from desc", - ) - out = {} - for r in rows: - out.setdefault(r["item_tax_template"], []).append((r.get("valid_from"), r.get("tax_category"))) - return out - - -def _resolve_tax_code(item_tax_template, posting_date, bands_map): - """Derive FTA tax code (SR/ZR/EX/RC/IG/OA/IA) from one Item Tax Template. - - Picks the Item Tax row whose ``valid_from`` is the most recent value - that is still on or before ``posting_date``; rows with no - ``valid_from`` are treated as always-valid and used only as a - fallback. Defaults to ``SR`` (Standard Rated) when nothing matches or - the chosen ``tax_category`` isn't one of the FTA codes. - """ - if not item_tax_template: - return "SR" - - bands = bands_map.get(item_tax_template) or [] - posting = getdate(posting_date) if posting_date else None - fallback_category = None - for valid_from, tax_category in bands: - if valid_from is None: - fallback_category = fallback_category or tax_category - continue - if posting is None or getdate(valid_from) <= posting: - return tax_category if tax_category in _FTA_TAX_CODES else "SR" - - if fallback_category and fallback_category in _FTA_TAX_CODES: - return fallback_category - return "SR" diff --git a/erpnext/regional/doctype/fta_audit_file/test_fta_audit_file.py b/erpnext/regional/doctype/fta_audit_file/test_fta_audit_file.py deleted file mode 100644 index 2754c53cbba..00000000000 --- a/erpnext/regional/doctype/fta_audit_file/test_fta_audit_file.py +++ /dev/null @@ -1,238 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -import frappe -from frappe.tests.utils import FrappeTestCase - - -class TestFTAAuditFile(FrappeTestCase): - def setUp(self): - """Create a UAE test company with TRN before each test. - - Per-test creation (not setUpClass) because FrappeTestCase rolls - back the database after each test, including class-level fixtures. - """ - self.company = self._get_or_create_test_company() - - def _get_or_create_test_company(self): - company_name = "_Test Company UAE" - if not frappe.db.exists("Company", company_name): - frappe.get_doc( - { - "doctype": "Company", - "company_name": company_name, - "abbr": "_TCU", - "country": "United Arab Emirates", - "default_currency": "AED", - "tax_id": "100123456789012", - } - ).insert(ignore_permissions=True) - else: - company = frappe.get_doc("Company", company_name) - if not company.tax_id: - company.tax_id = "100123456789012" - company.save(ignore_permissions=True) - return company_name - - def test_fta_audit_file_creation(self): - """Test that FTA Audit File can be created.""" - doc = frappe.get_doc( - { - "doctype": "FTA Audit File", - "company": self.company, - "from_date": "2024-01-01", - "to_date": "2024-03-31", - "file_type": "VAT", - } - ) - doc.insert() - - self.assertTrue(doc.name) - self.assertEqual(doc.status, "Draft") - self.assertEqual(doc.file_type, "VAT") - - def test_date_validation(self): - """Test that from_date cannot be after to_date.""" - doc = frappe.get_doc( - { - "doctype": "FTA Audit File", - "company": self.company, - "from_date": "2024-03-31", - "to_date": "2024-01-01", - "file_type": "VAT", - } - ) - - self.assertRaises(frappe.ValidationError, doc.insert) - - def test_company_trn_validation(self): - """Test that company must have a TRN.""" - company_no_trn = "_Test Company No TRN" - - if not frappe.db.exists("Company", company_no_trn): - company = frappe.get_doc( - { - "doctype": "Company", - "company_name": company_no_trn, - "abbr": "_TCNT", - "country": "United Arab Emirates", - "default_currency": "AED", - } - ) - company.insert(ignore_permissions=True) - - doc = frappe.get_doc( - { - "doctype": "FTA Audit File", - "company": company_no_trn, - "from_date": "2024-01-01", - "to_date": "2024-03-31", - "file_type": "VAT", - } - ) - - self.assertRaises(frappe.ValidationError, doc.insert) - - def test_generate_faf_empty_period(self): - """End-to-end smoke test: generate against an empty period. - - With ``frappe.flags.in_test`` set by FrappeTestCase, the enqueued - job runs synchronously, so by the time generate_faf() returns the - doc has reached its terminal status. - """ - doc = frappe.get_doc( - { - "doctype": "FTA Audit File", - "company": self.company, - "from_date": "2099-01-01", - "to_date": "2099-01-31", - "file_type": "VAT", - } - ) - doc.insert() - - result = doc.generate_faf() - self.assertTrue(result["success"]) - - doc.reload() - self.assertEqual(doc.status, "Generated") - self.assertTrue(doc.faf_file) - - self.assertIn("Company Information written", doc.generation_log) - self.assertIn("Purchase Listing written", doc.generation_log) - self.assertIn("Supply Listing written", doc.generation_log) - self.assertIn("General Ledger written", doc.generation_log) - - def test_generate_faf_csv_structure(self): - """The generated CSV must contain the four spec section markers.""" - doc = frappe.get_doc( - { - "doctype": "FTA Audit File", - "company": self.company, - "from_date": "2099-02-01", - "to_date": "2099-02-28", - "file_type": "VAT", - } - ) - doc.insert() - doc.generate_faf() - doc.reload() - self.assertEqual(doc.status, "Generated") - - file_doc = frappe.get_doc("File", {"file_url": doc.faf_file}) - csv_content = file_doc.get_content() - if isinstance(csv_content, bytes): - csv_content = csv_content.decode("utf-8") - for marker in ( - "CompInfoStart", - "CompInfoEnd", - "PurcDataStart", - "PurcDataEnd", - "SuppDataStart", - "SuppDataEnd", - "GLDataStart", - "GLDataEnd", - ): - self.assertIn(marker, csv_content, f"Missing FAF section marker {marker!r}") - - self.assertIn("FAFv1.0.0", csv_content) - - def test_tax_agent_fields_appear_in_company_info(self): - """Tax Agency / Tax Agent details must round-trip into the CSV.""" - doc = frappe.get_doc( - { - "doctype": "FTA Audit File", - "company": self.company, - "from_date": "2099-04-01", - "to_date": "2099-04-30", - "file_type": "VAT", - "tax_agency_name": "Acme Tax Agency", - "tan": "TAN-555-001", - "tax_agent_name": "Jane Auditor", - "taan": "TAAN-777", - } - ) - doc.insert() - doc.generate_faf() - doc.reload() - self.assertEqual(doc.status, "Generated") - - file_doc = frappe.get_doc("File", {"file_url": doc.faf_file}) - csv_content = file_doc.get_content() - if isinstance(csv_content, bytes): - csv_content = csv_content.decode("utf-8") - - for value in ("Acme Tax Agency", "TAN-555-001", "Jane Auditor", "TAAN-777"): - self.assertIn(value, csv_content, f"Missing tax-agent value {value!r} in FAF") - - def test_decimal_fields_use_two_decimal_places(self): - """Decimal[14,2] cells must always emit two decimal places per spec.""" - doc = frappe.get_doc( - { - "doctype": "FTA Audit File", - "company": self.company, - "from_date": "2099-05-01", - "to_date": "2099-05-31", - "file_type": "VAT", - } - ) - doc.insert() - doc.generate_faf() - doc.reload() - - file_doc = frappe.get_doc("File", {"file_url": doc.faf_file}) - csv_content = file_doc.get_content() - if isinstance(csv_content, bytes): - csv_content = csv_content.decode("utf-8") - - self.assertIn("PurcDataEnd,0.00,0.00,0", csv_content) - self.assertIn("SuppDataEnd,0.00,0.00,0", csv_content) - self.assertIn("GLDataEnd,0.00,0.00,0,AED", csv_content) - - self.assertNotIn("PurcDataEnd,0.0,", csv_content) - self.assertNotIn("SuppDataEnd,0.0,", csv_content) - self.assertNotIn("GLDataEnd,0.0,", csv_content) - - def test_mark_as_submitted_workflow(self): - """Generated docs can be marked submitted; non-Generated cannot.""" - doc = frappe.get_doc( - { - "doctype": "FTA Audit File", - "company": self.company, - "from_date": "2099-05-01", - "to_date": "2099-05-31", - "file_type": "VAT", - } - ) - doc.insert() - - self.assertRaises(frappe.ValidationError, doc.mark_as_submitted) - - doc.generate_faf() - doc.reload() - self.assertEqual(doc.status, "Generated") - - result = doc.mark_as_submitted() - self.assertTrue(result["success"]) - doc.reload() - self.assertEqual(doc.status, "Submitted") diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.html b/erpnext/regional/report/uae_vat_201/uae_vat_201.html index b458e3d09ba..7328f3f218e 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.html +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.html @@ -1,104 +1,77 @@ {% - var report_columns = report.get_columns_for_print(); - report_columns = report_columns.filter(col => !col.hidden); + var report_columns = report.get_columns_for_print(); + report_columns = report_columns.filter(col => !col.hidden); %} -

{%= __(report.report_name) %}

+

{%= __(report.report_name) %}

-

{%= __("VAT on Sales and All Other Outputs") %}

+

{%= __("VAT on Sales and All Other Outputs") %}

+ - - - - - - + + + + {% for (let i=2; i{%= report_columns[i].label %} + {% } %} + - {% for (let j=1; j<13; j++) { %} - {% var row = data[j]; %} - {% if (row) { %} - - {% for (let i=0; i - {% const fieldname = report_columns[i].fieldname; %} - {% if (!is_null(row[fieldname])) { %} - {%= frappe.format(row[fieldname], report_columns[i], {}, row) %} - {% } %} - + {% for (let j=1; j<12; j++) { %} + {% + var row = data[j]; + %} + + {% for (let i=0; i + {% const fieldname = report_columns[i].fieldname; %} + {% if (!is_null(row[fieldname])) { %} + {%= frappe.format(row[fieldname], report_columns[i], {}, row) %} {% } %} - + {% } %} + {% } %}
{%= report_columns[0].label %}{%= report_columns[1].label %}{%= report_columns[2].label %}{%= report_columns[3].label %}
{%= report_columns[0].label %}{%= report_columns[1].label %}
-

{%= __("VAT on Expenses and All Other Inputs") %}

+

{%= __("VAT on Expenses and All Other Inputs") %}

- +
- - - - - - + + + + {% for (let i=2; i{%= report_columns[i].label %} + {% } %} + - {% for (let j=15; j<18; j++) { %} - {% var row = data[j]; %} - {% if (row) { %} - - {% for (let i=0; i - {% const fieldname = report_columns[i].fieldname; %} - {% if (!is_null(row[fieldname])) { %} - {%= frappe.format(row[fieldname], report_columns[i], {}, row) %} - {% } %} - + {% for (let j=14; j + {% for (let i=0; i + {% const fieldname = report_columns[i].fieldname; %} + {% if (!is_null(row[fieldname])) { %} + {%= frappe.format(row[fieldname], report_columns[i], {}, row) %} {% } %} - + {% } %} - {% } %} - -
{%= report_columns[0].label %}{%= report_columns[1].label %}{%= report_columns[2].label %}{%= report_columns[3].label %}
{%= report_columns[0].label %}{%= report_columns[1].label %}
- -

{%= __("Net VAT Due") %}

- - - - - - - - - - {% for (let j=20; j<23; j++) { %} - {% var row = data[j]; %} - {% if (row) { %} - - - - - - {% } %} {% } %} +
{%= report_columns[0].label %}{%= report_columns[1].label %}{%= report_columns[3].label %}
{%= row.no %}{%= row.legend %}{%= row.vat_amount %}
diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.js b/erpnext/regional/report/uae_vat_201/uae_vat_201.js index 21c8cba72ac..e62d3395f20 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.js +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.js @@ -33,14 +33,11 @@ frappe.query_reports["UAE VAT 201"] = { default: frappe.datetime.get_today(), }, ], - formatter: function (value, row, column, data, default_formatter) { if ( data && (data.legend == "VAT on Sales and All Other Outputs" || - data.legend == "VAT on Expenses and All Other Inputs" || - data.legend == "Net VAT Due" || - data.legend == "Total") && + data.legend == "VAT on Expenses and All Other Inputs") && data.legend == value ) { value = $(`${value}`); 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 1459602ac02..aaca8f01654 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -2,75 +2,17 @@ # For license information, please see license.txt -from html import escape -from urllib.parse import urlencode - import frappe from frappe import _ -from frappe.query_builder.functions import Coalesce, Sum -from frappe.utils import flt +from frappe.query_builder.functions import Sum from erpnext import get_region -# Per-request memoization cache for the helper functions below. Stored on -# ``frappe.local`` so concurrent requests under gevent/threaded workers -# never share or race on this state; cleared at the start of every -# ``execute()`` so each report run gets fresh data. -_CACHE_ATTR = "_uae_vat_201_cache" - - -def _get_cache(): - cache = getattr(frappe.local, _CACHE_ATTR, None) - if cache is None: - cache = {} - setattr(frappe.local, _CACHE_ATTR, cache) - return cache - - -def _drill_down_link(text, filters, **extra): - """Return an `` tag pointing at the UAE VAT Register report. - - Filter values are URL-encoded so company names with ``&`` or other - reserved characters don't break the query string, and the link text - is HTML-escaped to prevent injection from user-controlled fields. - """ - params = {} - for key in ("company", "from_date", "to_date"): - value = (filters or {}).get(key) - if value: - params[key] = value - for key, value in extra.items(): - if value is not None: - params[key] = value - query = urlencode(params) - return f'{escape(str(text))}' - - -def _cached(fn): - def wrapper(filters, *args, **kwargs): - # ``frappe.local`` survives across unit-test methods (it is request - # scoped, not test scoped). Two tests that call the same helper with - # equivalent filter dicts would otherwise share a cached value from - # the first test's data set. Bypass the cache in tests so each - # call hits the DB; production callers (one execute() per HTTP - # request, cache cleared at its start) still see the optimisation. - if frappe.flags.in_test: - return fn(filters, *args, **kwargs) - cache = _get_cache() - key = (fn.__name__, tuple(sorted((filters or {}).items()))) - if key not in cache: - cache[key] = fn(filters, *args, **kwargs) - return cache[key] - - return wrapper - def execute(filters=None): - filters = filters or {} validate_company_region(filters) - _get_cache().clear() columns = get_columns() - data = get_data(filters) + data, emirates, amounts_by_emirate = get_data(filters) return columns, data @@ -106,86 +48,23 @@ def get_columns(): def get_data(filters=None): """Returns the list of dictionaries. Each dictionary is a row in the datatable and chart data.""" data = [] - amounts_by_emirate = append_vat_on_sales(data, filters) + emirates, amounts_by_emirate = append_vat_on_sales(data, filters) append_vat_on_expenses(data, filters) - net_vat_due(data, filters, amounts_by_emirate) - - dubai_label_override = _company_emirate_label(filters) - - final_data = [] - for row in data: - key = row.get("_key") - legend = row.get("legend") - new_legend = legend - - if key and key.startswith("emirate:"): - emirate = key.split(":", 1)[1] - label = dubai_label_override if emirate == "Dubai" and dubai_label_override else legend - new_legend = _drill_down_link( - label, filters, doc_type="Sales Invoice", vat=emirate, category="Standard" - ) - elif key == "reverse_charge_supplies": - new_legend = _drill_down_link(legend, filters, doc_type="Purchase Invoice", reverse_charge="Y") - elif key == "zero_rated": - new_legend = _drill_down_link(legend, filters, doc_type="Sales Invoice", category="Zero Rated") - elif key == "exempt_supplies": - new_legend = _drill_down_link(legend, filters, doc_type="Sales Invoice", category="Exempt Rated") - elif key == "standard_rated_expenses": - new_legend = _drill_down_link(legend, filters, doc_type="Purchase Invoice") - - final_data.append( - { - "no": row.get("no"), - "legend": new_legend, - "amount": row.get("amount"), - "vat_amount": row.get("vat_amount"), - } - ) - - return final_data - - -def _company_emirate_label(filters): - """Return the home-emirate label for the company in ``filters`` if any. - - The Dubai row is conventionally relabeled with the actual emirate of - the filtered company's primary address. Falls back to ``None`` when - no company filter is set or the address has no emirate, in which case - callers keep the original "Standard rated supplies in Dubai" wording. - """ - company = (filters or {}).get("company") - if not company: - return None - address = frappe.get_all( - "Address", - filters=[ - ["Dynamic Link", "link_doctype", "=", "Company"], - ["Dynamic Link", "link_name", "=", company], - ["Address", "is_your_company_address", "=", 1], - ], - fields=["emirate"], - limit=1, - ) - if address and address[0].get("emirate"): - return _("Standard rated supplies in {0}").format(address[0]["emirate"]) - return None + return data, emirates, amounts_by_emirate def append_vat_on_sales(data, filters): """Appends Sales and All Other Outputs.""" append_data(data, "", _("VAT on Sales and All Other Outputs"), "", "") - amounts_by_emirate = standard_rated_expenses_emiratewise(data, filters) - - si_amount = amounts_by_emirate[1] - si_vat = amounts_by_emirate[2] + emirates, amounts_by_emirate = standard_rated_expenses_emiratewise(data, filters) append_data( data, "2", _("Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme"), - format_currency_signed((-1) * get_tourist_tax_return_total(filters)), - format_currency_signed((-1) * get_tourist_tax_return_tax(filters)), + frappe.format((-1) * get_tourist_tax_return_total(filters), "Currency"), + frappe.format((-1) * get_tourist_tax_return_tax(filters), "Currency"), ) append_data( @@ -194,48 +73,15 @@ def append_vat_on_sales(data, filters): _("Supplies subject to the reverse charge provision"), frappe.format(get_reverse_charge_total(filters), "Currency"), frappe.format(get_reverse_charge_tax(filters), "Currency"), - key="reverse_charge_supplies", ) - append_data( - data, - "4", - _("Zero Rated"), - frappe.format(get_zero_rated_total(filters), "Currency"), - "-", - key="zero_rated", - ) + append_data(data, "4", _("Zero Rated"), frappe.format(get_zero_rated_total(filters), "Currency"), "-") - append_data( - data, - "5", - _("Exempt Supplies"), - frappe.format(get_exempt_total(filters), "Currency"), - "-", - key="exempt_supplies", - ) - - append_data( - data, - "8", - _("Total"), - frappe.format( - (-1) * get_tourist_tax_return_total(filters) - + get_reverse_charge_total(filters) - + get_zero_rated_total(filters) - + get_exempt_total(filters) - + sum(si_amount), - "Currency", - ), - frappe.format( - (-1) * get_tourist_tax_return_tax(filters) + get_reverse_charge_tax(filters) + sum(si_vat), - "Currency", - ), - ) + append_data(data, "5", _("Exempt Supplies"), frappe.format(get_exempt_total(filters), "Currency"), "-") append_data(data, "", "", "", "") - return amounts_by_emirate + return emirates, amounts_by_emirate def standard_rated_expenses_emiratewise(data, filters): @@ -252,22 +98,16 @@ def standard_rated_expenses_emiratewise(data, filters): "vat_amount": frappe.format(vat, "Currency"), } amounts_by_emirate = append_emiratewise_expenses(data, emirates, amounts_by_emirate) - return amounts_by_emirate + return emirates, amounts_by_emirate def append_emiratewise_expenses(data, emirates, amounts_by_emirate): """Append emiratewise standard rated expenses and vat.""" - s_amount = [] - v_amount = [] for no, emirate in enumerate(emirates, 97): if emirate in amounts_by_emirate: amounts_by_emirate[emirate]["no"] = _("1{0}").format(chr(no)) amounts_by_emirate[emirate]["legend"] = _("Standard rated supplies in {0}").format(emirate) - amounts_by_emirate[emirate]["_key"] = f"emirate:{emirate}" data.append(amounts_by_emirate[emirate]) - - s_amount.append(amounts_by_emirate[emirate].get("raw_amount") or 0) - v_amount.append(amounts_by_emirate[emirate].get("raw_vat_amount") or 0) else: append_data( data, @@ -275,9 +115,8 @@ def append_emiratewise_expenses(data, emirates, amounts_by_emirate): _("Standard rated supplies in {0}").format(emirate), frappe.format(0, "Currency"), frappe.format(0, "Currency"), - key=f"emirate:{emirate}", ) - return amounts_by_emirate, s_amount, v_amount + return amounts_by_emirate def append_vat_on_expenses(data, filters): @@ -289,7 +128,6 @@ def append_vat_on_expenses(data, filters): _("Standard Rated Expenses"), frappe.format(get_standard_rated_expenses_total(filters), "Currency"), frappe.format(get_standard_rated_expenses_tax(filters), "Currency"), - key="standard_rated_expenses", ) append_data( data, @@ -299,103 +137,30 @@ def append_vat_on_expenses(data, filters): frappe.format(get_reverse_charge_recoverable_tax(filters), "Currency"), ) - append_data( - data, - "11", - _("Total"), - frappe.format( - get_standard_rated_expenses_total(filters) + get_reverse_charge_recoverable_total(filters), - "Currency", - ), - frappe.format( - get_standard_rated_expenses_tax(filters) + get_reverse_charge_recoverable_tax(filters), - "Currency", - ), - ) + +def append_data(data, no, legend, amount, vat_amount): + """Returns data with appended value.""" + data.append({"no": no, "legend": legend, "amount": amount, "vat_amount": vat_amount}) -def net_vat_due(data, filters, amounts_by_emirate): - si_vat = amounts_by_emirate[2] - - append_data(data, "", "", "", "") - append_data(data, "", _("Net VAT Due"), "", "") - append_data( - data, - "12", - _("Total value of due tax for the period"), - frappe.format(0.00, "Currency"), - frappe.format( - sum(si_vat) + (-1) * get_tourist_tax_return_tax(filters) + get_reverse_charge_tax(filters), - "Currency", - ), - ) - append_data( - data, - "13", - _("Total value of recoverable tax for the period"), - frappe.format(0.00, "Currency"), - frappe.format( - get_standard_rated_expenses_tax(filters) + get_reverse_charge_recoverable_tax(filters), - "Currency", - ), - ) - - # Calculate payable tax: Due Tax - Recoverable Tax - due_tax = sum(si_vat) + (-1) * get_tourist_tax_return_tax(filters) + get_reverse_charge_tax(filters) - recoverable_tax = get_standard_rated_expenses_tax(filters) + get_reverse_charge_recoverable_tax(filters) - payable_tax = due_tax - recoverable_tax - - append_data( - data, - "14", - _("Payable tax for the period"), - frappe.format(0.00, "Currency"), - frappe.format(payable_tax, "Currency"), - ) - - -def append_data(data, no, legend, amount, vat_amount, key=None): - """Append one row to ``data``. - - ``key`` (when provided) is a language-independent identifier used by - ``get_data`` to decide which rows get drill-down links. Without it, - dispatch would have to match the localized ``legend`` text and would - silently break under any non-English language. - """ - data.append({"no": no, "legend": legend, "amount": amount, "vat_amount": vat_amount, "_key": key}) - - -def format_currency_signed(value): - """Format a number as currency, placing the minus sign *before* the currency symbol - when negative (e.g. "-د.إ 5,000.00" rather than "د.إ -5,000.00").""" - if value is None: - value = 0 - if value < 0: - return "-" + frappe.format(abs(value), "Currency") - return frappe.format(value, "Currency") - - -@_cached def get_total_emiratewise(filters): """Returns Emiratewise Amount and Taxes.""" - si = frappe.qb.DocType("Sales Invoice") - sii = frappe.qb.DocType("Sales Invoice Item") + i = frappe.qb.DocType("Sales Invoice Item") + s = frappe.qb.DocType("Sales Invoice") query = ( - frappe.qb.from_(sii) - .inner_join(si) - .on(sii.parent == si.name) - .where(si.docstatus == 1) - .where(sii.is_exempt != 1) - .where(sii.is_zero_rated != 1) - .groupby(si.vat_emirate) - .select( - si.vat_emirate.as_("emirate"), - Coalesce(Sum(sii.base_net_amount), 0).as_("total"), - Coalesce(Sum(sii.tax_amount), 0), - ) + frappe.qb.from_(i) + .inner_join(s) + .on(i.parent == s.name) + .select(s.vat_emirate.as_("emirate"), Sum(i.base_net_amount).as_("total"), Sum(i.tax_amount)) + .where((s.docstatus == 1) & (i.is_exempt != 1) & (i.is_zero_rated != 1)) + .groupby(s.vat_emirate) ) - query = _apply_period_filters(query, si, filters) - return query.run() + for condition in get_conditions(filters, s): + query = query.where(condition) + try: + return query.run() + except (IndexError, TypeError): + return 0 def get_emirates(): @@ -403,185 +168,245 @@ def get_emirates(): return ["Abu Dhabi", "Dubai", "Sharjah", "Ajman", "Umm Al Quwain", "Ras Al Khaimah", "Fujairah"] -def _apply_period_filters(query, table, filters): - """Apply company / posting-date filters from ``filters`` to a frappe.qb query.""" - filters = filters or {} +def get_filters(filters): + """The conditions to be used to filter data to calculate the total sale.""" + query_filters = [] if filters.get("company"): - query = query.where(table.company == filters["company"]) + query_filters.append(["company", "=", filters["company"]]) if filters.get("from_date"): - query = query.where(table.posting_date >= filters["from_date"]) - if filters.get("to_date"): - query = query.where(table.posting_date <= filters["to_date"]) - return query + query_filters.append(["posting_date", ">=", filters["from_date"]]) + if filters.get("from_date"): + query_filters.append(["posting_date", "<=", filters["to_date"]]) + return query_filters -def _sum_invoice_field(doctype, field, filters, extra_where=None): - """Return ``sum(field)`` on a submitted invoice doctype with the standard - period filters. ``extra_where(table)`` may yield additional ``Criterion``s.""" - table = frappe.qb.DocType(doctype) - query = frappe.qb.from_(table).where(table.docstatus == 1).select(Coalesce(Sum(table[field]), 0)) - query = _apply_period_filters(query, table, filters) - if extra_where is not None: - for criterion in extra_where(table): - query = query.where(criterion) - result = query.run() - return flt(result[0][0]) if result else 0 - - -def _sum_item_field(parent_doctype, child_doctype, field, filters, extra_item_where=None): - """Return ``sum(child.field)`` for child rows of submitted parents in the period.""" - parent = frappe.qb.DocType(parent_doctype) - child = frappe.qb.DocType(child_doctype) - query = ( - frappe.qb.from_(child) - .inner_join(parent) - .on(child.parent == parent.name) - .where(parent.docstatus == 1) - .select(Coalesce(Sum(child[field]), 0)) - ) - query = _apply_period_filters(query, parent, filters) - if extra_item_where is not None: - for criterion in extra_item_where(child): - query = query.where(criterion) - result = query.run() - return flt(result[0][0]) if result else 0 - - -def _sum_vat_account_debit(filters, recoverable=False): - """Sum of GL debit for reverse-charge purchases booked to UAE VAT Accounts. - - With ``recoverable=True``, multiplies the debit by the invoice's - ``recoverable_reverse_charge`` percentage (and only sums rows with a - non-zero recoverable rate). Returns 0 when no company filter is set, - since UAE VAT Accounts are scoped per company. - """ - if not (filters or {}).get("company"): - return 0 - - pi = frappe.qb.DocType("Purchase Invoice") - gl = frappe.qb.DocType("GL Entry") - uva = frappe.qb.DocType("UAE VAT Account") - - vat_accounts = frappe.qb.from_(uva).where(uva.parent == filters["company"]).select(uva.account) - - amount = gl.debit - if recoverable: - amount = amount * pi.recoverable_reverse_charge / 100 - - query = ( - frappe.qb.from_(pi) - .inner_join(gl) - .on(gl.voucher_no == pi.name) - .where(pi.reverse_charge == "Y") - .where(pi.docstatus == 1) - .where(gl.docstatus == 1) - .where(gl.account.isin(vat_accounts)) - .select(Coalesce(Sum(amount), 0)) - ) - if recoverable: - query = query.where(pi.recoverable_reverse_charge > 0) - query = _apply_period_filters(query, pi, filters) - result = query.run() - return flt(result[0][0]) if result else 0 - - -@_cached def get_reverse_charge_total(filters): """Returns the sum of the total of each Purchase invoice made.""" - return _sum_invoice_field( - "Purchase Invoice", - "base_net_total", - filters, - extra_where=lambda t: [t.reverse_charge == "Y"], - ) + query_filters = get_filters(filters) + query_filters.append(["reverse_charge", "=", "Y"]) + query_filters.append(["docstatus", "=", 1]) + try: + return ( + frappe.db.get_all( + "Purchase Invoice", + filters=query_filters, + fields=[{"SUM": "base_total"}], + as_list=True, + limit=1, + )[0][0] + or 0 + ) + except (IndexError, TypeError): + return 0 -@_cached def get_reverse_charge_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" - return _sum_vat_account_debit(filters) + p = frappe.qb.DocType("Purchase Invoice") + gl = frappe.qb.DocType("GL Entry") + uae_vat = frappe.qb.DocType("UAE VAT Account") + query = ( + frappe.qb.from_(p) + .inner_join(gl) + .on(gl.voucher_no == p.name) + .select(Sum(gl.debit)) + .where( + (p.reverse_charge == "Y") + & (p.docstatus == 1) + & (gl.docstatus == 1) + & gl.account.isin( + frappe.qb.from_(uae_vat) + .select(uae_vat.account) + .where(uae_vat.parent == filters.get("company")) + ) + ) + ) + for condition in get_conditions_join(filters, p): + query = query.where(condition) + return query.run()[0][0] or 0 -@_cached def get_reverse_charge_recoverable_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" - return _sum_invoice_field( - "Purchase Invoice", - "base_net_total", - filters, - extra_where=lambda t: [t.reverse_charge == "Y", t.recoverable_reverse_charge > 0], - ) + query_filters = get_filters(filters) + query_filters.append(["reverse_charge", "=", "Y"]) + query_filters.append(["recoverable_reverse_charge", ">", "0"]) + query_filters.append(["docstatus", "=", 1]) + try: + return ( + frappe.db.get_all( + "Purchase Invoice", + filters=query_filters, + fields=[{"SUM": "base_total"}], + as_list=True, + limit=1, + )[0][0] + or 0 + ) + except (IndexError, TypeError): + return 0 -@_cached def get_reverse_charge_recoverable_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" - return _sum_vat_account_debit(filters, recoverable=True) + p = frappe.qb.DocType("Purchase Invoice") + gl = frappe.qb.DocType("GL Entry") + uae_vat = frappe.qb.DocType("UAE VAT Account") + query = ( + frappe.qb.from_(p) + .inner_join(gl) + .on(gl.voucher_no == p.name) + .select(Sum(gl.debit * p.recoverable_reverse_charge / 100)) + .where( + (p.reverse_charge == "Y") + & (p.docstatus == 1) + & (p.recoverable_reverse_charge > 0) + & (gl.docstatus == 1) + & gl.account.isin( + frappe.qb.from_(uae_vat) + .select(uae_vat.account) + .where(uae_vat.parent == filters.get("company")) + ) + ) + ) + for condition in get_conditions_join(filters, p): + query = query.where(condition) + return query.run()[0][0] or 0 + + +def get_conditions_join(filters, p): + """The conditions to be used to filter data to calculate the total vat.""" + conditions = [] + if filters.get("company"): + conditions.append(p.company == filters.get("company")) + if filters.get("from_date"): + conditions.append(p.posting_date >= filters.get("from_date")) + if filters.get("to_date"): + conditions.append(p.posting_date <= filters.get("to_date")) + return conditions -@_cached def get_standard_rated_expenses_total(filters): """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge.""" - return _sum_invoice_field( - "Purchase Invoice", - "base_net_total", - filters, - extra_where=lambda t: [t.recoverable_standard_rated_expenses > 0], - ) + query_filters = get_filters(filters) + query_filters.append(["recoverable_standard_rated_expenses", ">", 0]) + query_filters.append(["docstatus", "=", 1]) + try: + return ( + frappe.db.get_all( + "Purchase Invoice", + filters=query_filters, + fields=[{"SUM": "base_total"}], + as_list=True, + limit=1, + )[0][0] + or 0 + ) + except (IndexError, TypeError): + return 0 -@_cached def get_standard_rated_expenses_tax(filters): """Returns the sum of the tax of each Purchase invoice made.""" - return _sum_invoice_field( - "Purchase Invoice", - "recoverable_standard_rated_expenses", - filters, - extra_where=lambda t: [t.recoverable_standard_rated_expenses > 0], - ) + query_filters = get_filters(filters) + query_filters.append(["recoverable_standard_rated_expenses", ">", 0]) + query_filters.append(["docstatus", "=", 1]) + try: + return ( + frappe.db.get_all( + "Purchase Invoice", + filters=query_filters, + fields=[{"SUM": "recoverable_standard_rated_expenses"}], + as_list=True, + limit=1, + )[0][0] + or 0 + ) + except (IndexError, TypeError): + return 0 -@_cached def get_tourist_tax_return_total(filters): """Returns the sum of the total of each Sales invoice with non zero tourist_tax_return.""" - return _sum_invoice_field( - "Sales Invoice", - "base_net_total", - filters, - extra_where=lambda t: [t.tourist_tax_return > 0], - ) + query_filters = get_filters(filters) + query_filters.append(["tourist_tax_return", ">", 0]) + query_filters.append(["docstatus", "=", 1]) + try: + return ( + frappe.db.get_all( + "Sales Invoice", filters=query_filters, fields=[{"SUM": "base_total"}], as_list=True, limit=1 + )[0][0] + or 0 + ) + except (IndexError, TypeError): + return 0 -@_cached def get_tourist_tax_return_tax(filters): """Returns the sum of the tax of each Sales invoice with non zero tourist_tax_return.""" - return _sum_invoice_field( - "Sales Invoice", - "tourist_tax_return", - filters, - extra_where=lambda t: [t.tourist_tax_return > 0], - ) + query_filters = get_filters(filters) + query_filters.append(["tourist_tax_return", ">", 0]) + query_filters.append(["docstatus", "=", 1]) + try: + return ( + frappe.db.get_all( + "Sales Invoice", + filters=query_filters, + fields=[{"SUM": "tourist_tax_return"}], + as_list=True, + limit=1, + )[0][0] + or 0 + ) + except (IndexError, TypeError): + return 0 -@_cached def get_zero_rated_total(filters): """Returns the sum of each Sales Invoice Item Amount which is zero rated.""" - return _sum_item_field( - "Sales Invoice", - "Sales Invoice Item", - "base_net_amount", - filters, - extra_item_where=lambda i: [i.is_zero_rated == 1], + i = frappe.qb.DocType("Sales Invoice Item") + s = frappe.qb.DocType("Sales Invoice") + query = ( + frappe.qb.from_(i) + .inner_join(s) + .on(i.parent == s.name) + .select(Sum(i.base_net_amount).as_("total")) + .where((s.docstatus == 1) & (i.is_zero_rated == 1)) ) + for condition in get_conditions(filters, s): + query = query.where(condition) + try: + return query.run()[0][0] or 0 + except (IndexError, TypeError): + return 0 -@_cached def get_exempt_total(filters): """Returns the sum of each Sales Invoice Item Amount which is Vat Exempt.""" - return _sum_item_field( - "Sales Invoice", - "Sales Invoice Item", - "base_net_amount", - filters, - extra_item_where=lambda i: [i.is_exempt == 1], + i = frappe.qb.DocType("Sales Invoice Item") + s = frappe.qb.DocType("Sales Invoice") + query = ( + frappe.qb.from_(i) + .inner_join(s) + .on(i.parent == s.name) + .select(Sum(i.base_net_amount).as_("total")) + .where((s.docstatus == 1) & (i.is_exempt == 1)) ) + for condition in get_conditions(filters, s): + query = query.where(condition) + try: + return query.run()[0][0] or 0 + except (IndexError, TypeError): + return 0 + + +def get_conditions(filters, s): + """The conditions to be used to filter data to calculate the total sale.""" + conditions = [] + if filters.get("company"): + conditions.append(s.company == filters.get("company")) + if filters.get("from_date"): + conditions.append(s.posting_date >= filters.get("from_date")) + if filters.get("to_date"): + conditions.append(s.posting_date <= filters.get("to_date")) + return conditions diff --git a/erpnext/regional/report/uae_vat_register/__init__.py b/erpnext/regional/report/uae_vat_register/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/regional/report/uae_vat_register/uae_vat_register.js b/erpnext/regional/report/uae_vat_register/uae_vat_register.js deleted file mode 100644 index 232aa7df691..00000000000 --- a/erpnext/regional/report/uae_vat_register/uae_vat_register.js +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.query_reports["UAE VAT Register"] = { - filters: [ - { - fieldname: "company", - label: __("Company"), - fieldtype: "Link", - options: "Company", - reqd: 1, - default: frappe.defaults.get_user_default("Company"), - }, - { - fieldname: "from_date", - label: __("From Date"), - fieldtype: "Date", - reqd: 1, - default: frappe.datetime.add_months(frappe.datetime.get_today(), -3), - }, - { - fieldname: "to_date", - label: __("To Date"), - fieldtype: "Date", - reqd: 1, - default: frappe.datetime.get_today(), - }, - { - fieldname: "doc_type", - label: __("Document Type"), - fieldtype: "Select", - options: ["Sales Invoice", "Purchase Invoice"], - default: "Sales Invoice", - reqd: 1, - }, - { - fieldname: "category", - label: __("Category"), - fieldtype: "Select", - options: ["", "Standard", "Zero Rated", "Exempt Rated"], - depends_on: "eval: doc.doc_type == 'Sales Invoice'", - }, - { - fieldname: "vat", - label: __("Emirate"), - fieldtype: "Select", - options: [ - "", - "Abu Dhabi", - "Dubai", - "Sharjah", - "Ajman", - "Umm Al Quwain", - "Ras Al Khaimah", - "Fujairah", - ], - depends_on: "eval: doc.doc_type == 'Sales Invoice'", - }, - { - fieldname: "reverse_charge", - label: __("Reverse Charge"), - fieldtype: "Select", - options: ["", "Y", "N"], - depends_on: "eval: doc.doc_type == 'Purchase Invoice'", - }, - { - fieldname: "item_wise", - label: __("Item-wise"), - fieldtype: "Check", - default: 0, - }, - ], -}; diff --git a/erpnext/regional/report/uae_vat_register/uae_vat_register.json b/erpnext/regional/report/uae_vat_register/uae_vat_register.json deleted file mode 100644 index 95c38c145e3..00000000000 --- a/erpnext/regional/report/uae_vat_register/uae_vat_register.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "add_total_row": 1, - "add_translate_data": 0, - "columns": [], - "creation": "2025-12-11 00:29:21.891860", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "filters": [], - "idx": 0, - "is_standard": "Yes", - "letterhead": null, - "modified": "2025-12-11 00:29:21.891860", - "modified_by": "Administrator", - "module": "Regional", - "name": "UAE VAT Register", - "owner": "Administrator", - "prepared_report": 0, - "ref_doctype": "GL Entry", - "report_name": "UAE VAT Register", - "report_type": "Script Report", - "roles": [], - "timeout": 0 -} \ No newline at end of file diff --git a/erpnext/regional/report/uae_vat_register/uae_vat_register.py b/erpnext/regional/report/uae_vat_register/uae_vat_register.py deleted file mode 100644 index 534e914936c..00000000000 --- a/erpnext/regional/report/uae_vat_register/uae_vat_register.py +++ /dev/null @@ -1,197 +0,0 @@ -# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - - -import frappe -from frappe import _ -from frappe.query_builder.functions import Coalesce, Sum - - -def execute(filters=None): - if not filters: - filters = {} - columns = get_columns(filters) - data = get_data(filters) - return columns, data - - -def get_columns(filters): - doc_type = filters.get("doc_type") or "Sales Invoice" - is_sales = doc_type == "Sales Invoice" - item_wise = bool(filters.get("item_wise")) - - columns = [ - { - "label": _("Invoice"), - "fieldname": "name", - "fieldtype": "Link", - "options": doc_type, - "width": 180, - }, - {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, - { - "label": _("Customer") if is_sales else _("Supplier"), - "fieldname": "party", - "fieldtype": "Link", - "options": "Customer" if is_sales else "Supplier", - "width": 150, - }, - { - "label": _("Cost Center"), - "fieldname": "cost_center", - "fieldtype": "Link", - "options": "Cost Center", - "width": 130, - }, - ] - if is_sales: - columns.append({"label": _("Emirate"), "fieldname": "emirate", "fieldtype": "Data", "width": 110}) - else: - columns.append( - { - "label": _("Reverse Charge"), - "fieldname": "reverse_charge", - "fieldtype": "Data", - "width": 110, - } - ) - if item_wise: - columns.extend( - [ - { - "label": _("Item Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 150, - }, - {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 80}, - {"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 100}, - ] - ) - else: - columns.append({"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 80}) - columns.extend( - [ - {"label": _("Net Amount"), "fieldname": "net_amount", "fieldtype": "Currency", "width": 120}, - {"label": _("VAT Amount"), "fieldname": "vat_amount", "fieldtype": "Currency", "width": 120}, - { - "label": _("Total Amount"), - "fieldname": "total_amount", - "fieldtype": "Currency", - "width": 120, - }, - ] - ) - return columns - - -def get_data(filters): - doc_type = filters.get("doc_type") or "Sales Invoice" - if doc_type == "Sales Invoice": - return _fetch_rows(filters, is_sales=True) - if doc_type == "Purchase Invoice": - return _fetch_rows(filters, is_sales=False) - return [] - - -def _fetch_rows(filters, is_sales): - """Build the VAT register query for either Sales or Purchase Invoices. - - Item-wise mode returns one row per Sales/Purchase Invoice Item; the - default mode aggregates back to one row per invoice with summed qty, - net, VAT, and total. ``COALESCE(i.tax_amount, 0)`` is used everywhere - so a missing VAT amount surfaces as 0 instead of NULL — matching the - currency display and avoiding NULLs in client-side totals. - """ - parent_doctype = "Sales Invoice" if is_sales else "Purchase Invoice" - child_doctype = "Sales Invoice Item" if is_sales else "Purchase Invoice Item" - parent = frappe.qb.DocType(parent_doctype) - child = frappe.qb.DocType(child_doctype) - item_wise = bool(filters.get("item_wise")) - - party_field = parent.customer if is_sales else parent.supplier - party_extra = parent.vat_emirate.as_("emirate") if is_sales else parent.reverse_charge - - tax_amount = Coalesce(child.tax_amount, 0) - gross = child.base_net_amount + tax_amount - - if item_wise: - query = ( - frappe.qb.from_(parent) - .inner_join(child) - .on(child.parent == parent.name) - .where(parent.docstatus == 1) - .select( - parent.name, - parent.posting_date, - party_field.as_("party"), - Coalesce(child.cost_center, parent.cost_center).as_("cost_center"), - party_extra, - child.item_code, - child.qty, - child.rate, - child.base_net_amount.as_("net_amount"), - tax_amount.as_("vat_amount"), - gross.as_("total_amount"), - ) - .orderby(parent.posting_date) - .orderby(parent.name) - .orderby(child.idx) - ) - else: - cost_center = parent.cost_center - query = ( - frappe.qb.from_(parent) - .inner_join(child) - .on(child.parent == parent.name) - .where(parent.docstatus == 1) - .select( - parent.name, - parent.posting_date, - party_field.as_("party"), - cost_center, - party_extra, - Sum(child.qty).as_("qty"), - Coalesce(Sum(child.base_net_amount), 0).as_("net_amount"), - Coalesce(Sum(tax_amount), 0).as_("vat_amount"), - Coalesce(Sum(gross), 0).as_("total_amount"), - ) - .groupby(parent.name, parent.posting_date, party_field, cost_center, party_extra) - .orderby(parent.posting_date) - .orderby(parent.name) - ) - - query = _apply_period_filters(query, parent, filters) - - if is_sales and filters.get("vat"): - query = query.where(parent.vat_emirate == filters["vat"]) - if not is_sales and filters.get("reverse_charge") in ("Y", "N"): - query = query.where(parent.reverse_charge == filters["reverse_charge"]) - if is_sales: - category_criterion = _sales_category_criterion(child, filters.get("category")) - if category_criterion is not None: - query = query.where(category_criterion) - - return query.run(as_dict=True) - - -def _apply_period_filters(query, parent, filters): - if filters.get("company"): - query = query.where(parent.company == filters["company"]) - if filters.get("from_date"): - query = query.where(parent.posting_date >= filters["from_date"]) - if filters.get("to_date"): - query = query.where(parent.posting_date <= filters["to_date"]) - return query - - -def _sales_category_criterion(child, category): - """Translate the ``category`` filter into a Sales Invoice Item criterion.""" - if category == "Standard": - return (child.is_zero_rated != 1) & (child.is_exempt != 1) - if category == "Zero Rated": - return child.is_zero_rated == 1 - if category == "Exempt Rated": - return child.is_exempt == 1 - return None diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 69a442619ce..2a26af226fa 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -200,14 +200,6 @@ def make_custom_fields(): print_hide=1, ), ], - "Company": [ - dict( - fieldname="company_name_in_arabic", - label="Company Name in Arabic", - fieldtype="Data", - insert_after="company_name", - ), - ], "Customer": [ dict( fieldname="customer_name_in_arabic",