diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 5db549806a3..19f2451576e 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -489,3 +489,4 @@ 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
new file mode 100644
index 00000000000..57b8db59f97
--- /dev/null
+++ b/erpnext/patches/v16_0/add_arabic_company_name_field.py
@@ -0,0 +1,10 @@
+import frappe
+
+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_custom_fields()
diff --git a/erpnext/regional/doctype/fta_audit_file/__init__.py b/erpnext/regional/doctype/fta_audit_file/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/doctype/fta_audit_file/fta_audit_file.js b/erpnext/regional/doctype/fta_audit_file/fta_audit_file.js
new file mode 100644
index 00000000000..5240da2e314
--- /dev/null
+++ b/erpnext/regional/doctype/fta_audit_file/fta_audit_file.js
@@ -0,0 +1,123 @@
+// 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) {
+ // Add Generate FAF button for Draft status
+ if (frm.doc.status === "Draft" && !frm.is_new()) {
+ frm.add_custom_button(
+ __("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
new file mode 100644
index 00000000000..db64126ae59
--- /dev/null
+++ b/erpnext/regional/doctype/fta_audit_file/fta_audit_file.json
@@ -0,0 +1,221 @@
+{
+ "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\nExcise",
+ "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
new file mode 100644
index 00000000000..fd8d937735e
--- /dev/null
+++ b/erpnext/regional/doctype/fta_audit_file/fta_audit_file.py
@@ -0,0 +1,659 @@
+# 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. All amounts are in AED (foreign-currency mirrors are
+emitted alongside when the source invoice is non-AED).
+"""
+
+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"
+
+
+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
+ )
+ )
+
+ if self.status != "Error":
+ self.error_message = None
+
+ @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.
+ """
+ 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)."""
+ 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:
+ pass
+ 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}")
+
+ if self.file_type != "VAT":
+ frappe.throw(_("FAF generation for {0} is not yet implemented").format(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_map = {
+ t: _resolve_tax_code(t)
+ for t in {item.item_tax_template for items in items_by_invoice.values() for item in items}
+ if t
+ }
+
+ company_currency = _company_currency(self.company)
+
+ total_purchase_aed = 0.0
+ total_vat_aed = 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, []):
+ net_aed = flt(item.base_net_amount, 2)
+ vat_aed = flt(item.tax_amount or 0, 2)
+ net_fcy = flt((item.net_amount or 0) if fcy_code != "XXX" else 0, 2)
+ vat_fcy = flt(vat_aed / fcy_factor if fcy_factor else 0, 2) 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_aed),
+ _money(vat_aed),
+ tax_code_map.get(item.item_tax_template, "SR"),
+ fcy_code,
+ _money(net_fcy),
+ _money(vat_fcy),
+ ]
+ )
+ total_purchase_aed += net_aed
+ total_vat_aed += vat_aed
+ line_count += 1
+
+ writer.writerow(
+ [
+ "PurcDataEnd",
+ _money(total_purchase_aed),
+ _money(total_vat_aed),
+ 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_map = {
+ t: _resolve_tax_code(t)
+ for t in {item.item_tax_template for items in items_by_invoice.values() for item in items}
+ if t
+ }
+
+ company_currency = _company_currency(self.company)
+
+ total_supply_aed = 0.0
+ total_vat_aed = 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, []):
+ net_aed = flt(item.base_net_amount, 2)
+ vat_aed = flt(item.tax_amount or 0, 2)
+ net_fcy = flt((item.net_amount or 0) if fcy_code != "XXX" else 0, 2)
+ vat_fcy = flt(vat_aed / fcy_factor if fcy_factor else 0, 2) 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 = tax_code_map.get(item.item_tax_template, "SR")
+
+ 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_aed),
+ _money(vat_aed),
+ tax_code,
+ _clean(customer_country),
+ fcy_code,
+ _money(net_fcy),
+ _money(vat_fcy),
+ ]
+ )
+ total_supply_aed += net_aed
+ total_vat_aed += vat_aed
+ line_count += 1
+
+ writer.writerow(
+ [
+ "SuppDataEnd",
+ _money(total_supply_aed),
+ _money(total_vat_aed),
+ line_count,
+ ]
+ )
+ return line_count
+
+ def _write_gl_listing(self, writer):
+ """Emit General Ledger per Appendix 5 with end-of-table totals row."""
+ writer.writerow(["GLDataStart"])
+
+ entries = frappe.get_all(
+ "GL Entry",
+ filters={
+ "company": self.company,
+ "posting_date": ["between", [self.from_date, self.to_date]],
+ "is_cancelled": 0,
+ },
+ fields=[
+ "name",
+ "posting_date",
+ "account",
+ "remarks",
+ "against",
+ "voucher_no",
+ "voucher_type",
+ "debit",
+ "credit",
+ ],
+ order_by="posting_date asc, creation asc",
+ )
+ if not entries:
+ writer.writerow(["GLDataEnd", _money(0), _money(0), 0, "AED"])
+ return 0
+
+ account_names = list({e.account for e in entries if e.account})
+ account_name_map = _bulk_party_field("Account", account_names, "account_name")
+
+ if self.include_opening_balance:
+ running_balance = _opening_balances_by_account(self.company, self.from_date, account_names)
+ else:
+ running_balance = {}
+
+ 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
+
+ for entry in entries:
+ 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
+
+ writer.writerow(
+ [
+ "GLDataEnd",
+ _money(total_debit),
+ _money(total_credit),
+ count,
+ "AED",
+ ]
+ )
+ 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}`` taken from each party's first address with a country."""
+ 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)
+ .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 _resolve_tax_code(item_tax_template):
+ """Derive FTA tax code (SR/ZR/EX/RC/IG/OA/IA) from one Item Tax Template.
+
+ Uses the Item Tax row's ``tax_category`` if it matches an FTA code;
+ otherwise defaults to ``SR`` (Standard Rated). Setting ``tax_category``
+ on each Item Tax is the supported way to control this — there is no
+ heuristic fallback on the template name.
+ """
+ if not item_tax_template:
+ return "SR"
+
+ tax_category = frappe.db.get_value(
+ "Item Tax",
+ {"item_tax_template": item_tax_template},
+ "tax_category",
+ )
+ if tax_category and tax_category in _FTA_TAX_CODES:
+ return tax_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
new file mode 100644
index 00000000000..b5f76558d24
--- /dev/null
+++ b/erpnext/regional/doctype/fta_audit_file/test_fta_audit_file.py
@@ -0,0 +1,258 @@
+# 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_generate_faf_excise_not_yet_implemented(self):
+ """Excise FAF (Appendix 6) should error cleanly until implemented."""
+ doc = frappe.get_doc(
+ {
+ "doctype": "FTA Audit File",
+ "company": self.company,
+ "from_date": "2099-03-01",
+ "to_date": "2099-03-31",
+ "file_type": "Excise",
+ }
+ )
+ doc.insert()
+
+ self.assertRaises(frappe.ValidationError, doc.generate_faf)
+ doc.reload()
+ self.assertEqual(doc.status, "Error")
+
+ 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")
+
+ def tearDown(self):
+ frappe.db.rollback()
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 7328f3f218e..b458e3d09ba 100644
--- a/erpnext/regional/report/uae_vat_201/uae_vat_201.html
+++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.html
@@ -1,77 +1,104 @@
{%
- 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") %}
-
- | {%= report_columns[0].label %} |
- {%= report_columns[1].label %} |
-
- {% for (let i=2; i{%= report_columns[i].label %}
- {% } %}
-
-
-
- {% 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 %} |
+
+
+ {% 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) %}
+ {% } %}
+
+ {% } %}
+
+ {% } %}
{% } %}
-{%= __("VAT on Expenses and All Other Inputs") %}
+{%= __("VAT on Expenses and All Other Inputs") %}
-
+
- | {%= report_columns[0].label %} |
- {%= report_columns[1].label %} |
-
- {% for (let i=2; i{%= report_columns[i].label %}
- {% } %}
-
-
-
- {% 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 %} |
+
+
+ {% 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) %}
+ {% } %}
+
+ {% } %}
+
+ {% } %}
+ {% } %}
+
+
+
+{%= __("Net VAT Due") %}
+
+
+
+
+ | {%= report_columns[0].label %} |
+ {%= report_columns[1].label %} |
+ {%= report_columns[3].label %} |
+
+
+
+ {% for (let j=20; j<23; j++) { %}
+ {% var row = data[j]; %}
+ {% if (row) { %}
+
+ | {%= 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 e62d3395f20..21c8cba72ac 100644
--- a/erpnext/regional/report/uae_vat_201/uae_vat_201.js
+++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.js
@@ -33,11 +33,14 @@ 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 == "VAT on Expenses and All Other Inputs" ||
+ data.legend == "Net VAT Due" ||
+ data.legend == "Total") &&
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 aaca8f01654..6dc18501af7 100644
--- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py
+++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py
@@ -8,11 +8,27 @@ from frappe.query_builder.functions import Sum
from erpnext import get_region
+# Per-execution memoization cache for the helper functions below.
+# Cleared at the start of every execute() call so each report run gets
+# fresh data; within a single run, repeated calls reuse the result.
+_cache = {}
+
+
+def _cached(fn):
+ def wrapper(filters, *args, **kwargs):
+ 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):
validate_company_region(filters)
+ _cache.clear()
columns = get_columns()
- data, emirates, amounts_by_emirate = get_data(filters)
+ data = get_data(filters)
return columns, data
@@ -48,23 +64,198 @@ 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 = []
- emirates, amounts_by_emirate = append_vat_on_sales(data, filters)
+ amounts_by_emirate = append_vat_on_sales(data, filters)
append_vat_on_expenses(data, filters)
- return data, emirates, amounts_by_emirate
+ net_vat_due(data, filters, amounts_by_emirate)
+
+ final_data = []
+ for i in range(0, len(data)):
+ if data[i].get("legend") == "Standard rated supplies in Abu Dhabi":
+ legend_link = f"""
+ {data[i].get("legend")}
+ """
+ final_data.append(
+ {
+ "no": data[i].get("no"),
+ "legend": legend_link,
+ "amount": data[i].get("amount"),
+ "vat_amount": data[i].get("vat_amount"),
+ }
+ )
+ elif data[i].get("legend") == "Standard rated supplies in Dubai":
+ company = frappe.defaults.get_user_default("Company")
+ company_filters = [
+ ["Dynamic Link", "link_doctype", "=", "Company"],
+ ["Dynamic Link", "link_name", "=", company],
+ ["Address", "is_your_company_address", "=", 1],
+ ]
+ company_fields = [
+ "name",
+ "address_line1",
+ "address_line2",
+ "city",
+ "state",
+ "country",
+ "emirate",
+ ]
+ address = frappe.get_all("Address", filters=company_filters, fields=company_fields)
+
+ if address:
+ if address[0].get("emirate"):
+ name = "Standard rated supplies in" + " " + address[0].get("emirate")
+ else:
+ name = "Standard rated supplies in Dubai"
+ else:
+ name = "Standard rated supplies in Dubai"
+
+ legend_link = f"""
+ {name}
+ """
+ final_data.append(
+ {
+ "no": data[i].get("no"),
+ "legend": legend_link,
+ "amount": data[i].get("amount"),
+ "vat_amount": data[i].get("vat_amount"),
+ }
+ )
+
+ elif data[i].get("legend") == "Standard rated supplies in Sharjah":
+ legend_link = f"""
+ {data[i].get("legend")}
+ """
+ final_data.append(
+ {
+ "no": data[i].get("no"),
+ "legend": legend_link,
+ "amount": data[i].get("amount"),
+ "vat_amount": data[i].get("vat_amount"),
+ }
+ )
+ elif data[i].get("legend") == "Standard rated supplies in Ajman":
+ legend_link = f"""
+ {data[i].get("legend")}
+ """
+ final_data.append(
+ {
+ "no": data[i].get("no"),
+ "legend": legend_link,
+ "amount": data[i].get("amount"),
+ "vat_amount": data[i].get("vat_amount"),
+ }
+ )
+ elif data[i].get("legend") == "Standard rated supplies in Umm Al Quwain":
+ legend_link = f"""
+ {data[i].get("legend")}
+ """
+ final_data.append(
+ {
+ "no": data[i].get("no"),
+ "legend": legend_link,
+ "amount": data[i].get("amount"),
+ "vat_amount": data[i].get("vat_amount"),
+ }
+ )
+ elif data[i].get("legend") == "Standard rated supplies in Ras Al Khaimah":
+ legend_link = f"""
+ {data[i].get("legend")}
+ """
+ final_data.append(
+ {
+ "no": data[i].get("no"),
+ "legend": legend_link,
+ "amount": data[i].get("amount"),
+ "vat_amount": data[i].get("vat_amount"),
+ }
+ )
+ elif data[i].get("legend") == "Standard rated supplies in Fujairah":
+ legend_link = f"""
+ {data[i].get("legend")}
+ """
+ final_data.append(
+ {
+ "no": data[i].get("no"),
+ "legend": legend_link,
+ "amount": data[i].get("amount"),
+ "vat_amount": data[i].get("vat_amount"),
+ }
+ )
+ elif data[i].get("legend") == "Supplies subject to the reverse charge provision":
+ legend_link = f"""
+ {data[i].get("legend")}
+ """
+ final_data.append(
+ {
+ "no": data[i].get("no"),
+ "legend": legend_link,
+ "amount": data[i].get("amount"),
+ "vat_amount": data[i].get("vat_amount"),
+ }
+ )
+ elif data[i].get("legend") == "Zero Rated":
+ legend_link = f"""
+ {data[i].get("legend")}
+ """
+ final_data.append(
+ {
+ "no": data[i].get("no"),
+ "legend": legend_link,
+ "amount": data[i].get("amount"),
+ "vat_amount": data[i].get("vat_amount"),
+ }
+ )
+ elif data[i].get("legend") == "Exempt Supplies":
+ legend_link = f"""
+ {data[i].get("legend")}
+ """
+ final_data.append(
+ {
+ "no": data[i].get("no"),
+ "legend": legend_link,
+ "amount": data[i].get("amount"),
+ "vat_amount": data[i].get("vat_amount"),
+ }
+ )
+ elif data[i].get("legend") == "Standard Rated Expenses":
+ legend_link = f"""
+ {data[i].get("legend")}
+ """
+ final_data.append(
+ {
+ "no": data[i].get("no"),
+ "legend": legend_link,
+ "amount": data[i].get("amount"),
+ "vat_amount": data[i].get("vat_amount"),
+ }
+ )
+ else:
+ final_data.append(
+ {
+ "no": data[i].get("no"),
+ "legend": data[i].get("legend"),
+ "amount": data[i].get("amount"),
+ "vat_amount": data[i].get("vat_amount"),
+ }
+ )
+
+ return final_data
def append_vat_on_sales(data, filters):
"""Appends Sales and All Other Outputs."""
append_data(data, "", _("VAT on Sales and All Other Outputs"), "", "")
- emirates, amounts_by_emirate = standard_rated_expenses_emiratewise(data, filters)
+ amounts_by_emirate = standard_rated_expenses_emiratewise(data, filters)
+
+ si_amount = amounts_by_emirate[1]
+ si_vat = amounts_by_emirate[2]
append_data(
data,
"2",
_("Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme"),
- frappe.format((-1) * get_tourist_tax_return_total(filters), "Currency"),
- frappe.format((-1) * get_tourist_tax_return_tax(filters), "Currency"),
+ format_currency_signed((-1) * get_tourist_tax_return_total(filters)),
+ format_currency_signed((-1) * get_tourist_tax_return_tax(filters)),
)
append_data(
@@ -79,9 +270,27 @@ def append_vat_on_sales(data, filters):
append_data(data, "5", _("Exempt Supplies"), frappe.format(get_exempt_total(filters), "Currency"), "-")
+ 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, "", "", "", "")
- return emirates, amounts_by_emirate
+ return amounts_by_emirate
def standard_rated_expenses_emiratewise(data, filters):
@@ -98,16 +307,21 @@ 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 emirates, amounts_by_emirate
+ return 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)
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,
@@ -116,7 +330,7 @@ def append_emiratewise_expenses(data, emirates, amounts_by_emirate):
frappe.format(0, "Currency"),
frappe.format(0, "Currency"),
)
- return amounts_by_emirate
+ return amounts_by_emirate, s_amount, v_amount
def append_vat_on_expenses(data, filters):
@@ -137,12 +351,77 @@ 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 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):
"""Returns data with appended value."""
data.append({"no": no, "legend": legend, "amount": amount, "vat_amount": vat_amount})
+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."""
i = frappe.qb.DocType("Sales Invoice Item")
@@ -175,11 +454,12 @@ def get_filters(filters):
query_filters.append(["company", "=", filters["company"]])
if filters.get("from_date"):
query_filters.append(["posting_date", ">=", filters["from_date"]])
- if filters.get("from_date"):
+ if filters.get("to_date"):
query_filters.append(["posting_date", "<=", filters["to_date"]])
return query_filters
+@_cached
def get_reverse_charge_total(filters):
"""Returns the sum of the total of each Purchase invoice made."""
query_filters = get_filters(filters)
@@ -190,7 +470,7 @@ def get_reverse_charge_total(filters):
frappe.db.get_all(
"Purchase Invoice",
filters=query_filters,
- fields=[{"SUM": "base_total"}],
+ fields=["sum(base_net_total)"],
as_list=True,
limit=1,
)[0][0]
@@ -200,6 +480,7 @@ def get_reverse_charge_total(filters):
return 0
+@_cached
def get_reverse_charge_tax(filters):
"""Returns the sum of the tax of each Purchase invoice made."""
p = frappe.qb.DocType("Purchase Invoice")
@@ -226,6 +507,7 @@ def get_reverse_charge_tax(filters):
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."""
query_filters = get_filters(filters)
@@ -237,7 +519,7 @@ def get_reverse_charge_recoverable_total(filters):
frappe.db.get_all(
"Purchase Invoice",
filters=query_filters,
- fields=[{"SUM": "base_total"}],
+ fields=["sum(base_net_total)"],
as_list=True,
limit=1,
)[0][0]
@@ -247,6 +529,7 @@ def get_reverse_charge_recoverable_total(filters):
return 0
+@_cached
def get_reverse_charge_recoverable_tax(filters):
"""Returns the sum of the tax of each Purchase invoice made."""
p = frappe.qb.DocType("Purchase Invoice")
@@ -286,6 +569,7 @@ def get_conditions_join(filters, p):
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."""
query_filters = get_filters(filters)
@@ -296,7 +580,7 @@ def get_standard_rated_expenses_total(filters):
frappe.db.get_all(
"Purchase Invoice",
filters=query_filters,
- fields=[{"SUM": "base_total"}],
+ fields=["sum(base_net_total)"],
as_list=True,
limit=1,
)[0][0]
@@ -306,6 +590,7 @@ def get_standard_rated_expenses_total(filters):
return 0
+@_cached
def get_standard_rated_expenses_tax(filters):
"""Returns the sum of the tax of each Purchase invoice made."""
query_filters = get_filters(filters)
@@ -316,7 +601,7 @@ def get_standard_rated_expenses_tax(filters):
frappe.db.get_all(
"Purchase Invoice",
filters=query_filters,
- fields=[{"SUM": "recoverable_standard_rated_expenses"}],
+ fields=["sum(recoverable_standard_rated_expenses)"],
as_list=True,
limit=1,
)[0][0]
@@ -326,6 +611,7 @@ def get_standard_rated_expenses_tax(filters):
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."""
query_filters = get_filters(filters)
@@ -334,7 +620,7 @@ def get_tourist_tax_return_total(filters):
try:
return (
frappe.db.get_all(
- "Sales Invoice", filters=query_filters, fields=[{"SUM": "base_total"}], as_list=True, limit=1
+ "Sales Invoice", filters=query_filters, fields=["sum(base_net_total)"], as_list=True, limit=1
)[0][0]
or 0
)
@@ -342,6 +628,7 @@ def get_tourist_tax_return_total(filters):
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."""
query_filters = get_filters(filters)
@@ -352,7 +639,7 @@ def get_tourist_tax_return_tax(filters):
frappe.db.get_all(
"Sales Invoice",
filters=query_filters,
- fields=[{"SUM": "tourist_tax_return"}],
+ fields=["sum(tourist_tax_return)"],
as_list=True,
limit=1,
)[0][0]
@@ -362,6 +649,7 @@ def get_tourist_tax_return_tax(filters):
return 0
+@_cached
def get_zero_rated_total(filters):
"""Returns the sum of each Sales Invoice Item Amount which is zero rated."""
i = frappe.qb.DocType("Sales Invoice Item")
@@ -381,6 +669,7 @@ def get_zero_rated_total(filters):
return 0
+@_cached
def get_exempt_total(filters):
"""Returns the sum of each Sales Invoice Item Amount which is Vat Exempt."""
i = frappe.qb.DocType("Sales Invoice Item")
diff --git a/erpnext/regional/report/uae_vat_register/__init__.py b/erpnext/regional/report/uae_vat_register/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/report/uae_vat_register/uae_vat_register.js b/erpnext/regional/report/uae_vat_register/uae_vat_register.js
new file mode 100644
index 00000000000..232aa7df691
--- /dev/null
+++ b/erpnext/regional/report/uae_vat_register/uae_vat_register.js
@@ -0,0 +1,73 @@
+// 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
new file mode 100644
index 00000000000..95c38c145e3
--- /dev/null
+++ b/erpnext/regional/report/uae_vat_register/uae_vat_register.json
@@ -0,0 +1,24 @@
+{
+ "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
new file mode 100644
index 00000000000..04427f7359a
--- /dev/null
+++ b/erpnext/regional/report/uae_vat_register/uae_vat_register.py
@@ -0,0 +1,216 @@
+# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+
+import frappe
+from frappe import _
+
+
+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_sales_rows(filters)
+ if doc_type == "Purchase Invoice":
+ return fetch_purchase_rows(filters)
+ return []
+
+
+def fetch_sales_rows(filters):
+ conditions, params = build_conditions(filters)
+ category_clause = sales_category_clause(filters.get("category"))
+
+ emirate_clause = ""
+ if filters.get("vat"):
+ emirate_clause = "AND s.vat_emirate = %(vat)s"
+ params["vat"] = filters["vat"]
+
+ if filters.get("item_wise"):
+ return frappe.db.sql(
+ f"""
+ SELECT
+ s.name, s.posting_date, s.customer AS party,
+ COALESCE(i.cost_center, s.cost_center) AS cost_center,
+ s.vat_emirate AS emirate,
+ i.item_code, i.qty, i.rate,
+ i.base_net_amount AS net_amount,
+ i.tax_amount AS vat_amount,
+ (i.base_net_amount + COALESCE(i.tax_amount, 0)) AS total_amount
+ FROM `tabSales Invoice` s
+ INNER JOIN `tabSales Invoice Item` i ON i.parent = s.name
+ WHERE s.docstatus = 1 {conditions} {category_clause} {emirate_clause}
+ ORDER BY s.posting_date, s.name, i.idx
+ """,
+ params,
+ as_dict=True,
+ )
+
+ return frappe.db.sql(
+ f"""
+ SELECT
+ s.name, s.posting_date, s.customer AS party, s.cost_center,
+ s.vat_emirate AS emirate,
+ SUM(i.qty) AS qty,
+ SUM(i.base_net_amount) AS net_amount,
+ SUM(i.tax_amount) AS vat_amount,
+ SUM(i.base_net_amount + COALESCE(i.tax_amount, 0)) AS total_amount
+ FROM `tabSales Invoice` s
+ INNER JOIN `tabSales Invoice Item` i ON i.parent = s.name
+ WHERE s.docstatus = 1 {conditions} {category_clause} {emirate_clause}
+ GROUP BY s.name, s.posting_date, s.customer, s.cost_center, s.vat_emirate
+ ORDER BY s.posting_date, s.name
+ """,
+ params,
+ as_dict=True,
+ )
+
+
+def fetch_purchase_rows(filters):
+ conditions, params = build_conditions(filters)
+
+ rc_clause = ""
+ if filters.get("reverse_charge") in ("Y", "N"):
+ rc_clause = "AND s.reverse_charge = %(reverse_charge)s"
+ params["reverse_charge"] = filters["reverse_charge"]
+
+ if filters.get("item_wise"):
+ return frappe.db.sql(
+ f"""
+ SELECT
+ s.name, s.posting_date, s.supplier AS party,
+ COALESCE(i.cost_center, s.cost_center) AS cost_center,
+ s.reverse_charge,
+ i.item_code, i.qty, i.rate,
+ i.base_net_amount AS net_amount,
+ i.tax_amount AS vat_amount,
+ (i.base_net_amount + COALESCE(i.tax_amount, 0)) AS total_amount
+ FROM `tabPurchase Invoice` s
+ INNER JOIN `tabPurchase Invoice Item` i ON i.parent = s.name
+ WHERE s.docstatus = 1 {conditions} {rc_clause}
+ ORDER BY s.posting_date, s.name, i.idx
+ """,
+ params,
+ as_dict=True,
+ )
+
+ return frappe.db.sql(
+ f"""
+ SELECT
+ s.name, s.posting_date, s.supplier AS party, s.cost_center,
+ s.reverse_charge,
+ SUM(i.qty) AS qty,
+ SUM(i.base_net_amount) AS net_amount,
+ SUM(i.tax_amount) AS vat_amount,
+ SUM(i.base_net_amount + COALESCE(i.tax_amount, 0)) AS total_amount
+ FROM `tabPurchase Invoice` s
+ INNER JOIN `tabPurchase Invoice Item` i ON i.parent = s.name
+ WHERE s.docstatus = 1 {conditions} {rc_clause}
+ GROUP BY s.name, s.posting_date, s.supplier, s.cost_center, s.reverse_charge
+ ORDER BY s.posting_date, s.name
+ """,
+ params,
+ as_dict=True,
+ )
+
+
+def build_conditions(filters):
+ conditions = ""
+ params = {}
+ if filters.get("company"):
+ conditions += " AND s.company = %(company)s"
+ params["company"] = filters["company"]
+ if filters.get("from_date"):
+ conditions += " AND s.posting_date >= %(from_date)s"
+ params["from_date"] = filters["from_date"]
+ if filters.get("to_date"):
+ conditions += " AND s.posting_date <= %(to_date)s"
+ params["to_date"] = filters["to_date"]
+ return conditions, params
+
+
+def sales_category_clause(category):
+ if category == "Standard":
+ return "AND i.is_zero_rated != 1 AND i.is_exempt != 1"
+ if category == "Zero Rated":
+ return "AND i.is_zero_rated = 1"
+ if category == "Exempt Rated":
+ return "AND i.is_exempt = 1"
+ return ""
diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py
index 2a26af226fa..69a442619ce 100644
--- a/erpnext/regional/united_arab_emirates/setup.py
+++ b/erpnext/regional/united_arab_emirates/setup.py
@@ -200,6 +200,14 @@ 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",