diff --git a/erpnext/regional/doctype/fta_audit_file/fta_audit_file.js b/erpnext/regional/doctype/fta_audit_file/fta_audit_file.js index 5240da2e314..233819b190d 100644 --- a/erpnext/regional/doctype/fta_audit_file/fta_audit_file.js +++ b/erpnext/regional/doctype/fta_audit_file/fta_audit_file.js @@ -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"); }, diff --git a/erpnext/regional/doctype/fta_audit_file/fta_audit_file.py b/erpnext/regional/doctype/fta_audit_file/fta_audit_file.py index 1356c76a744..366705c3079 100644 --- a/erpnext/regional/doctype/fta_audit_file/fta_audit_file.py +++ b/erpnext/regional/doctype/fta_audit_file/fta_audit_file.py @@ -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 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 ae50b88187c..e795e7709e9 100644 --- a/erpnext/regional/report/uae_vat_201/uae_vat_201.py +++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py @@ -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):