mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-29 14:48:26 +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", {
|
frappe.ui.form.on("FTA Audit File", {
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
// Add Generate FAF button for Draft status
|
// Generate FAF — available from Draft (first generation) and Error
|
||||||
if (frm.doc.status === "Draft" && !frm.is_new()) {
|
// (retry after a previous attempt failed). Queued/Generating are
|
||||||
|
// blocked by the server guard; Generated/Submitted are intentionally
|
||||||
|
// not re-generable.
|
||||||
|
if (!frm.is_new() && ["Draft", "Error"].includes(frm.doc.status)) {
|
||||||
frm.add_custom_button(
|
frm.add_custom_button(
|
||||||
__("Generate FAF"),
|
__(frm.doc.status === "Error" ? "Retry FAF Generation" : "Generate FAF"),
|
||||||
function () {
|
function () {
|
||||||
frm.trigger("generate_faf");
|
frm.trigger("generate_faf");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ delimited by an explicit start/end marker row:
|
|||||||
4. General Ledger (GLDataStart .. GLDataEnd)
|
4. General Ledger (GLDataStart .. GLDataEnd)
|
||||||
|
|
||||||
The footer of each transactional table carries running totals plus a
|
The footer of each transactional table carries running totals plus a
|
||||||
transaction count. All amounts are in AED (foreign-currency mirrors are
|
transaction count. Primary amount columns are in the company's accounting
|
||||||
emitted alongside when the source invoice is non-AED).
|
currency (typically AED for a UAE-registered entity); foreign-currency
|
||||||
|
mirrors are emitted alongside when the source invoice is in a different
|
||||||
|
currency.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
@@ -169,7 +171,14 @@ class FTAAuditFile(Document):
|
|||||||
err_doc.generation_log = (err_doc.generation_log or "") + f"\n\nError: {e}"
|
err_doc.generation_log = (err_doc.generation_log or "") + f"\n\nError: {e}"
|
||||||
err_doc.save()
|
err_doc.save()
|
||||||
except Exception:
|
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(
|
frappe.log_error(
|
||||||
title=_("FAF Generation Error"),
|
title=_("FAF Generation Error"),
|
||||||
message=frappe.get_traceback(),
|
message=frappe.get_traceback(),
|
||||||
@@ -307,8 +316,8 @@ class FTAAuditFile(Document):
|
|||||||
|
|
||||||
company_currency = _company_currency(self.company)
|
company_currency = _company_currency(self.company)
|
||||||
|
|
||||||
total_purchase_aed = 0.0
|
total_purchase_company = 0.0
|
||||||
total_vat_aed = 0.0
|
total_vat_company = 0.0
|
||||||
line_count = 0
|
line_count = 0
|
||||||
|
|
||||||
for inv in invoices:
|
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)
|
fcy_code, fcy_factor = _fcy_for_invoice(inv.currency, inv.conversion_rate, company_currency)
|
||||||
|
|
||||||
for item in items_by_invoice.get(inv.name, []):
|
for item in items_by_invoice.get(inv.name, []):
|
||||||
net_aed = flt(item.base_net_amount, 2)
|
# base_net_amount is company-currency; tax_amount is a UAE
|
||||||
vat_aed = flt(item.tax_amount or 0, 2)
|
# custom field with options="currency" and therefore stored
|
||||||
net_fcy = flt((item.net_amount or 0) if fcy_code != "XXX" else 0, 2)
|
# in the document's invoice currency. Multiply by the
|
||||||
vat_fcy = flt(vat_aed / fcy_factor if fcy_factor else 0, 2) if fcy_code != "XXX" else 0.0
|
# 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(
|
writer.writerow(
|
||||||
[
|
[
|
||||||
@@ -330,23 +344,23 @@ class FTAAuditFile(Document):
|
|||||||
inv.permit_no or "",
|
inv.permit_no or "",
|
||||||
item.idx,
|
item.idx,
|
||||||
_clean(item.description or item.item_name or ""),
|
_clean(item.description or item.item_name or ""),
|
||||||
_money(net_aed),
|
_money(net_company),
|
||||||
_money(vat_aed),
|
_money(vat_company),
|
||||||
_resolve_tax_code(item.item_tax_template, inv.posting_date, tax_code_bands),
|
_resolve_tax_code(item.item_tax_template, inv.posting_date, tax_code_bands),
|
||||||
fcy_code,
|
fcy_code,
|
||||||
_money(net_fcy),
|
_money(net_fcy),
|
||||||
_money(vat_fcy),
|
_money(vat_fcy),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
total_purchase_aed += net_aed
|
total_purchase_company += net_company
|
||||||
total_vat_aed += vat_aed
|
total_vat_company += vat_company
|
||||||
line_count += 1
|
line_count += 1
|
||||||
|
|
||||||
writer.writerow(
|
writer.writerow(
|
||||||
[
|
[
|
||||||
"PurcDataEnd",
|
"PurcDataEnd",
|
||||||
_money(total_purchase_aed),
|
_money(total_purchase_company),
|
||||||
_money(total_vat_aed),
|
_money(total_vat_company),
|
||||||
line_count,
|
line_count,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -409,8 +423,8 @@ class FTAAuditFile(Document):
|
|||||||
|
|
||||||
company_currency = _company_currency(self.company)
|
company_currency = _company_currency(self.company)
|
||||||
|
|
||||||
total_supply_aed = 0.0
|
total_supply_company = 0.0
|
||||||
total_vat_aed = 0.0
|
total_vat_company = 0.0
|
||||||
line_count = 0
|
line_count = 0
|
||||||
|
|
||||||
for inv in invoices:
|
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)
|
fcy_code, fcy_factor = _fcy_for_invoice(inv.currency, inv.conversion_rate, company_currency)
|
||||||
|
|
||||||
for item in items_by_invoice.get(inv.name, []):
|
for item in items_by_invoice.get(inv.name, []):
|
||||||
net_aed = flt(item.base_net_amount, 2)
|
# See _write_purchase_listing for the currency convention:
|
||||||
vat_aed = flt(item.tax_amount or 0, 2)
|
# tax_amount is invoice-currency, base_net_amount is
|
||||||
net_fcy = flt((item.net_amount or 0) if fcy_code != "XXX" else 0, 2)
|
# company-currency, and fcy_factor converts invoice → company.
|
||||||
vat_fcy = flt(vat_aed / fcy_factor if fcy_factor else 0, 2) if fcy_code != "XXX" else 0.0
|
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:
|
if item.is_zero_rated:
|
||||||
tax_code = "ZR"
|
tax_code = "ZR"
|
||||||
@@ -439,8 +457,8 @@ class FTAAuditFile(Document):
|
|||||||
inv.name,
|
inv.name,
|
||||||
item.idx,
|
item.idx,
|
||||||
_clean(item.description or item.item_name or ""),
|
_clean(item.description or item.item_name or ""),
|
||||||
_money(net_aed),
|
_money(net_company),
|
||||||
_money(vat_aed),
|
_money(vat_company),
|
||||||
tax_code,
|
tax_code,
|
||||||
_clean(customer_country),
|
_clean(customer_country),
|
||||||
fcy_code,
|
fcy_code,
|
||||||
@@ -448,15 +466,15 @@ class FTAAuditFile(Document):
|
|||||||
_money(vat_fcy),
|
_money(vat_fcy),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
total_supply_aed += net_aed
|
total_supply_company += net_company
|
||||||
total_vat_aed += vat_aed
|
total_vat_company += vat_company
|
||||||
line_count += 1
|
line_count += 1
|
||||||
|
|
||||||
writer.writerow(
|
writer.writerow(
|
||||||
[
|
[
|
||||||
"SuppDataEnd",
|
"SuppDataEnd",
|
||||||
_money(total_supply_aed),
|
_money(total_supply_company),
|
||||||
_money(total_vat_aed),
|
_money(total_vat_company),
|
||||||
line_count,
|
line_count,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -466,6 +484,8 @@ class FTAAuditFile(Document):
|
|||||||
"""Emit General Ledger per Appendix 5 with end-of-table totals row."""
|
"""Emit General Ledger per Appendix 5 with end-of-table totals row."""
|
||||||
writer.writerow(["GLDataStart"])
|
writer.writerow(["GLDataStart"])
|
||||||
|
|
||||||
|
company_currency = _company_currency(self.company)
|
||||||
|
|
||||||
entries = frappe.get_all(
|
entries = frappe.get_all(
|
||||||
"GL Entry",
|
"GL Entry",
|
||||||
filters={
|
filters={
|
||||||
@@ -487,7 +507,7 @@ class FTAAuditFile(Document):
|
|||||||
order_by="posting_date asc, creation asc",
|
order_by="posting_date asc, creation asc",
|
||||||
)
|
)
|
||||||
if not entries:
|
if not entries:
|
||||||
writer.writerow(["GLDataEnd", _money(0), _money(0), 0, "AED"])
|
writer.writerow(["GLDataEnd", _money(0), _money(0), 0, company_currency])
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
account_names = list({e.account for e in entries if e.account})
|
account_names = list({e.account for e in entries if e.account})
|
||||||
@@ -546,7 +566,7 @@ class FTAAuditFile(Document):
|
|||||||
_money(total_debit),
|
_money(total_debit),
|
||||||
_money(total_credit),
|
_money(total_credit),
|
||||||
count,
|
count,
|
||||||
"AED",
|
company_currency,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return count
|
return count
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ def _cached(fn):
|
|||||||
|
|
||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
|
filters = filters or {}
|
||||||
validate_company_region(filters)
|
validate_company_region(filters)
|
||||||
_cache.clear()
|
_cache.clear()
|
||||||
columns = get_columns()
|
columns = get_columns()
|
||||||
@@ -91,28 +92,27 @@ def get_data(filters=None):
|
|||||||
append_vat_on_expenses(data, filters)
|
append_vat_on_expenses(data, filters)
|
||||||
net_vat_due(data, filters, amounts_by_emirate)
|
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)
|
dubai_label_override = _company_emirate_label(filters)
|
||||||
|
|
||||||
final_data = []
|
final_data = []
|
||||||
for row in data:
|
for row in data:
|
||||||
|
key = row.get("_key")
|
||||||
legend = row.get("legend")
|
legend = row.get("legend")
|
||||||
new_legend = legend
|
new_legend = legend
|
||||||
|
|
||||||
if legend in emirate_drill_downs:
|
if key and key.startswith("emirate:"):
|
||||||
emirate = emirate_drill_downs[legend]
|
emirate = key.split(":", 1)[1]
|
||||||
label = dubai_label_override if legend == dubai_legend and dubai_label_override else legend
|
label = dubai_label_override if emirate == "Dubai" and dubai_label_override else legend
|
||||||
new_legend = _drill_down_link(
|
new_legend = _drill_down_link(
|
||||||
label, filters, doc_type="Sales Invoice", vat=emirate, category="Standard"
|
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")
|
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")
|
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")
|
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")
|
new_legend = _drill_down_link(legend, filters, doc_type="Purchase Invoice")
|
||||||
|
|
||||||
final_data.append(
|
final_data.append(
|
||||||
@@ -176,11 +176,26 @@ def append_vat_on_sales(data, filters):
|
|||||||
_("Supplies subject to the reverse charge provision"),
|
_("Supplies subject to the reverse charge provision"),
|
||||||
frappe.format(get_reverse_charge_total(filters), "Currency"),
|
frappe.format(get_reverse_charge_total(filters), "Currency"),
|
||||||
frappe.format(get_reverse_charge_tax(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(
|
append_data(
|
||||||
data,
|
data,
|
||||||
@@ -230,6 +245,7 @@ def append_emiratewise_expenses(data, emirates, amounts_by_emirate):
|
|||||||
if emirate in amounts_by_emirate:
|
if emirate in amounts_by_emirate:
|
||||||
amounts_by_emirate[emirate]["no"] = _("1{0}").format(chr(no))
|
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]["legend"] = _("Standard rated supplies in {0}").format(emirate)
|
||||||
|
amounts_by_emirate[emirate]["_key"] = f"emirate:{emirate}"
|
||||||
data.append(amounts_by_emirate[emirate])
|
data.append(amounts_by_emirate[emirate])
|
||||||
|
|
||||||
s_amount.append(amounts_by_emirate[emirate].get("raw_amount") or 0)
|
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),
|
_("Standard rated supplies in {0}").format(emirate),
|
||||||
frappe.format(0, "Currency"),
|
frappe.format(0, "Currency"),
|
||||||
frappe.format(0, "Currency"),
|
frappe.format(0, "Currency"),
|
||||||
|
key=f"emirate:{emirate}",
|
||||||
)
|
)
|
||||||
return amounts_by_emirate, s_amount, v_amount
|
return amounts_by_emirate, s_amount, v_amount
|
||||||
|
|
||||||
@@ -254,6 +271,7 @@ def append_vat_on_expenses(data, filters):
|
|||||||
_("Standard Rated Expenses"),
|
_("Standard Rated Expenses"),
|
||||||
frappe.format(get_standard_rated_expenses_total(filters), "Currency"),
|
frappe.format(get_standard_rated_expenses_total(filters), "Currency"),
|
||||||
frappe.format(get_standard_rated_expenses_tax(filters), "Currency"),
|
frappe.format(get_standard_rated_expenses_tax(filters), "Currency"),
|
||||||
|
key="standard_rated_expenses",
|
||||||
)
|
)
|
||||||
append_data(
|
append_data(
|
||||||
data,
|
data,
|
||||||
@@ -318,9 +336,15 @@ def net_vat_due(data, filters, amounts_by_emirate):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def append_data(data, no, legend, amount, vat_amount):
|
def append_data(data, no, legend, amount, vat_amount, key=None):
|
||||||
"""Returns data with appended value."""
|
"""Append one row to ``data``.
|
||||||
data.append({"no": no, "legend": legend, "amount": amount, "vat_amount": vat_amount})
|
|
||||||
|
``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):
|
def format_currency_signed(value):
|
||||||
|
|||||||
Reference in New Issue
Block a user