mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-11 05:31:48 +00:00
feat(FTA Audit File): Enhance FAF generation logic and error handling; update currency handling in VAT reports
This commit is contained in:
@@ -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");
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user