feat(FTA Audit File): Enhance FAF generation logic and error handling; update currency handling in VAT reports

This commit is contained in:
Bibin
2026-06-14 11:37:35 +00:00
parent 806f30fa87
commit dffe4bd22d
3 changed files with 93 additions and 46 deletions

View File

@@ -3,10 +3,13 @@
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()) {
// 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(
__("Generate FAF"),
__(frm.doc.status === "Error" ? "Retry FAF Generation" : "Generate FAF"),
function () {
frm.trigger("generate_faf");
},

View File

@@ -17,8 +17,10 @@ delimited by an explicit start/end marker row:
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).
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
@@ -169,7 +171,14 @@ class FTAAuditFile(Document):
err_doc.generation_log = (err_doc.generation_log or "") + f"\n\nError: {e}"
err_doc.save()
except Exception:
pass
# 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(),
@@ -307,8 +316,8 @@ class FTAAuditFile(Document):
company_currency = _company_currency(self.company)
total_purchase_aed = 0.0
total_vat_aed = 0.0
total_purchase_company = 0.0
total_vat_company = 0.0
line_count = 0
for inv in invoices:
@@ -316,10 +325,15 @@ class FTAAuditFile(Document):
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
# 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(
[
@@ -330,23 +344,23 @@ class FTAAuditFile(Document):
inv.permit_no or "",
item.idx,
_clean(item.description or item.item_name or ""),
_money(net_aed),
_money(vat_aed),
_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_aed += net_aed
total_vat_aed += vat_aed
total_purchase_company += net_company
total_vat_company += vat_company
line_count += 1
writer.writerow(
[
"PurcDataEnd",
_money(total_purchase_aed),
_money(total_vat_aed),
_money(total_purchase_company),
_money(total_vat_company),
line_count,
]
)
@@ -409,8 +423,8 @@ class FTAAuditFile(Document):
company_currency = _company_currency(self.company)
total_supply_aed = 0.0
total_vat_aed = 0.0
total_supply_company = 0.0
total_vat_company = 0.0
line_count = 0
for inv in invoices:
@@ -419,10 +433,14 @@ class FTAAuditFile(Document):
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
# 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"
@@ -439,8 +457,8 @@ class FTAAuditFile(Document):
inv.name,
item.idx,
_clean(item.description or item.item_name or ""),
_money(net_aed),
_money(vat_aed),
_money(net_company),
_money(vat_company),
tax_code,
_clean(customer_country),
fcy_code,
@@ -448,15 +466,15 @@ class FTAAuditFile(Document):
_money(vat_fcy),
]
)
total_supply_aed += net_aed
total_vat_aed += vat_aed
total_supply_company += net_company
total_vat_company += vat_company
line_count += 1
writer.writerow(
[
"SuppDataEnd",
_money(total_supply_aed),
_money(total_vat_aed),
_money(total_supply_company),
_money(total_vat_company),
line_count,
]
)
@@ -466,6 +484,8 @@ class FTAAuditFile(Document):
"""Emit General Ledger per Appendix 5 with end-of-table totals row."""
writer.writerow(["GLDataStart"])
company_currency = _company_currency(self.company)
entries = frappe.get_all(
"GL Entry",
filters={
@@ -487,7 +507,7 @@ class FTAAuditFile(Document):
order_by="posting_date asc, creation asc",
)
if not entries:
writer.writerow(["GLDataEnd", _money(0), _money(0), 0, "AED"])
writer.writerow(["GLDataEnd", _money(0), _money(0), 0, company_currency])
return 0
account_names = list({e.account for e in entries if e.account})
@@ -546,7 +566,7 @@ class FTAAuditFile(Document):
_money(total_debit),
_money(total_credit),
count,
"AED",
company_currency,
]
)
return count

View File

@@ -48,6 +48,7 @@ def _cached(fn):
def execute(filters=None):
filters = filters or {}
validate_company_region(filters)
_cache.clear()
columns = get_columns()
@@ -91,28 +92,27 @@ def get_data(filters=None):
append_vat_on_expenses(data, filters)
net_vat_due(data, filters, amounts_by_emirate)
emirate_drill_downs = {f"Standard rated supplies in {emirate}": emirate for emirate in get_emirates()}
dubai_legend = "Standard rated supplies in Dubai"
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 legend in emirate_drill_downs:
emirate = emirate_drill_downs[legend]
label = dubai_label_override if legend == dubai_legend and dubai_label_override else 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 legend == "Supplies subject to the reverse charge provision":
elif key == "reverse_charge_supplies":
new_legend = _drill_down_link(legend, filters, doc_type="Purchase Invoice", reverse_charge="Y")
elif legend == "Zero Rated":
elif key == "zero_rated":
new_legend = _drill_down_link(legend, filters, doc_type="Sales Invoice", category="Zero Rated")
elif legend == "Exempt Supplies":
elif key == "exempt_supplies":
new_legend = _drill_down_link(legend, filters, doc_type="Sales Invoice", category="Exempt Rated")
elif legend == "Standard Rated Expenses":
elif key == "standard_rated_expenses":
new_legend = _drill_down_link(legend, filters, doc_type="Purchase Invoice")
final_data.append(
@@ -176,11 +176,26 @@ 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"), "-")
append_data(
data,
"4",
_("Zero Rated"),
frappe.format(get_zero_rated_total(filters), "Currency"),
"-",
key="zero_rated",
)
append_data(data, "5", _("Exempt Supplies"), frappe.format(get_exempt_total(filters), "Currency"), "-")
append_data(
data,
"5",
_("Exempt Supplies"),
frappe.format(get_exempt_total(filters), "Currency"),
"-",
key="exempt_supplies",
)
append_data(
data,
@@ -230,6 +245,7 @@ def append_emiratewise_expenses(data, emirates, amounts_by_emirate):
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)
@@ -241,6 +257,7 @@ 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
@@ -254,6 +271,7 @@ 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,
@@ -318,9 +336,15 @@ def net_vat_due(data, filters, amounts_by_emirate):
)
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 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):