add cash method

This commit is contained in:
Ceyhun 2026-04-02 15:41:26 +00:00
parent e86e8c841b
commit 6afb8ca3d3
45 changed files with 11054 additions and 3 deletions

281
taxes_az/cash_method.py Normal file
View File

@ -0,0 +1,281 @@
"""
Cash Method (Kassa Metodu) Payment Entry Hook
When a company uses Kassa metodu, VAT obligations and revenue are recognized
only when payment is received/made, not at invoice time.
This hook runs on Payment Entry submission and creates Journal Entries
to transfer deferred amounts:
- 245.1 (ƏDV gözləmədə satış) 521.3 (ƏDV к уплате)
- 245.2 (ƏDV gözləmədə alış) 226 (ƏDV к зачёту)
- 542 (Отложенный доход) 601 (Доход)
"""
import frappe
from frappe import _
from frappe.utils import flt
def on_payment_entry_submit(doc, method):
"""Hook: called when Payment Entry is submitted."""
company = doc.company
accounting_method = frappe.db.get_value("Company", company, "accounting_method")
if accounting_method != "Kassa metodu":
return
settings = frappe.get_single("Tax Period Closing Settings")
deferred_vat_sales = settings.deferred_vat_sales_account
deferred_vat_purchase = settings.deferred_vat_purchase_account
deferred_revenue = settings.deferred_revenue_account
vat_output = settings.vat_output_account
vat_input = settings.vat_input_account
if not deferred_vat_sales or not vat_output:
return
for ref in doc.references or []:
if not ref.reference_doctype or not ref.reference_name:
continue
if ref.reference_doctype == "Sales Invoice":
_process_sales_payment(
doc, ref, deferred_vat_sales, vat_output,
deferred_revenue, settings
)
elif ref.reference_doctype == "Purchase Invoice":
_process_purchase_payment(
doc, ref, deferred_vat_purchase, vat_input, settings
)
def _process_sales_payment(payment, ref, deferred_vat_acc, vat_output_acc,
deferred_revenue_acc, settings):
"""Transfer deferred VAT and revenue for a sales payment."""
si = frappe.get_doc("Sales Invoice", ref.reference_name)
if not si or si.docstatus != 1:
return
# Calculate payment ratio
total_invoice = flt(si.grand_total) or 1
paid_amount = flt(ref.allocated_amount)
ratio = paid_amount / total_invoice
# VAT portion
total_vat = flt(si.total_taxes_and_charges)
vat_to_transfer = flt(total_vat * ratio, 2)
# Revenue portion (net total)
total_net = flt(si.net_total)
revenue_to_transfer = flt(total_net * ratio, 2)
entries = []
# Transfer deferred VAT → actual VAT obligation
if vat_to_transfer > 0 and deferred_vat_acc:
entries.append({
"debit_account": deferred_vat_acc,
"credit_account": vat_output_acc,
"amount": vat_to_transfer,
"remark": (f"Kassa metodu: ƏDV tanınması{ref.reference_name} "
f"({ratio*100:.0f}% ödənildi). Payment: {payment.name}"),
})
# Transfer deferred revenue → actual revenue
if revenue_to_transfer > 0 and deferred_revenue_acc:
# Find the income account from the invoice
income_account = None
for item in si.items:
if item.income_account:
income_account = item.income_account
break
if income_account:
entries.append({
"debit_account": deferred_revenue_acc,
"credit_account": income_account,
"amount": revenue_to_transfer,
"remark": (f"Kassa metodu: gəlir tanınması{ref.reference_name} "
f"({ratio*100:.0f}% ödənildi). Payment: {payment.name}"),
})
# Create Journal Entries
for entry in entries:
_create_transfer_je(payment, entry, settings)
def _process_purchase_payment(payment, ref, deferred_vat_acc, vat_input_acc, settings):
"""Transfer deferred input VAT for a purchase payment."""
pi = frappe.get_doc("Purchase Invoice", ref.reference_name)
if not pi or pi.docstatus != 1:
return
total_invoice = flt(pi.grand_total) or 1
paid_amount = flt(ref.allocated_amount)
ratio = paid_amount / total_invoice
total_vat = flt(pi.total_taxes_and_charges)
vat_to_transfer = flt(total_vat * ratio, 2)
if vat_to_transfer > 0 and deferred_vat_acc and vat_input_acc:
_create_transfer_je(payment, {
"debit_account": vat_input_acc,
"credit_account": deferred_vat_acc,
"amount": vat_to_transfer,
"remark": (f"Kassa metodu: ƏDV əvəzləşdirmə — {ref.reference_name} "
f"({ratio*100:.0f}% ödənildi). Payment: {payment.name}"),
}, settings)
def _create_transfer_je(payment, entry, settings):
"""Create a Journal Entry for cash method transfer."""
je = frappe.new_doc("Journal Entry")
je.company = payment.company
je.posting_date = payment.posting_date
je.voucher_type = "Journal Entry"
je.user_remark = entry["remark"]
cost_center = settings.default_cost_center or frappe.db.get_value(
"Company", payment.company, "cost_center")
je.append("accounts", {
"account": entry["debit_account"],
"debit_in_account_currency": entry["amount"],
"cost_center": cost_center,
})
je.append("accounts", {
"account": entry["credit_account"],
"credit_in_account_currency": entry["amount"],
"cost_center": cost_center,
})
je.insert(ignore_permissions=True)
je.submit()
def on_journal_entry_submit(doc, method):
"""Hook: when JE with Дт 226 / Кт 211 is submitted, auto-allocate via FIFO."""
company = doc.company
accounting_method = frappe.db.get_value("Company", company, "accounting_method")
if accounting_method != "Kassa metodu":
return
# Skip if this JE is itself a FIFO allocation
if "FIFO" in (doc.user_remark or ""):
return
# Check if this JE has Дт 226 / Кт 211 (НДС от клиента)
has_226_debit = False
vat_amount = 0
customer = None
for row in doc.accounts:
if "226" in (row.account or "") and flt(row.debit_in_account_currency) > 0:
has_226_debit = True
vat_amount += flt(row.debit_in_account_currency)
if "211" in (row.account or "") and flt(row.credit_in_account_currency) > 0:
customer = row.party
if not has_226_debit or vat_amount <= 0:
return
# Auto FIFO allocation for this payment
settings = frappe.get_single("Tax Period Closing Settings")
deferred_vat = settings.deferred_vat_sales_account
deferred_rev = settings.deferred_revenue_account
vat_output = settings.vat_output_account
if not deferred_vat or not deferred_rev or not vat_output:
return
# Find oldest unrecognized SI for this customer
sis = frappe.db.sql("""
SELECT si.name, si.grand_total, si.net_total, si.total_taxes_and_charges
FROM `tabSales Invoice` si
WHERE si.company=%s AND si.docstatus=1 AND si.customer=%s
ORDER BY si.posting_date
""", (company, customer), as_dict=True)
cost_center = settings.default_cost_center or frappe.db.get_value(
"Company", company, "cost_center")
income_account = frappe.db.get_value("Account", {
"company": company, "account_number": "601", "is_group": 0
}, "name")
remaining = vat_amount # НДС amount = proxy for payment amount
for si in sis:
if remaining <= 0.01:
break
# Check how much already recognized
recognized_vat = abs(flt(frappe.db.sql("""
SELECT SUM(debit) FROM `tabGL Entry`
WHERE account=%s AND is_cancelled=0
AND against_voucher_type='Sales Invoice' AND against_voucher=%s
""", (deferred_vat, si.name))[0][0] or 0))
si_vat = flt(si.total_taxes_and_charges)
unrecognized_vat = si_vat - recognized_vat
if unrecognized_vat <= 0.01:
continue
allocate_vat = min(remaining, unrecognized_vat)
ratio = allocate_vat / si_vat if si_vat else 0
allocate_rev = flt(si.net_total * ratio, 2)
# Create transfer JE
transfer_je = frappe.new_doc("Journal Entry")
transfer_je.company = company
transfer_je.posting_date = doc.posting_date
transfer_je.voucher_type = "Journal Entry"
transfer_je.user_remark = (
f"FIFO Kassa auto: {si.name} ({ratio*100:.0f}%). "
f"Source JE: {doc.name}"
)
if allocate_vat > 0:
transfer_je.append("accounts", {
"account": deferred_vat, "debit_in_account_currency": allocate_vat,
"cost_center": cost_center,
"against_voucher_type": "Sales Invoice", "against_voucher": si.name,
})
transfer_je.append("accounts", {
"account": vat_output, "credit_in_account_currency": allocate_vat,
"cost_center": cost_center,
})
if allocate_rev > 0 and income_account:
transfer_je.append("accounts", {
"account": deferred_rev, "debit_in_account_currency": allocate_rev,
"cost_center": cost_center,
"against_voucher_type": "Sales Invoice", "against_voucher": si.name,
})
transfer_je.append("accounts", {
"account": income_account, "credit_in_account_currency": allocate_rev,
"cost_center": cost_center,
})
transfer_je.insert(ignore_permissions=True)
transfer_je.submit()
remaining -= allocate_vat
def on_payment_entry_cancel(doc, method):
"""Hook: cancel related cash method JEs when Payment is cancelled."""
company = doc.company
accounting_method = frappe.db.get_value("Company", company, "accounting_method")
if accounting_method != "Kassa metodu":
return
# Find JEs created by this payment
jes = frappe.get_all("Journal Entry", filters={
"user_remark": ["like", f"%Payment: {doc.name}%"],
"docstatus": 1,
}, pluck="name")
for je_name in jes:
je = frappe.get_doc("Journal Entry", je_name)
je.cancel()

View File

@ -20,6 +20,11 @@ frappe.ui.form.on('Company', {
// Check gambling activity on refresh
ETaxesCompany.checkGamblingActivity(frm);
// Render tax wizard links
if (frm.fields_dict.tax_wizards_html && frm.doc.name) {
render_tax_wizards(frm);
}
},
main_activity: function(frm) {
@ -1745,3 +1750,108 @@ window.ETaxesCompany = {
dialog.set_primary_action(__('Close'), () => dialog.hide());
}
};
function render_tax_wizards(frm) {
const company = frm.doc.name;
// Get existing wizards
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "Tax Year Closing Wizard",
filters: { company: company },
fields: ["name", "fiscal_year", "year", "overall_status", "overall_progress"],
order_by: "year desc",
limit: 5,
},
callback(r) {
const year_wizards = r.message || [];
frappe.call({
method: "frappe.client.get_list",
args: {
doctype: "Tax Period Closing Wizard",
filters: { company: company },
fields: ["name", "fiscal_year", "wizard_status", "period_type"],
order_by: "creation desc",
limit: 5,
},
callback(r2) {
const period_wizards = r2.message || [];
build_wizard_html(frm, year_wizards, period_wizards);
}
});
}
});
}
function build_wizard_html(frm, year_wizards, period_wizards) {
const status_badge = (status) => {
const colors = {
"Not Started": "background:#6c757d;", "In Progress": "background:#ffc107;color:#333;",
"Completed": "background:#28a745;", "Draft": "background:#6c757d;",
"Calculated": "background:#17a2b8;", "Entries Created": "background:#28a745;",
};
return `<span style="padding:2px 8px;border-radius:10px;font-size:11px;color:white;${colors[status] || 'background:#999;'}">${status}</span>`;
};
let html = `<div style="display:flex;gap:16px;flex-wrap:wrap;">`;
// Year Wizard card
html += `<div style="flex:1;min-width:300px;border:1px solid #e1e4e8;border-radius:8px;padding:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<h5 style="margin:0;">📅 İllik bağlanış</h5>
<a href="/app/tax-year-closing-wizard/new?company=${encodeURIComponent(frm.doc.name)}" class="btn btn-xs btn-primary">+ Yeni</a>
</div>`;
if (year_wizards.length > 0) {
for (const w of year_wizards) {
const pct = Math.round(w.overall_progress || 0);
html += `<a href="/app/tax-year-closing-wizard/${w.name}" style="display:block;padding:8px;margin-bottom:4px;
background:#f8f9fa;border-radius:4px;text-decoration:none;color:#333;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span><strong>${w.name}</strong> ${w.fiscal_year}</span>
${status_badge(w.overall_status)}
</div>
<div style="background:#e9ecef;border-radius:4px;height:6px;margin-top:6px;">
<div style="width:${pct}%;height:100%;background:#28a745;border-radius:4px;"></div>
</div>
</a>`;
}
} else {
html += `<div style="color:#999;font-size:12px;">Hələ yaradılmayıb</div>`;
}
html += `</div>`;
// Period Wizard card
html += `<div style="flex:1;min-width:300px;border:1px solid #e1e4e8;border-radius:8px;padding:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<h5 style="margin:0;">📋 Dövr bağlanışı</h5>
<a href="/app/tax-period-closing-wizard/new?company=${encodeURIComponent(frm.doc.name)}" class="btn btn-xs btn-default">+ Yeni</a>
</div>`;
if (period_wizards.length > 0) {
for (const w of period_wizards) {
html += `<a href="/app/tax-period-closing-wizard/${w.name}" style="display:block;padding:8px;margin-bottom:4px;
background:#f8f9fa;border-radius:4px;text-decoration:none;color:#333;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span><strong>${w.name}</strong> ${w.fiscal_year} (${w.period_type})</span>
${status_badge(w.wizard_status)}
</div>
</a>`;
}
} else {
html += `<div style="color:#999;font-size:12px;">Hələ yaradılmayıb</div>`;
}
html += `</div>`;
// Settings link
html += `<div style="flex:0 0 auto;border:1px solid #e1e4e8;border-radius:8px;padding:16px;min-width:200px;">
<h5 style="margin:0 0 12px 0;"> Parametrlər</h5>
<a href="/app/tax-period-closing-settings" class="btn btn-xs btn-default" style="display:block;margin-bottom:6px;">Tax Closing Settings</a>
</div>`;
html += `</div>`;
frm.fields_dict.tax_wizards_html.$wrapper.html(html);
}

View File

@ -24,7 +24,8 @@ fixtures = [
after_migrate = [
"taxes_az.create_item_group.create_item_groups",
"taxes_az.normalize_tax_articles.normalize_on_migrate"
"taxes_az.normalize_tax_articles.normalize_on_migrate",
"taxes_az.setup_accounts.check_and_create_accounts"
]
doctype_js = {
@ -42,6 +43,16 @@ doctype_js = {
doc_events = {
"Item Group": {
"on_trash": "taxes_az.item_group.validate_item_group_deletion"
},
"Company": {
"on_update": "taxes_az.setup_accounts.on_company_update"
},
"Payment Entry": {
"on_submit": "taxes_az.cash_method.on_payment_entry_submit",
"on_cancel": "taxes_az.cash_method.on_payment_entry_cancel"
},
"Journal Entry": {
"on_submit": "taxes_az.cash_method.on_journal_entry_submit"
}
}

235
taxes_az/setup_accounts.py Normal file
View File

@ -0,0 +1,235 @@
"""
Setup required accounts for Tax Year Closing Wizard.
Runs after migrate checks if accounts exist and creates them if missing.
"""
import frappe
from frappe import _
REQUIRED_ACCOUNTS = [
{
"account_number": "245.1",
"account_name": "ƏDV gözləmədə (satış)",
"parent_number": "245",
"parent_name": "Digər qısamüddətli aktivlər",
"root_type": "Asset",
"account_type": "Tax",
"description": "Kassa metodu: Satış fakturasında ƏDV burada saxlanılır, ödəniş alındıqda 521.3-ə köçürülür",
"needed_for": "Kassa metodu",
},
{
"account_number": "245.2",
"account_name": "ƏDV gözləmədə (alış)",
"parent_number": "245",
"parent_name": "Digər qısamüddətli aktivlər",
"root_type": "Asset",
"account_type": "Tax",
"description": "Kassa metodu: Alış fakturasında ƏDV burada saxlanılır, ödəniş edildikdə 226-ya köçürülür",
"needed_for": "Kassa metodu",
},
]
def check_and_create_accounts():
"""Check required accounts for all companies and create if missing."""
companies = frappe.get_all("Company", filters={"is_group": 0}, fields=["name", "abbr"])
for company in companies:
missing = []
for acc_def in REQUIRED_ACCOUNTS:
exists = frappe.db.exists("Account", {
"company": company.name,
"account_number": acc_def["account_number"],
})
if not exists:
missing.append(acc_def)
if not missing:
continue
# Check accounting method — only create if Kassa
method = frappe.db.get_value("Company", company.name, "accounting_method") or ""
if "Kassa" not in method:
continue
for acc_def in missing:
_create_account(company.name, company.abbr, acc_def)
def _create_account(company, abbr, acc_def):
"""Create a single account if parent exists."""
# Ensure parent is group
parent_acc = frappe.db.get_value("Account", {
"company": company,
"account_number": acc_def["parent_number"],
}, ["name", "is_group"], as_dict=True)
if not parent_acc:
frappe.log_error(
f"taxes_az: Cannot create account {acc_def['account_number']}"
f"parent {acc_def['parent_number']} not found for {company}",
"Account Setup"
)
return
# Convert parent to group if needed
if not parent_acc.is_group:
gl_count = frappe.db.count("GL Entry", {
"account": parent_acc.name, "is_cancelled": 0
})
if gl_count == 0:
frappe.db.set_value("Account", parent_acc.name, "is_group", 1)
else:
frappe.log_error(
f"taxes_az: Cannot convert {parent_acc.name} to group — has GL entries",
"Account Setup"
)
return
try:
acc = frappe.get_doc({
"doctype": "Account",
"account_name": acc_def["account_name"],
"account_number": acc_def["account_number"],
"parent_account": parent_acc.name,
"company": company,
"account_type": acc_def.get("account_type", ""),
"is_group": 0,
})
acc.flags.ignore_permissions = True
acc.flags.ignore_mandatory = True
acc.insert(ignore_permissions=True)
frappe.db.commit()
print(f" ✓ Created {acc_def['account_number']}{acc_def['account_name']} for {company}")
except Exception as e:
frappe.log_error(
f"taxes_az: Failed to create {acc_def['account_number']} for {company}: {e}",
"Account Setup"
)
def update_tax_templates_for_method(company):
"""Switch Sales/Purchase Tax Templates based on accounting method."""
method = frappe.db.get_value("Company", company, "accounting_method") or ""
abbr = frappe.db.get_value("Company", company, "abbr")
if "Kassa" in method:
# Kassa: 521.3 → 245.1 (sales), 226 → 245.2 (purchase)
sales_from = f"521.3 - ƏDV - {abbr}"
sales_to = frappe.db.get_value("Account", {"company": company, "account_number": "245.1"}, "name")
purchase_from = f"226 - ƏDV sub-uçot hesabı - {abbr}"
purchase_to = frappe.db.get_value("Account", {"company": company, "account_number": "245.2"}, "name")
else:
# Hesablama: 245.1 → 521.3 (sales), 245.2 → 226 (purchase)
sales_from = frappe.db.get_value("Account", {"company": company, "account_number": "245.1"}, "name")
sales_to = f"521.3 - ƏDV - {abbr}"
purchase_from = frappe.db.get_value("Account", {"company": company, "account_number": "245.2"}, "name")
purchase_to = f"226 - ƏDV sub-uçot hesabı - {abbr}"
if not sales_to or not sales_from:
return
# Update Sales Templates
updated = 0
for t_name in frappe.get_all("Sales Taxes and Charges Template",
filters={"company": company}, pluck="name"):
doc = frappe.get_doc("Sales Taxes and Charges Template", t_name)
changed = False
for tax in doc.taxes:
if tax.account_head == sales_from:
tax.account_head = sales_to
changed = True
if changed:
doc.save(ignore_permissions=True)
updated += 1
# Update Purchase Templates
if purchase_from and purchase_to:
for t_name in frappe.get_all("Purchase Taxes and Charges Template",
filters={"company": company}, pluck="name"):
doc = frappe.get_doc("Purchase Taxes and Charges Template", t_name)
changed = False
for tax in doc.taxes:
if tax.account_head == purchase_from:
tax.account_head = purchase_to
changed = True
if changed:
doc.save(ignore_permissions=True)
updated += 1
if updated:
frappe.db.commit()
def on_company_update(doc, method):
"""Hook: when Company is saved, update templates if accounting method changed."""
old_method = doc.get_db_value("accounting_method") if not doc.is_new() else ""
new_method = doc.accounting_method or ""
if old_method != new_method and new_method:
# Create accounts if switching to Kassa
if "Kassa" in new_method:
check_and_create_accounts()
# Update templates
update_tax_templates_for_method(doc.name)
@frappe.whitelist()
def check_required_accounts(company):
"""API: Check which accounts are missing for a company."""
if not company:
return {"accounts": [], "missing": []}
abbr = frappe.db.get_value("Company", company, "abbr")
method = frappe.db.get_value("Company", company, "accounting_method") or ""
accounts = []
missing = []
for acc_def in REQUIRED_ACCOUNTS:
acc = frappe.db.get_value("Account", {
"company": company,
"account_number": acc_def["account_number"],
}, ["name", "account_name"], as_dict=True)
status = {
"number": acc_def["account_number"],
"name": acc_def["account_name"],
"description": acc_def["description"],
"needed_for": acc_def["needed_for"],
"exists": bool(acc),
"account": acc.name if acc else None,
}
accounts.append(status)
if not acc:
missing.append(status)
return {
"accounts": accounts,
"missing": missing,
"method": method,
"needs_kassa_accounts": "Kassa" in method,
}
@frappe.whitelist()
def create_missing_accounts(company):
"""API: Create all missing required accounts for a company."""
if not company:
return {"created": []}
abbr = frappe.db.get_value("Company", company, "abbr")
created = []
for acc_def in REQUIRED_ACCOUNTS:
exists = frappe.db.exists("Account", {
"company": company,
"account_number": acc_def["account_number"],
})
if exists:
continue
_create_account(company, abbr, acc_def)
created.append(acc_def["account_number"])
return {"created": created}

View File

@ -0,0 +1,75 @@
{
"actions": [],
"creation": "2026-04-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"tax_type",
"column_break_1",
"debit_account",
"column_break_2",
"credit_account",
"column_break_3",
"cost_center"
],
"fields": [
{
"fieldname": "tax_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Tax Type",
"options": "VAT (ƏDV)\nProfit Tax (Mənfəət)\nSimplified Tax (Sadələşdirilmiş)\nProperty Tax (Əmlak)\nLand Tax (Torpaq)\nExcise Tax (Aksiz)\nMining Tax (Mədən)\nRoad Tax (Yol)\nWithholding Tax (ÖMV)",
"reqd": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"description": "Expense/Asset account to debit (e.g., 901 for profit tax, 226 for VAT input)",
"fieldname": "debit_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Debit Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"description": "Liability account to credit (e.g., 521.1 for profit tax payable, 521.3 for VAT)",
"fieldname": "credit_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Credit Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"description": "Optional: override default cost center for this tax type. Leave blank to use the default.",
"fieldname": "cost_center",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Cost Center",
"options": "Cost Center"
}
],
"index_web_pages_for_search": 0,
"istable": 1,
"links": [],
"modified": "2026-04-01 14:00:00.000000",
"modified_by": "Administrator",
"module": "Taxes Az",
"name": "Tax Closing Account Map",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,5 @@
from frappe.model.document import Document
class TaxClosingAccountMap(Document):
pass

View File

@ -0,0 +1,118 @@
{
"actions": [],
"creation": "2026-04-01 18:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enabled",
"article",
"column_break_1",
"benefit_name",
"applies_to",
"column_break_2",
"exemption_percent",
"condition_met",
"column_break_3",
"condition_details",
"law_url",
"tax_reduction"
],
"fields": [
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Apply"
},
{
"fieldname": "article",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Article (Maddə)",
"read_only": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "benefit_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Benefit",
"read_only": 1
},
{
"fieldname": "applies_to",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Applies To",
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "exemption_percent",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "Exemption %",
"read_only": 1
},
{
"fieldname": "condition_met",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Condition",
"options": "Yes\nNo\nPartial\nManual Check",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "condition_details",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Details",
"read_only": 1,
"columns": 3
},
{
"fieldname": "law_reference",
"fieldtype": "Small Text",
"label": "Law Reference",
"read_only": 1
},
{
"fieldname": "law_url",
"fieldtype": "Data",
"label": "Law URL",
"read_only": 1,
"hidden": 1
},
{
"fieldname": "tax_reduction",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Tax Reduction",
"precision": "2",
"read_only": 1
}
],
"index_web_pages_for_search": 0,
"istable": 1,
"links": [],
"modified": "2026-04-01 18:00:00.000000",
"modified_by": "Administrator",
"module": "Taxes Az",
"name": "Tax Closing Benefit",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,5 @@
from frappe.model.document import Document
class TaxClosingBenefit(Document):
pass

View File

@ -0,0 +1,125 @@
{
"actions": [],
"creation": "2026-04-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"tax_type",
"sub_period",
"column_break_1",
"posting_date",
"debit_account",
"credit_account",
"column_break_2",
"amount",
"remark",
"column_break_3",
"status",
"existing_entry",
"existing_amount",
"difference"
],
"fields": [
{
"fieldname": "tax_type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Tax Type",
"read_only": 1
},
{
"fieldname": "sub_period",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Sub-Period",
"read_only": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"read_only": 1
},
{
"fieldname": "debit_account",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Debit Account",
"read_only": 1
},
{
"fieldname": "credit_account",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Credit Account",
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Calculated Amount",
"precision": "2",
"read_only": 1
},
{
"fieldname": "remark",
"fieldtype": "Small Text",
"label": "Remark",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Pending\nExisting\nDifference\nCreated\nFailed\nSkipped",
"read_only": 1
},
{
"fieldname": "existing_entry",
"fieldtype": "Small Text",
"label": "Existing Entry",
"read_only": 1
},
{
"fieldname": "existing_amount",
"fieldtype": "Currency",
"label": "Existing Amount",
"precision": "2",
"read_only": 1
},
{
"fieldname": "difference",
"fieldtype": "Currency",
"label": "Difference",
"precision": "2",
"read_only": 1
}
],
"index_web_pages_for_search": 0,
"istable": 1,
"links": [],
"modified": "2026-04-01 16:00:00.000000",
"modified_by": "Administrator",
"module": "Taxes Az",
"name": "Tax Closing Entry Preview",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,5 @@
from frappe.model.document import Document
class TaxClosingEntryPreview(Document):
pass

View File

@ -0,0 +1,74 @@
{
"actions": [],
"creation": "2026-04-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"journal_entry",
"column_break_1",
"tax_type",
"sub_period",
"column_break_2",
"amount",
"posting_date"
],
"fields": [
{
"fieldname": "journal_entry",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Journal Entry",
"options": "Journal Entry",
"read_only": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "tax_type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Tax Type",
"read_only": 1
},
{
"fieldname": "sub_period",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Sub-Period",
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"precision": "2",
"read_only": 1
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"read_only": 1
}
],
"index_web_pages_for_search": 0,
"istable": 1,
"links": [],
"modified": "2026-04-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Taxes Az",
"name": "Tax Closing Journal Entry",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,5 @@
from frappe.model.document import Document
class TaxClosingJournalEntry(Document):
pass

View File

@ -0,0 +1,61 @@
{
"actions": [],
"creation": "2026-04-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"timestamp",
"phase",
"column_break_1",
"level",
"message"
],
"fields": [
{
"fieldname": "timestamp",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Timestamp",
"read_only": 1
},
{
"fieldname": "phase",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Phase",
"options": "Initialization\nTax Detection\nData Collection\nCalculation\nEntry Preview\nEntry Creation\nPeriod Closing\nSummary\nSetup\nStep Complete\nStep Skipped\nBenefit Detection",
"read_only": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "level",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Level",
"options": "Info\nSuccess\nWarning\nError\nHeader",
"read_only": 1
},
{
"fieldname": "message",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Message",
"read_only": 1
}
],
"index_web_pages_for_search": 0,
"istable": 1,
"links": [],
"modified": "2026-04-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Taxes Az",
"name": "Tax Closing Log Entry",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,5 @@
from frappe.model.document import Document
class TaxClosingLogEntry(Document):
pass

View File

@ -0,0 +1,65 @@
{
"actions": [],
"creation": "2026-04-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"component",
"column_break_1",
"amount",
"column_break_2",
"account",
"source"
],
"fields": [
{
"fieldname": "component",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Component",
"read_only": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"precision": "2",
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "account",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Account",
"read_only": 1
},
{
"fieldname": "source",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Source",
"read_only": 1
}
],
"index_web_pages_for_search": 0,
"istable": 1,
"links": [],
"modified": "2026-04-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Taxes Az",
"name": "Tax Closing Payroll Summary",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,5 @@
from frappe.model.document import Document
class TaxClosingPayrollSummary(Document):
pass

View File

@ -0,0 +1,119 @@
{
"actions": [],
"creation": "2026-04-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enabled",
"tax_type",
"column_break_1",
"period_type",
"sub_period",
"column_break_2",
"source_amount",
"rate",
"tax_amount",
"column_break_3",
"status",
"declaration_deadline",
"payment_deadline"
],
"fields": [
{
"default": "1",
"fieldname": "enabled",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Enabled"
},
{
"fieldname": "tax_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Tax Type",
"options": "VAT (ƏDV)\nProfit Tax (Mənfəət)\nSimplified Tax (Sadələşdirilmiş)\nProperty Tax (Əmlak)\nLand Tax (Torpaq)\nExcise Tax (Aksiz)\nMining Tax (Mədən)\nRoad Tax (Yol)\nWithholding Tax (ÖMV)\nUnemployment Insurance (İşsizlik)\nDSMF\nMedical Insurance (Tibbi sığorta)",
"reqd": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "period_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Period Type",
"options": "Monthly\nQuarterly\nAnnually",
"read_only": 1
},
{
"fieldname": "sub_period",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Sub-Period",
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "source_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Source Amount",
"precision": "2",
"read_only": 1
},
{
"fieldname": "rate",
"fieldtype": "Percent",
"label": "Rate (%)",
"read_only": 1
},
{
"fieldname": "tax_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Tax Amount",
"precision": "2",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Pending\nCalculated\nEntry Created\nData Only\nSkipped\nError",
"read_only": 1
},
{
"fieldname": "declaration_deadline",
"fieldtype": "Date",
"label": "Declaration Deadline",
"read_only": 1
},
{
"fieldname": "payment_deadline",
"fieldtype": "Date",
"label": "Payment Deadline",
"read_only": 1
}
],
"index_web_pages_for_search": 0,
"istable": 1,
"links": [],
"modified": "2026-04-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Taxes Az",
"name": "Tax Closing Tax Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,5 @@
from frappe.model.document import Document
class TaxClosingTaxItem(Document):
pass

View File

@ -0,0 +1,395 @@
frappe.ui.form.on("Tax Period Closing Settings", {
refresh(frm) {
frm.trigger("render_detection_status");
frm.trigger("render_regime_explanation");
frm.trigger("setup_buttons");
},
setup_buttons(frm) {
// Auto-detect button — always available
if (frm.doc.company) {
frm.add_custom_button(__("Auto-detect from Company"), () => {
frm.set_value("auto_detect_on_save", 1);
frm.save();
}, __("Setup"));
frm.add_custom_button(__("Auto-detect Accounts"), () => {
frm.trigger("detect_accounts");
}, __("Setup"));
}
// Load defaults buttons
frm.add_custom_button(__("Load Default AZ Rates"), () => {
frm.trigger("load_default_rates");
}, __("Setup"));
frm.add_custom_button(__("Load Default Account Map"), () => {
if (!frm.doc.company) {
frappe.msgprint(__("Please select a Company first."));
return;
}
frm.trigger("detect_accounts_and_map");
}, __("Setup"));
},
company(frm) {
if (frm.doc.company && frm.doc.auto_detect_on_save) {
frappe.show_alert({
message: __("Company changed. Save to run auto-detection."),
indicator: "blue"
});
}
frm.trigger("render_detection_status");
},
default_tax_regime(frm) {
frm.trigger("render_regime_explanation");
frm.trigger("sync_regime_enables");
},
// ── Sync regime with enables ──────────────────────────
sync_regime_enables(frm) {
const regime = frm.doc.default_tax_regime;
if (regime === "General (VAT + Profit)") {
frm.set_value("enable_vat", 1);
frm.set_value("enable_profit_tax", 1);
frm.set_value("enable_simplified_tax", 0);
} else if (regime === "Simplified Tax" || regime === "Special Regime") {
frm.set_value("enable_vat", 0);
frm.set_value("enable_profit_tax", 0);
frm.set_value("enable_simplified_tax", 1);
}
},
// ── Contradiction warnings in real-time ───────────────
enable_vat(frm) {
if (frm.doc.enable_vat && frm.doc.default_tax_regime !== "General (VAT + Profit)") {
frappe.show_alert({
message: __("VAT is typically not applicable in Simplified/Special regime."),
indicator: "orange"
});
}
},
enable_profit_tax(frm) {
if (frm.doc.enable_profit_tax && frm.doc.default_tax_regime !== "General (VAT + Profit)") {
frappe.show_alert({
message: __("Profit Tax is replaced by Simplified Tax in non-General regimes."),
indicator: "orange"
});
}
},
enable_simplified_tax(frm) {
if (frm.doc.enable_simplified_tax && frm.doc.default_tax_regime === "General (VAT + Profit)") {
frappe.show_alert({
message: __("Simplified Tax is not applicable in General (VAT + Profit) regime."),
indicator: "orange"
});
}
},
auto_submit_entries(frm) {
if (frm.doc.auto_submit_entries) {
frappe.confirm(
__("Auto-submit means journal entries will be posted immediately and will affect account balances. Submitted entries require cancellation to undo. Are you sure?"),
() => {},
() => { frm.set_value("auto_submit_entries", 0); }
);
}
},
enable_period_closing(frm) {
if (frm.doc.enable_period_closing && frm.doc.auto_submit_entries) {
frappe.show_alert({
message: __("Warning: Both auto-submit and period closing are ON. Period Closing Voucher is irreversible once submitted."),
indicator: "red"
});
}
},
vat_input_account(frm) {
if (frm.doc.vat_input_account && frm.doc.vat_output_account
&& frm.doc.vat_input_account === frm.doc.vat_output_account) {
frappe.show_alert({
message: __("VAT Input and Output accounts must be different!"),
indicator: "red"
});
frm.set_value("vat_input_account", "");
}
},
vat_output_account(frm) {
if (frm.doc.vat_input_account && frm.doc.vat_output_account
&& frm.doc.vat_input_account === frm.doc.vat_output_account) {
frappe.show_alert({
message: __("VAT Input and Output accounts must be different!"),
indicator: "red"
});
frm.set_value("vat_output_account", "");
}
},
profit_tax_advance_method(frm) {
if (frm.doc.profit_tax_advance_method === "1/4 of Previous Year Tax"
&& !frm.doc.profit_tax_previous_year_amount) {
frappe.show_alert({
message: __("Please enter the previous year's total profit tax amount below."),
indicator: "orange"
});
}
},
// ── Regime Explanation HTML ────────────────────────────
render_regime_explanation(frm) {
const regime = frm.doc.default_tax_regime || "";
let html = "";
const explanations = {
"General (VAT + Profit)": {
color: "#28a745",
icon: "📊",
taxes: [
__("VAT (ƏDV) — 18%, monthly, declaration by 20th of next month"),
__("Profit Tax (Mənfəət) — 20%, annual declaration (Mar 31), quarterly advances"),
__("Property Tax (Əmlak) — 1%, annual declaration, quarterly advances"),
],
note: __("Most common regime for medium/large businesses with VAT registration.")
},
"Simplified Tax": {
color: "#17a2b8",
icon: "📋",
taxes: [
__("Simplified Tax — 2%/4%/8% of revenue, quarterly"),
__("Property Tax (Əmlak) — 1%, annual declaration, quarterly advances"),
],
note: __("For micro/small businesses WITHOUT VAT registration. Replaces VAT + Profit Tax.")
},
"Special Regime": {
color: "#6f42c1",
icon: "🏗",
taxes: [
__("Special Simplified Tax — rates vary by activity type"),
__("Property Tax (Əmlak) — 1%, annual declaration, quarterly advances"),
],
note: __("For construction, real estate, housing, cash withdrawals. Uses special declaration forms.")
}
};
const info = explanations[regime];
if (info) {
html = `
<div style="padding: 12px; border-left: 4px solid ${info.color}; background: #f8f9fa; border-radius: 4px; margin-bottom: 10px;">
<div style="font-weight: bold; color: ${info.color}; margin-bottom: 8px;">
${info.icon} ${regime}
</div>
<div style="font-size: 12px; margin-bottom: 8px;">
<strong>${__("Applicable taxes:")}</strong><br>
${info.taxes.map(t => `&nbsp;&nbsp;• ${t}`).join("<br>")}
</div>
<div style="font-size: 11px; color: #666; font-style: italic;">
${info.note}
</div>
</div>
`;
}
if (frm.fields_dict.regime_explanation_html) {
frm.fields_dict.regime_explanation_html.$wrapper.html(html);
}
},
// ── Detection Status HTML ─────────────────────────────
render_detection_status(frm) {
if (!frm.doc.company) {
if (frm.fields_dict.detection_status_html) {
frm.fields_dict.detection_status_html.$wrapper.html(
`<div style="padding: 10px; color: #999; font-size: 12px;">${__("Select a Company to enable auto-detection.")}</div>`
);
}
return;
}
const checks = [
{
label: __("Company"),
ok: !!frm.doc.company,
value: frm.doc.company || __("Not set")
},
{
label: __("Tax Regime"),
ok: !!frm.doc.default_tax_regime,
value: frm.doc.default_tax_regime || __("Not detected")
},
{
label: __("VAT Accounts"),
ok: frm.doc.default_tax_regime !== "General (VAT + Profit)" || (!!frm.doc.vat_input_account && !!frm.doc.vat_output_account),
value: frm.doc.vat_input_account && frm.doc.vat_output_account
? `${frm.doc.vat_input_account} / ${frm.doc.vat_output_account}`
: (frm.doc.default_tax_regime !== "General (VAT + Profit)" ? __("N/A (Simplified)") : __("Not set"))
},
{
label: __("Account Mapping"),
ok: (frm.doc.account_map || []).length > 0,
value: __("{0} mappings", [(frm.doc.account_map || []).length])
},
{
label: __("Tax Rates"),
ok: (frm.doc.tax_rates || []).length > 0,
value: __("{0} rates", [(frm.doc.tax_rates || []).length])
},
{
label: __("Cost Center"),
ok: !!frm.doc.default_cost_center,
value: frm.doc.default_cost_center || __("Will use company default")
}
];
let html = '<div style="display: flex; flex-wrap: wrap; gap: 6px; padding: 5px 0;">';
for (const c of checks) {
const color = c.ok ? "#28a745" : "#dc3545";
const bg = c.ok ? "#d4edda" : "#f8d7da";
const icon = c.ok ? "✓" : "✗";
html += `
<div style="background: ${bg}; border: 1px solid ${color}; border-radius: 4px; padding: 4px 8px; font-size: 11px;" title="${c.value}">
<span style="color: ${color}; font-weight: bold;">${icon}</span> ${c.label}
</div>
`;
}
html += '</div>';
if (frm.fields_dict.detection_status_html) {
frm.fields_dict.detection_status_html.$wrapper.html(html);
}
},
// ── Detect Accounts ───────────────────────────────────
detect_accounts(frm) {
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_period_closing_settings.tax_period_closing_settings.auto_detect_accounts",
args: { company: frm.doc.company },
callback(r) {
if (!r.message || !r.message.accounts) return;
const accounts = r.message.accounts;
let found = [];
if (accounts["226"]) {
frm.set_value("vat_input_account", accounts["226"].name);
found.push(`VAT Input → ${accounts["226"].name}`);
}
if (accounts["521.3"]) {
frm.set_value("vat_output_account", accounts["521.3"].name);
found.push(`VAT Output → ${accounts["521.3"].name}`);
}
if (accounts["341"]) {
frm.set_value("closing_account", accounts["341"].name);
found.push(`Closing Account → ${accounts["341"].name}`);
}
if (found.length) {
frappe.show_alert({
message: __("Found {0} accounts", [found.length]),
indicator: "green"
});
} else {
frappe.show_alert({
message: __("No standard AZ accounts found. Set up manually."),
indicator: "orange"
});
}
frm.trigger("render_detection_status");
}
});
},
detect_accounts_and_map(frm) {
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_period_closing_settings.tax_period_closing_settings.auto_detect_accounts",
args: { company: frm.doc.company },
callback(r) {
if (!r.message || !r.message.accounts) return;
const accounts = r.message.accounts;
// Set individual account fields
if (accounts["226"]) frm.set_value("vat_input_account", accounts["226"].name);
if (accounts["521.3"]) frm.set_value("vat_output_account", accounts["521.3"].name);
if (accounts["341"]) frm.set_value("closing_account", accounts["341"].name);
// Build account mapping table
const map_defs = [
{ tax_type: "Profit Tax (Mənfəət)", debit: "901", credit: "521.1" },
{ tax_type: "Simplified Tax (Sadələşdirilmiş)", debit: "731", credit: "521.9" },
{ tax_type: "Property Tax (Əmlak)", debit: "731", credit: "521.5" },
{ tax_type: "Land Tax (Torpaq)", debit: "731", credit: "521.6" },
{ tax_type: "Excise Tax (Aksiz)", debit: "731", credit: "521.4" },
{ tax_type: "Mining Tax (Mədən)", debit: "731", credit: "521.8" },
{ tax_type: "Road Tax (Yol)", debit: "731", credit: "521.7" },
];
frm.doc.account_map = [];
let mapped = 0;
for (const def of map_defs) {
const debit_acc = accounts[def.debit];
const credit_acc = accounts[def.credit];
if (debit_acc && credit_acc) {
let row = frm.add_child("account_map");
row.tax_type = def.tax_type;
row.debit_account = debit_acc.name;
row.credit_account = credit_acc.name;
mapped++;
}
}
frm.refresh_field("account_map");
frappe.show_alert({
message: __("{0} account mappings created from Chart of Accounts", [mapped]),
indicator: mapped > 0 ? "green" : "orange"
});
frm.trigger("render_detection_status");
}
});
},
// ── Load Default Rates ────────────────────────────────
load_default_rates(frm) {
const defaults = [
{ tax_type: "VAT (ƏDV)", rate: 18, description: __("Standard VAT rate — Maddə 159 Tax Code") },
{ tax_type: "Profit Tax (Mənfəət)", rate: 20, description: __("Corporate profit tax — Maddə 104 Tax Code") },
{ tax_type: "Simplified Tax - Trade Baku (Sadələşdirilmiş - Ticarət Bakı)", rate: 2, description: __("Retail/wholesale trade in Baku — Maddə 220.1") },
{ tax_type: "Simplified Tax - Catering (Sadələşdirilmiş - İaşə)", rate: 8, description: __("Catering/public food services — Maddə 220.1") },
{ tax_type: "Simplified Tax - Other (Sadələşdirilmiş - Digər)", rate: 4, description: __("Other activities — Maddə 220.1") },
{ tax_type: "Property Tax (Əmlak)", rate: 1, description: __("Fixed assets — Maddə 199 Tax Code") },
{ tax_type: "Unemployment Insurance Employer (İşsizlik - İşəgötürən)", rate: 0.5, description: __("Employer contribution — İşsizlikdən sığorta haqqında qanun") },
{ tax_type: "Unemployment Insurance Employee (İşsizlik - İşçi)", rate: 0.5, description: __("Employee withholding — İşsizlikdən sığorta haqqında qanun") },
{ tax_type: "DSMF Employer (DSMF - İşəgötürən)", rate: 22, description: __("Social insurance employer — DSMF qanunu") },
{ tax_type: "DSMF Employee (DSMF - İşçi)", rate: 3, description: __("Social insurance employee — DSMF qanunu") },
{ tax_type: "Medical Insurance Employer (Tibbi sığorta - İşəgötürən)", rate: 2, description: __("Medical insurance employer — İcbari tibbi sığorta qanunu") },
{ tax_type: "Medical Insurance Employee (Tibbi sığorta - İşçi)", rate: 2, description: __("Medical insurance employee — İcbari tibbi sığorta qanunu") }
];
frm.doc.tax_rates = [];
defaults.forEach(d => {
let row = frm.add_child("tax_rates");
row.tax_type = d.tax_type;
row.rate = d.rate;
row.description = d.description;
});
frm.refresh_field("tax_rates");
frappe.show_alert({
message: __("{0} default AZ tax rates loaded", [defaults.length]),
indicator: "green"
});
}
});

View File

@ -0,0 +1,686 @@
{
"actions": [],
"creation": "2026-04-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"issingle": 1,
"field_order": [
"company_section",
"company",
"column_break_comp1",
"auto_detect_on_save",
"detection_status_html",
"accounting_method_section",
"accounting_method",
"column_break_am1",
"deferred_vat_sales_account",
"deferred_vat_purchase_account",
"column_break_am2",
"deferred_revenue_account",
"regime_section",
"auto_detect_regime",
"default_tax_regime",
"column_break_reg1",
"regime_explanation_html",
"scan_section",
"scan_existing_entries",
"column_break_scan1",
"force_create_if_exists",
"column_break_scan2",
"offer_correction_entry",
"behavior_section",
"auto_submit_entries",
"column_break_beh1",
"allow_reclosing",
"column_break_beh2",
"duplicate_check_scope",
"pcv_section",
"enable_period_closing",
"column_break_pcv1",
"closing_account",
"declaration_section",
"create_declaration_draft",
"column_break_decl1",
"auto_fill_declaration",
"cost_center_section",
"default_cost_center",
"column_break_cc1",
"cost_center_per_tax",
"vat_section",
"enable_vat",
"vat_rate",
"column_break_vat1",
"vat_input_account",
"vat_output_account",
"column_break_vat2",
"use_vat_allocation",
"vat_data_source",
"profit_tax_section",
"enable_profit_tax",
"profit_tax_rate",
"column_break_pt1",
"profit_tax_advance_method",
"column_break_pt2",
"profit_tax_previous_year_amount",
"simplified_tax_section",
"enable_simplified_tax",
"simplified_tax_auto_rate",
"column_break_st1",
"default_simplified_rate",
"column_break_st2",
"simplified_tax_revenue_source",
"property_tax_section",
"enable_property_tax",
"property_tax_rate",
"column_break_prop1",
"property_tax_threshold",
"column_break_prop2",
"property_tax_calc_method",
"land_tax_section",
"enable_land_tax",
"column_break_lt1",
"land_tax_source",
"other_taxes_section",
"enable_excise_tax",
"enable_mining_tax",
"column_break_ot1",
"enable_road_tax",
"payroll_section",
"enable_payroll_collection",
"column_break_pay1",
"payroll_data_source",
"tax_rates_section",
"tax_rates",
"account_mapping_section",
"account_map"
],
"fields": [
{
"fieldname": "company_section",
"fieldtype": "Section Break",
"label": "Company & Auto-Detection"
},
{
"description": "Select the company for which these settings apply. All auto-detection (regime, accounts, rates) will use data from this company. If you have multiple companies, the wizard will let you override per-run.",
"fieldname": "company",
"fieldtype": "Link",
"label": "Default Company",
"options": "Company"
},
{
"fieldname": "column_break_comp1",
"fieldtype": "Column Break"
},
{
"default": "1",
"description": "When enabled, saving this form will automatically detect: tax regime, VAT payer status, OKVED code, available assets, employees, and set recommended values for all settings below. You can always override any auto-detected value manually.",
"fieldname": "auto_detect_on_save",
"fieldtype": "Check",
"label": "Auto-detect parameters on Save"
},
{
"fieldname": "detection_status_html",
"fieldtype": "HTML",
"label": "Detection Status"
},
{
"fieldname": "accounting_method_section",
"fieldtype": "Section Break",
"label": "Accounting Method"
},
{
"description": "Read from Company settings. Kassa metodu: revenue, expenses, and VAT obligations are recognized only when payment is received/made. Hesablama metodu: recognized at invoice date.\n\nPer AZ Tax Code: Micro and Small entrepreneurs can use Kassa metodu. Medium and Big must use Hesablama metodu.",
"fieldname": "accounting_method",
"fieldtype": "Select",
"label": "Accounting Method",
"options": "\nKassa metodu\nHesablama metodu",
"read_only": 1
},
{
"fieldname": "column_break_am1",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.accounting_method === 'Kassa metodu'",
"description": "Account 245.1 — ƏDV gözləmədə (satış). Deferred output VAT — posted at Invoice, transferred to 521.3 at Payment.",
"fieldname": "deferred_vat_sales_account",
"fieldtype": "Link",
"label": "Deferred VAT (Sales)",
"options": "Account"
},
{
"depends_on": "eval:doc.accounting_method === 'Kassa metodu'",
"description": "Account 245.2 — ƏDV gözləmədə (alış). Deferred input VAT — posted at Invoice, transferred to 226 at Payment.",
"fieldname": "deferred_vat_purchase_account",
"fieldtype": "Link",
"label": "Deferred VAT (Purchases)",
"options": "Account"
},
{
"fieldname": "column_break_am2",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.accounting_method === 'Kassa metodu'",
"description": "Account 542 — Gələcək hesabat dövrünün gəlirləri. Deferred revenue — posted at Invoice, transferred to 601 at Payment.",
"fieldname": "deferred_revenue_account",
"fieldtype": "Link",
"label": "Deferred Revenue (542)",
"options": "Account"
},
{
"fieldname": "regime_section",
"fieldtype": "Section Break",
"label": "Tax Regime"
},
{
"default": "1",
"description": "If ON: the tax regime is determined automatically from the company's VAT certificate and OKVED code. Companies with a VAT certificate → General regime. Companies without → Simplified. Special OKVED codes (construction, real estate) → Special regime. If OFF: the regime below is used as-is for every wizard run.",
"fieldname": "auto_detect_regime",
"fieldtype": "Check",
"label": "Auto-detect Tax Regime"
},
{
"default": "General (VAT + Profit)",
"description": "This determines which taxes the wizard will calculate:\n\n• General (VAT + Profit) — VAT monthly + Profit Tax quarterly. Most common for medium/large businesses registered as VAT payers.\n• Simplified Tax — Quarterly simplified tax on revenue. For micro/small businesses NOT registered for VAT.\n• Special Regime — Construction, real estate, housing, cash withdrawals. Uses simplified tax with special declaration forms.\n\nIf 'Auto-detect' is ON above, this field shows the detected value but can still be changed.",
"fieldname": "default_tax_regime",
"fieldtype": "Select",
"label": "Default Tax Regime",
"options": "General (VAT + Profit)\nSimplified Tax\nSpecial Regime"
},
{
"fieldname": "column_break_reg1",
"fieldtype": "Column Break"
},
{
"fieldname": "regime_explanation_html",
"fieldtype": "HTML",
"label": "Regime Explanation"
},
{
"fieldname": "scan_section",
"fieldtype": "Section Break",
"label": "Smart Scan — Existing Entry Detection"
},
{
"default": "1",
"description": "If ON (recommended): before creating any journal entry, the wizard scans GL Entries to check if a matching entry already exists for the same tax type, accounts, and period.\n\nIf an existing entry is found:\n• The wizard marks it as 'Existing' (green) and shows the existing JE number and amount\n• No duplicate entry is created\n• If the calculated amount differs from the existing amount, a warning with the difference is shown\n\nThis is essential when tax entries come from external sources (e.g., e-taxes portal imports) or were created manually. The wizard automatically adapts — no configuration needed per period.",
"fieldname": "scan_existing_entries",
"fieldtype": "Check",
"label": "Scan for Existing Entries"
},
{
"fieldname": "column_break_scan1",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "scan_existing_entries",
"description": "If ON: the wizard will create a new journal entry even if a matching one already exists. Use only if you intentionally need duplicate entries (very rare).\n\nIf OFF (default, safe): existing entries are respected and not duplicated.",
"fieldname": "force_create_if_exists",
"fieldtype": "Check",
"label": "Force Create Even if Exists"
},
{
"fieldname": "column_break_scan2",
"fieldtype": "Column Break"
},
{
"default": "1",
"depends_on": "scan_existing_entries",
"description": "If ON: when an existing entry is found but its amount differs from the wizard's calculation, the wizard will offer a correction entry for the difference.\n\nFor example: existing VAT entry = 1,296 AZN, wizard calculates 1,300 AZN → offers a correction entry of 4 AZN.\n\nThe correction entry is shown in Entry Preview with status 'Difference' (orange) and is optional — you can disable it per row.\n\nIf OFF: differences are only logged as warnings, no correction entries are offered.",
"fieldname": "offer_correction_entry",
"fieldtype": "Check",
"label": "Offer Correction Entry for Differences"
},
{
"fieldname": "behavior_section",
"fieldtype": "Section Break",
"label": "Journal Entry Behavior"
},
{
"default": "0",
"description": "If ON: journal entries created by the wizard will be immediately submitted (docstatus=1) and will affect account balances right away. Use this only if you trust the calculation and don't need manual review.\n\nIf OFF (recommended): entries are created as Draft. You can review each one in the Journal Entry list and submit manually or in bulk. This is safer — you can edit or delete drafts if something looks wrong.",
"fieldname": "auto_submit_entries",
"fieldtype": "Check",
"label": "Auto-submit Journal Entries"
},
{
"fieldname": "column_break_beh1",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Controls what happens when you run the wizard for a period that was already closed.\n\nIf OFF (default, safe): the wizard will refuse to run and show which previous wizard closed this period. This prevents accidental double-posting.\n\nIf ON: the wizard will cancel all journal entries from the previous run and create new ones. The old wizard document will be marked as superseded. Use this when you need to correct previously posted tax entries (e.g., after fixing invoices or asset values).",
"fieldname": "allow_reclosing",
"fieldtype": "Check",
"label": "Allow Re-closing Periods"
},
{
"fieldname": "column_break_beh2",
"fieldtype": "Column Break"
},
{
"default": "Same Company + Same Period",
"depends_on": "eval:!doc.allow_reclosing",
"description": "How the wizard checks for duplicate closings:\n\n• Same Company + Same Period — blocks if the exact same company and date range was already closed. Most restrictive.\n• Same Company + Overlapping Dates — blocks if any part of the period overlaps with a previous closing. Catches partial overlaps.\n\nOnly relevant when 'Allow Re-closing' is OFF.",
"fieldname": "duplicate_check_scope",
"fieldtype": "Select",
"label": "Duplicate Check Scope",
"options": "Same Company + Same Period\nSame Company + Overlapping Dates"
},
{
"fieldname": "pcv_section",
"fieldtype": "Section Break",
"label": "Period Closing Voucher (P&L Close)"
},
{
"default": "0",
"description": "If ON: after creating tax journal entries, the wizard will also create a standard ERPNext Period Closing Voucher. This closes all Income and Expense accounts and transfers the net result to the Closing Account below.\n\nIf OFF (default): the wizard only creates tax accrual entries. You close P&L manually via Accounting → Period Closing Voucher. Recommended to keep OFF if your accountant closes periods separately.\n\nIMPORTANT: Period Closing Voucher is irreversible after submission. The wizard will create it in Draft if 'Auto-submit' is OFF.",
"fieldname": "enable_period_closing",
"fieldtype": "Check",
"label": "Create Period Closing Voucher"
},
{
"fieldname": "column_break_pcv1",
"fieldtype": "Column Break"
},
{
"depends_on": "enable_period_closing",
"description": "The equity/liability account where net profit or loss will be transferred when P&L accounts are closed.\n\nStandard AZ Chart of Accounts:\n• 341 — Hesabat dövrünün xalis mənfəəti (Current period net profit)\n• 342 — Keçmiş illərin bölüşdürülməmiş mənfəəti (Retained earnings)\n\nThe account MUST be of type Liability or Equity, and in the same currency as the company.",
"fieldname": "closing_account",
"fieldtype": "Link",
"label": "Closing Account",
"options": "Account"
},
{
"fieldname": "declaration_section",
"fieldtype": "Section Break",
"collapsible": 1,
"collapsible_depends_on": "eval:!doc.create_declaration_draft",
"label": "Declaration Integration (Future)"
},
{
"default": "0",
"description": "If ON: the wizard will create a draft of the corresponding tax declaration (e.g., Declaration of Value Added Tax, Income Tax Return) pre-filled with calculated values.\n\nIf OFF (default): only journal entries are created. Declarations are filed separately via Tax Declaration doctypes or the E-Taxes portal.\n\nNote: This feature is under development. Currently only VAT and Simplified Tax declarations are supported.",
"fieldname": "create_declaration_draft",
"fieldtype": "Check",
"label": "Create Declaration Draft"
},
{
"fieldname": "column_break_decl1",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "create_declaration_draft",
"description": "If ON: declaration fields (VOEN, activity code, tax authority, etc.) will be auto-filled from Company data and calculated amounts.\n\nIf OFF: only the amounts section will be filled; header fields (VOEN, period, etc.) are left blank for manual entry.\n\nRequires 'Create Declaration Draft' to be enabled.",
"fieldname": "auto_fill_declaration",
"fieldtype": "Check",
"label": "Auto-fill Declaration Header"
},
{
"fieldname": "cost_center_section",
"fieldtype": "Section Break",
"collapsible": 1,
"label": "Cost Center"
},
{
"description": "All tax journal entries will use this cost center by default. If left blank, the company's default cost center is used.\n\nYou can override this per wizard run.",
"fieldname": "default_cost_center",
"fieldtype": "Link",
"label": "Default Cost Center",
"options": "Cost Center"
},
{
"fieldname": "column_break_cc1",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "If ON: each tax type can have its own cost center (configured in the Account Mapping table below). For example, VAT entries go to 'Sales' cost center, while property tax goes to 'Admin'.\n\nIf OFF (default): all tax entries use the single default cost center above.",
"fieldname": "cost_center_per_tax",
"fieldtype": "Check",
"label": "Separate Cost Center per Tax Type"
},
{
"fieldname": "vat_section",
"fieldtype": "Section Break",
"label": "VAT (Value Added Tax)",
"depends_on": "eval:doc.default_tax_regime === 'General (VAT + Profit)'"
},
{
"default": "1",
"description": "Enable VAT calculation in the wizard. The wizard will calculate the difference between output VAT (collected from customers) and input VAT (paid to suppliers) for each month in the period.\n\nDisable this only if you handle VAT entries manually or via a separate process.",
"fieldname": "enable_vat",
"fieldtype": "Check",
"label": "Enable VAT Calculation"
},
{
"default": "18",
"depends_on": "enable_vat",
"description": "Standard VAT rate in Azerbaijan is 18% (Maddə 159 of the Tax Code). This is used for reference and reporting only — the actual VAT amounts are calculated from GL account balances, not by applying this rate.\n\nChange only if the government announces a new rate.",
"fieldname": "vat_rate",
"fieldtype": "Percent",
"label": "VAT Rate (%)"
},
{
"fieldname": "column_break_vat1",
"fieldtype": "Column Break"
},
{
"depends_on": "enable_vat",
"description": "The asset account where input VAT (VAT paid to suppliers on purchases) is recorded.\n\nStandard AZ: Account 226 — ƏDV sub-uçot hesabı.\n\nThis account has a DEBIT balance. The wizard reads this balance to determine how much input VAT you can offset against output VAT.",
"fieldname": "vat_input_account",
"fieldtype": "Link",
"label": "VAT Input Account",
"options": "Account"
},
{
"depends_on": "enable_vat",
"description": "The liability account where output VAT (VAT collected from customers on sales) is recorded.\n\nStandard AZ: Account 521.3 — ƏDV.\n\nThis account has a CREDIT balance. The wizard reads this balance and offsets it against input VAT to determine the net VAT payable to the budget.",
"fieldname": "vat_output_account",
"fieldtype": "Link",
"label": "VAT Output Account",
"options": "Account"
},
{
"fieldname": "column_break_vat2",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "enable_vat",
"description": "If ON: after calculating VAT from GL balances, the wizard will compare the result with the VAT Allocation doctype (if you use it). Any discrepancy will be logged as a warning.\n\nIf OFF: VAT is calculated purely from GL account balances. This is simpler and works for most companies.\n\nEnable only if you actively use the VAT Allocation system in taxes_az for distributing input VAT across taxable/exempt operations.",
"fieldname": "use_vat_allocation",
"fieldtype": "Check",
"label": "Cross-check with VAT Allocation"
},
{
"default": "GL Account Balance",
"depends_on": "enable_vat",
"description": "How VAT amounts are determined:\n\n• GL Account Balance — reads debit/credit balances from VAT accounts (226 and 521.3) for each month. Simple, reliable, captures all transactions including manual adjustments.\n\n• Sales/Purchase Invoices — sums VAT from individual invoices. More granular but may miss manual journal entries or corrections.\n\nRecommended: GL Account Balance.",
"fieldname": "vat_data_source",
"fieldtype": "Select",
"label": "VAT Data Source",
"options": "GL Account Balance\nSales/Purchase Invoices"
},
{
"fieldname": "profit_tax_section",
"fieldtype": "Section Break",
"label": "Profit Tax",
"depends_on": "eval:doc.default_tax_regime === 'General (VAT + Profit)'"
},
{
"default": "1",
"description": "Enable profit tax calculation. The wizard will calculate quarterly advance payments based on the method selected below.\n\nPer AZ Tax Code: annual declaration is due by March 31 of the following year, but advance payments are due quarterly (by the 15th of the month after the quarter ends).\n\nDisable only if you handle profit tax entries manually.",
"fieldname": "enable_profit_tax",
"fieldtype": "Check",
"label": "Enable Profit Tax Calculation"
},
{
"default": "20",
"depends_on": "enable_profit_tax",
"description": "Corporate profit tax rate. Standard rate in Azerbaijan is 20% (Maddə 104 of the Tax Code).\n\nApplied to: Net Profit = Total Income Total Expenses for the period.\n\nChange only if your company qualifies for a reduced rate (e.g., certain agricultural or IT activities may have incentives).",
"fieldname": "profit_tax_rate",
"fieldtype": "Percent",
"label": "Profit Tax Rate (%)"
},
{
"fieldname": "column_break_pt1",
"fieldtype": "Column Break"
},
{
"default": "Actual Quarterly Profit",
"depends_on": "enable_profit_tax",
"description": "How quarterly advance payments are calculated:\n\n• Actual Quarterly Profit — calculates 20% of the actual profit for the current quarter (Income Expenses from P&L). More accurate, reflects current performance. Recommended for growing or seasonal businesses.\n\n• 1/4 of Previous Year Tax — takes the total profit tax from the previous year and divides by 4. Simpler, more predictable payments. Better for stable businesses. Requires 'Previous Year Tax Amount' to be filled in.\n\nBoth methods are accepted by the AZ tax authorities. The annual declaration reconciles any over/underpayment.",
"fieldname": "profit_tax_advance_method",
"fieldtype": "Select",
"label": "Advance Payment Method",
"options": "Actual Quarterly Profit\n1/4 of Previous Year Tax"
},
{
"fieldname": "column_break_pt2",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:doc.enable_profit_tax && doc.profit_tax_advance_method === '1/4 of Previous Year Tax'",
"description": "Enter the total profit tax amount from the previous fiscal year. The wizard will divide this by 4 for each quarterly advance payment.\n\nYou can find this in last year's Income Tax Return (field: total calculated tax) or from the previous year's Tax Period Closing Wizard summary.\n\nLeave at 0 if using 'Actual Quarterly Profit' method.",
"fieldname": "profit_tax_previous_year_amount",
"fieldtype": "Currency",
"label": "Previous Year Tax Amount",
"precision": "2"
},
{
"fieldname": "simplified_tax_section",
"fieldtype": "Section Break",
"label": "Simplified Tax",
"depends_on": "eval:doc.default_tax_regime === 'Simplified Tax' || doc.default_tax_regime === 'Special Regime'"
},
{
"default": "1",
"description": "Enable simplified tax calculation. Applied quarterly to total revenue (not profit).\n\nPer AZ Tax Code (Maddə 218-227): simplified tax replaces VAT and profit tax for qualifying businesses. Declaration and payment are due by the 20th of the month after the quarter.\n\nDisable only if you handle simplified tax entries manually.",
"fieldname": "enable_simplified_tax",
"fieldtype": "Check",
"label": "Enable Simplified Tax"
},
{
"default": "1",
"depends_on": "enable_simplified_tax",
"description": "If ON: the wizard determines the simplified tax rate automatically based on the company's OKVED (activity code):\n\n• Trade activities in Baku → 2%\n• Catering (public food) → 8%\n• All other activities → 4%\n\nIf OFF: the 'Default Rate' below is used for all calculations regardless of activity type.",
"fieldname": "simplified_tax_auto_rate",
"fieldtype": "Check",
"label": "Auto-detect Rate by OKVED"
},
{
"fieldname": "column_break_st1",
"fieldtype": "Column Break"
},
{
"default": "4",
"depends_on": "eval:doc.enable_simplified_tax && !doc.simplified_tax_auto_rate",
"description": "The flat simplified tax rate applied to total revenue.\n\nStandard rates per AZ Tax Code (Maddə 220.1):\n• 2% — Retail/wholesale trade in Baku\n• 4% — Services, production, and other activities\n• 8% — Catering/public food services\n\nThis field is only used when 'Auto-detect by OKVED' is OFF.",
"fieldname": "default_simplified_rate",
"fieldtype": "Percent",
"label": "Default Simplified Tax Rate (%)"
},
{
"fieldname": "column_break_st2",
"fieldtype": "Column Break"
},
{
"default": "Total Income (P&L)",
"depends_on": "enable_simplified_tax",
"description": "What counts as 'revenue' for simplified tax calculation:\n\n• Total Income (P&L) — sums all Income-type accounts from the P&L for the quarter. Includes sales revenue, other operating income, etc. Most common.\n\n• Sales Revenue Only — only account 601 (Satış gəlirləri). Excludes other income like interest, rent, etc. Use if your other income is not subject to simplified tax.\n\n• Cash Receipts — revenue recognized when cash is received (cash-basis). For companies using cash-basis accounting.",
"fieldname": "simplified_tax_revenue_source",
"fieldtype": "Select",
"label": "Revenue Source",
"options": "Total Income (P&L)\nSales Revenue Only\nCash Receipts"
},
{
"fieldname": "property_tax_section",
"fieldtype": "Section Break",
"label": "Property Tax"
},
{
"default": "1",
"description": "Enable property tax calculation on fixed assets. Calculated annually but paid in quarterly advances (1/4 each quarter).\n\nPer AZ Tax Code (Maddə 196-202): tax is levied on the average annual net book value of fixed assets (buildings, equipment, vehicles, etc.).\n\nDisable if the company has no fixed assets or all assets are tax-exempt under Maddə 199.4.",
"fieldname": "enable_property_tax",
"fieldtype": "Check",
"label": "Enable Property Tax"
},
{
"default": "1",
"depends_on": "enable_property_tax",
"description": "Property tax rate on the average annual residual value of fixed assets.\n\nStandard rate: 1% (Maddə 199 of the Tax Code).\n\nApplied to: (Value at start of year + Value at end of year) / 2.\n\nThis rate applies uniformly to all taxable asset types (buildings, equipment, vehicles, etc.).",
"fieldname": "property_tax_rate",
"fieldtype": "Percent",
"label": "Property Tax Rate (%)"
},
{
"fieldname": "column_break_prop1",
"fieldtype": "Column Break"
},
{
"default": "1000000",
"depends_on": "enable_property_tax",
"description": "Net book value threshold for property tax reporting. Per Maddə 199 of the AZ Tax Code, enterprises with average annual asset value exceeding this threshold may have additional reporting requirements.\n\nStandard value: 1,000,000 AZN.\n\nThe wizard will show a warning in the log if your company's net book value exceeds this threshold.",
"fieldname": "property_tax_threshold",
"fieldtype": "Currency",
"label": "Reporting Threshold (AZN)",
"precision": "2"
},
{
"fieldname": "column_break_prop2",
"fieldtype": "Column Break"
},
{
"default": "Average Annual (Start + End) / 2",
"depends_on": "enable_property_tax",
"description": "How the taxable asset value is calculated:\n\n• Average Annual (Start + End) / 2 — standard method per Maddə 199. Takes the residual value at the start of the year and the end of the year, averages them. Used for companies operating the full year (Tam İl).\n\n• Pro-rata for New Companies — for companies established during the year (İl Ərzində). Formula: (Purchase value + Year-end value) / 24 × months of operation.\n\n• End of Period NBV — uses the net book value at the end of the closing period. Simpler but less accurate for annual tax.",
"fieldname": "property_tax_calc_method",
"fieldtype": "Select",
"label": "Calculation Method",
"options": "Average Annual (Start + End) / 2\nPro-rata for New Companies\nEnd of Period NBV"
},
{
"fieldname": "land_tax_section",
"fieldtype": "Section Break",
"collapsible": 1,
"label": "Land Tax"
},
{
"default": "0",
"description": "Enable land tax calculation. Uses existing calculation logic from Land Tax Declaration in taxes_az (cadastral scores, industrial land rates).\n\nPer AZ Tax Code (Maddə 206-212): land tax is calculated based on cadastral value and land purpose. Annual declaration, quarterly payments.\n\nEnable only if the company owns or uses land plots. The wizard will use rates from cadastral_points_data.json and industrial_land_data.json.",
"fieldname": "enable_land_tax",
"fieldtype": "Check",
"label": "Enable Land Tax"
},
{
"fieldname": "column_break_lt1",
"fieldtype": "Column Break"
},
{
"default": "Land Tax Declaration",
"depends_on": "enable_land_tax",
"description": "Where land tax amounts come from:\n\n• Land Tax Declaration — reads the calculated amount from an existing Land Tax Declaration document for the fiscal year. You must create and calculate the Land Tax Declaration first.\n\n• Manual Entry — you manually enter the annual land tax amount in the wizard. The wizard will divide by 4 for quarterly advances.\n\nRecommended: Land Tax Declaration (uses existing cadastral calculation logic).",
"fieldname": "land_tax_source",
"fieldtype": "Select",
"label": "Data Source",
"options": "Land Tax Declaration\nManual Entry"
},
{
"fieldname": "other_taxes_section",
"fieldtype": "Section Break",
"collapsible": 1,
"label": "Other Taxes (Optional)"
},
{
"default": "0",
"description": "Enable excise tax tracking. Relevant only for companies producing or importing excisable goods (alcohol, tobacco, fuel, vehicles).\n\nThe wizard will check if the company's OKVED code matches excise-related activities. If enabled but no excisable activities are found, it will be skipped with a note in the log.\n\nMonthly calculation and declaration.",
"fieldname": "enable_excise_tax",
"fieldtype": "Check",
"label": "Enable Excise Tax"
},
{
"default": "0",
"description": "Enable mining tax tracking. Relevant only for companies extracting mineral resources (oil, gas, metals, stone, etc.).\n\nThe wizard will check if the company's OKVED code matches mining activities and if mining-related Item Groups exist. Monthly calculation.\n\nEnable only if the company has mining operations.",
"fieldname": "enable_mining_tax",
"fieldtype": "Check",
"label": "Enable Mining Tax"
},
{
"fieldname": "column_break_ot1",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Enable road tax tracking. Relevant for companies owning vehicles registered in Azerbaijan.\n\nThe wizard will check if the company has transportation assets. Annual calculation.\n\nEnable only if the company owns registered vehicles.",
"fieldname": "enable_road_tax",
"fieldtype": "Check",
"label": "Enable Road Tax"
},
{
"fieldname": "payroll_section",
"fieldtype": "Section Break",
"label": "Payroll Data Collection"
},
{
"default": "1",
"description": "If ON: the wizard collects payroll data (gross pay, NDFL, DSMF, unemployment insurance, medical insurance) from Salary Slips for the period. This data is shown in the summary report for reference.\n\nNOTE: The wizard does NOT create payroll-related journal entries — those are already created by the Payroll Entry process. The wizard only COLLECTS and DISPLAYS the totals.\n\nIf OFF: payroll data is not included in the wizard summary.",
"fieldname": "enable_payroll_collection",
"fieldtype": "Check",
"label": "Collect Payroll Data"
},
{
"fieldname": "column_break_pay1",
"fieldtype": "Column Break"
},
{
"default": "Salary Slips",
"depends_on": "enable_payroll_collection",
"description": "Where payroll totals are collected from:\n\n• Salary Slips — reads submitted Salary Slip documents for the period. Provides detailed breakdown by salary component (deductions, contributions).\n\n• GL Account Balances — reads balances from payroll-related accounts (522, 522.1-4, 533). Faster but less detailed.\n\n• Both (with cross-check) — reads from both sources and flags discrepancies. Most thorough but slowest.\n\nRecommended: Salary Slips.",
"fieldname": "payroll_data_source",
"fieldtype": "Select",
"label": "Payroll Data Source",
"options": "Salary Slips\nGL Account Balances\nBoth (with cross-check)"
},
{
"fieldname": "tax_rates_section",
"fieldtype": "Section Break",
"label": "Tax Rates Reference Table",
"description": "Reference table of all tax rates used by the wizard. Click 'Load Default Rates' to populate with standard AZ rates. You can modify rates here — changes affect all future wizard runs."
},
{
"description": "Each row defines a tax type and its rate. These rates are used as reference for calculations and for taxes that use fixed rates (property, DSMF, unemployment insurance). VAT and profit tax have their own rate fields above for convenience.\n\nClick 'Load Default AZ Rates' button to populate with standard rates from the AZ Tax Code.",
"fieldname": "tax_rates",
"fieldtype": "Table",
"label": "Tax Rates",
"options": "Tax Rate Row"
},
{
"fieldname": "account_mapping_section",
"fieldtype": "Section Break",
"label": "Account Mapping (Debit → Credit)",
"description": "Maps each tax type to the accounts used for journal entries. Debit = Expense account, Credit = Liability account. Click 'Auto-detect Accounts' to find matching accounts from your Chart of Accounts."
},
{
"description": "Each row maps a tax type to a pair of accounts:\n• Debit (Expense) — where the tax cost is recorded (e.g., 901 for profit tax expense)\n• Credit (Liability) — where the tax payable is recorded (e.g., 521.1 for profit tax liability)\n\nIf the company uses the standard AZ Chart of Accounts, click 'Auto-detect Accounts' to find the correct accounts automatically.",
"fieldname": "account_map",
"fieldtype": "Table",
"label": "Account Mapping",
"options": "Tax Closing Account Map"
}
],
"index_web_pages_for_search": 0,
"links": [],
"modified": "2026-04-01 14:00:00.000000",
"modified_by": "Administrator",
"module": "Taxes Az",
"name": "Tax Period Closing Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,375 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import flt
class TaxPeriodClosingSettings(Document):
def validate(self):
self.validate_contradictions()
if self.auto_detect_on_save and self.company:
self.run_auto_detect()
def validate_contradictions(self):
"""Check for logical contradictions between settings."""
# 1. Regime vs tax enables
if self.default_tax_regime == "General (VAT + Profit)":
if self.enable_simplified_tax:
frappe.msgprint(
_("You have selected 'General (VAT + Profit)' regime but also enabled "
"Simplified Tax. In the General regime, Simplified Tax is not applicable. "
"The wizard will ignore Simplified Tax. Consider disabling it."),
indicator="orange", title=_("Setting Conflict"))
if not self.enable_vat and not self.auto_detect_regime:
frappe.msgprint(
_("General regime typically requires VAT. VAT is disabled. "
"The wizard will not calculate VAT entries. Enable it if this company is a VAT payer."),
indicator="orange", title=_("Check Required"))
if not self.enable_profit_tax and not self.auto_detect_regime:
frappe.msgprint(
_("General regime typically requires Profit Tax. It is disabled. "
"The wizard will not calculate profit tax entries."),
indicator="orange", title=_("Check Required"))
elif self.default_tax_regime in ("Simplified Tax", "Special Regime"):
if self.enable_vat:
frappe.msgprint(
_("Simplified/Special regime does not use VAT. VAT is enabled. "
"Simplified tax payers are not VAT payers per AZ Tax Code. "
"The wizard will skip VAT calculation. Consider disabling it."),
indicator="orange", title=_("Setting Conflict"))
if self.enable_profit_tax:
frappe.msgprint(
_("Simplified/Special regime does not use Profit Tax. It is enabled. "
"Simplified tax replaces profit tax per AZ Tax Code. "
"The wizard will skip profit tax. Consider disabling it."),
indicator="orange", title=_("Setting Conflict"))
# 2. Profit tax advance method consistency
if (self.enable_profit_tax
and self.profit_tax_advance_method == "1/4 of Previous Year Tax"
and flt(self.profit_tax_previous_year_amount) <= 0):
frappe.msgprint(
_("Profit tax advance method is set to '1/4 of Previous Year Tax' "
"but the previous year amount is 0. The wizard will calculate 0 tax. "
"Please enter the previous year's total profit tax amount."),
indicator="red", title=_("Missing Data"))
# 3. VAT accounts must be different
if self.enable_vat and self.vat_input_account and self.vat_output_account:
if self.vat_input_account == self.vat_output_account:
frappe.throw(
_("VAT Input Account and VAT Output Account must be different. "
"Input (226) is an Asset account for VAT paid on purchases. "
"Output (521.3) is a Liability account for VAT collected from sales."))
# 4. Period closing requires closing account
if self.enable_period_closing and not self.closing_account:
frappe.msgprint(
_("Period Closing Voucher is enabled but no Closing Account is set. "
"You must specify an Equity or Liability account (e.g., 341) where "
"net profit/loss will be transferred when P&L is closed."),
indicator="red", title=_("Missing Account"))
# 5. Auto-submit + period closing = dangerous
if self.auto_submit_entries and self.enable_period_closing:
frappe.msgprint(
_("Both 'Auto-submit' and 'Period Closing Voucher' are enabled. "
"This means journal entries AND the Period Closing Voucher will be "
"submitted immediately without manual review. This is irreversible. "
"Consider disabling auto-submit for safety."),
indicator="red", title=_("High Risk Configuration"))
# 6. Re-closing disabled but shown
if not self.allow_reclosing and self.duplicate_check_scope:
pass # This is fine, duplicate_check_scope only applies when reclosing is OFF
# 7. Declaration draft without regime match
if self.create_declaration_draft and self.auto_fill_declaration:
if not self.company:
frappe.msgprint(
_("Auto-fill declarations requires a Company to be set. "
"VOEN, activity code, and tax authority are read from the Company document."),
indicator="orange", title=_("Company Required"))
# 8. Property tax rate sanity
if flt(self.property_tax_rate) > 5:
frappe.msgprint(
_("Property tax rate is set to {}%. The standard AZ rate is 1%. "
"Are you sure this is correct?").format(self.property_tax_rate),
indicator="orange", title=_("Unusual Rate"))
# 9. VAT rate sanity
if self.enable_vat and flt(self.vat_rate) != 18 and flt(self.vat_rate) > 0:
frappe.msgprint(
_("VAT rate is set to {}%. The standard AZ rate is 18%. "
"This is unusual — change only if a new rate has been enacted.").format(
self.vat_rate),
indicator="orange", title=_("Non-standard Rate"))
# 10. Profit tax rate sanity
if self.enable_profit_tax and flt(self.profit_tax_rate) != 20 and flt(self.profit_tax_rate) > 0:
frappe.msgprint(
_("Profit tax rate is set to {}%. The standard AZ rate is 20%. "
"Change only if your company qualifies for a special rate.").format(
self.profit_tax_rate),
indicator="orange", title=_("Non-standard Rate"))
# 11. Account mapping completeness
self._validate_account_map()
def _validate_account_map(self):
"""Check that enabled taxes have corresponding account mappings."""
tax_map = {row.tax_type for row in self.account_map or []}
missing = []
if self.enable_vat and "VAT (ƏDV)" not in tax_map:
# VAT uses separate fields, so this is OK
if not self.vat_input_account or not self.vat_output_account:
missing.append("VAT (ƏDV) — set VAT Input/Output accounts above")
if self.enable_profit_tax and "Profit Tax (Mənfəət)" not in tax_map:
missing.append("Profit Tax (Mənfəət)")
if self.enable_simplified_tax and "Simplified Tax (Sadələşdirilmiş)" not in tax_map:
missing.append("Simplified Tax (Sadələşdirilmiş)")
if self.enable_property_tax and "Property Tax (Əmlak)" not in tax_map:
missing.append("Property Tax (Əmlak)")
if self.enable_land_tax and "Land Tax (Torpaq)" not in tax_map:
missing.append("Land Tax (Torpaq)")
if missing:
frappe.msgprint(
_("The following enabled taxes have no account mapping. "
"The wizard won't know which accounts to debit/credit:<br><br>"
"{}").format("<br>".join(f"{m}" for m in missing)),
indicator="orange", title=_("Missing Account Mapping"))
def run_auto_detect(self):
"""Auto-detect settings from company data."""
company = frappe.get_doc("Company", self.company)
detected = []
# Cost center
if not self.default_cost_center and company.cost_center:
self.default_cost_center = company.cost_center
detected.append(f"Cost Center → {company.cost_center}")
# VAT payer detection
has_vat_cert = (hasattr(company, "vat_certificate_number")
and company.vat_certificate_number)
# Regime detection
if self.auto_detect_regime:
if has_vat_cert:
self.default_tax_regime = "General (VAT + Profit)"
self.enable_vat = 1
self.enable_profit_tax = 1
self.enable_simplified_tax = 0
detected.append("Regime → General (VAT payer detected)")
else:
self.default_tax_regime = "Simplified Tax"
self.enable_vat = 0
self.enable_profit_tax = 0
self.enable_simplified_tax = 1
detected.append("Regime → Simplified (no VAT certificate)")
# Asset detection
asset_count = frappe.db.count("Asset", {
"company": self.company,
"docstatus": 1,
"status": ["not in", ["Scrapped", "Sold"]],
})
if asset_count > 0:
self.enable_property_tax = 1
detected.append(f"Property Tax → Enabled ({asset_count} assets found)")
else:
self.enable_property_tax = 0
detected.append("Property Tax → Disabled (no active assets)")
# Employee detection
emp_count = frappe.db.count("Employee", {
"company": self.company,
"status": "Active",
})
if emp_count > 0:
self.enable_payroll_collection = 1
detected.append(f"Payroll Collection → Enabled ({emp_count} active employees)")
else:
self.enable_payroll_collection = 0
detected.append("Payroll Collection → Disabled (no active employees)")
# VAT accounts auto-detect
if self.enable_vat and (not self.vat_input_account or not self.vat_output_account):
self._detect_vat_accounts(detected)
# Account mapping auto-detect
if not self.account_map or len(self.account_map) == 0:
self._detect_account_mapping(detected)
if detected:
frappe.msgprint(
_("Auto-detection results:<br><br>{}").format(
"<br>".join(f"{d}" for d in detected)),
indicator="green", title=_("Parameters Detected"))
def _detect_vat_accounts(self, detected):
"""Try to find VAT accounts from Chart of Accounts."""
# Look for account 226 (VAT input)
vat_input = frappe.db.get_value("Account", {
"company": self.company,
"account_number": "226",
"is_group": 0,
}, "name")
if vat_input:
self.vat_input_account = vat_input
detected.append(f"VAT Input Account → {vat_input}")
# Look for account 521.3 (VAT output)
vat_output = frappe.db.get_value("Account", {
"company": self.company,
"account_number": "521.3",
"is_group": 0,
}, "name")
if vat_output:
self.vat_output_account = vat_output
detected.append(f"VAT Output Account → {vat_output}")
def _detect_account_mapping(self, detected):
"""Try to auto-detect account mappings from Chart of Accounts."""
mapping_rules = [
{
"tax_type": "Profit Tax (Mənfəət)",
"debit_numbers": ["901"],
"credit_numbers": ["521.1"],
},
{
"tax_type": "Simplified Tax (Sadələşdirilmiş)",
"debit_numbers": ["731"],
"credit_numbers": ["521.9"],
},
{
"tax_type": "Property Tax (Əmlak)",
"debit_numbers": ["731"],
"credit_numbers": ["521.5"],
},
{
"tax_type": "Land Tax (Torpaq)",
"debit_numbers": ["731"],
"credit_numbers": ["521.6"],
},
{
"tax_type": "Excise Tax (Aksiz)",
"debit_numbers": ["731"],
"credit_numbers": ["521.4"],
},
{
"tax_type": "Mining Tax (Mədən)",
"debit_numbers": ["731"],
"credit_numbers": ["521.8"],
},
{
"tax_type": "Road Tax (Yol)",
"debit_numbers": ["731"],
"credit_numbers": ["521.7"],
},
]
found_count = 0
for rule in mapping_rules:
debit_acc = None
credit_acc = None
for num in rule["debit_numbers"]:
debit_acc = frappe.db.get_value("Account", {
"company": self.company,
"account_number": num,
"is_group": 0,
}, "name")
if debit_acc:
break
for num in rule["credit_numbers"]:
credit_acc = frappe.db.get_value("Account", {
"company": self.company,
"account_number": num,
"is_group": 0,
}, "name")
if credit_acc:
break
if debit_acc and credit_acc:
self.append("account_map", {
"tax_type": rule["tax_type"],
"debit_account": debit_acc,
"credit_account": credit_acc,
})
found_count += 1
if found_count:
detected.append(f"Account Mapping → {found_count} mappings auto-detected")
def on_update(self):
frappe.clear_cache(doctype="Tax Period Closing Settings")
def get_settings():
return frappe.get_single("Tax Period Closing Settings")
def get_tax_rate(tax_type):
settings = get_settings()
for row in settings.tax_rates or []:
if row.tax_type == tax_type:
return row.rate
return 0
def get_account_map(tax_type):
settings = get_settings()
for row in settings.account_map or []:
if row.tax_type == tax_type:
return {"debit": row.debit_account, "credit": row.credit_account}
return None
@frappe.whitelist()
def auto_detect_accounts(company):
"""API endpoint to detect accounts for a specific company."""
if not company:
return {"accounts": []}
results = {}
account_search = {
"226": "VAT Input (ƏDV sub-uçot)",
"521.3": "VAT Output (ƏDV)",
"521.1": "Profit Tax Liability",
"521.5": "Property Tax Liability",
"521.6": "Land Tax Liability",
"521.9": "Simplified Tax Liability",
"901": "Profit Tax Expense",
"731": "Other Operating Expenses",
"341": "Period Net Profit (Closing)",
}
for acc_num, label in account_search.items():
acc = frappe.db.get_value("Account", {
"company": company,
"account_number": acc_num,
"is_group": 0,
}, ["name", "account_name"], as_dict=True)
if acc:
results[acc_num] = {
"name": acc.name,
"account_name": acc.account_name,
"label": label,
}
return {"accounts": results}

View File

@ -0,0 +1,806 @@
frappe.ui.form.on("Tax Period Closing Wizard", {
onload(frm) {
// Expand form to full width
frm.$wrapper.find(".form-column, .section-body, .form-section").css("max-width", "100%");
$(`<style>
[data-doctype="Tax Period Closing Wizard"] .form-section .section-body {
max-width: 100% !important;
}
[data-doctype="Tax Period Closing Wizard"] .form-column {
max-width: 100% !important;
}
[data-doctype="Tax Period Closing Wizard"] .row.form-section {
max-width: 100% !important;
}
[data-doctype="Tax Period Closing Wizard"] .frappe-control[data-fieldtype="Table"] {
max-width: 100% !important;
}
[data-doctype="Tax Period Closing Wizard"] .form-layout {
max-width: 100% !important;
}
[data-doctype="Tax Period Closing Wizard"] .layout-main .layout-main-section {
max-width: 100% !important;
}
[data-doctype="Tax Period Closing Wizard"] .form-page {
max-width: 100% !important;
}
</style>`).appendTo("head");
},
refresh(frm) {
frm.trigger("render_progress_bar");
frm.trigger("render_benefits_html");
frm.trigger("render_log_html");
frm.trigger("render_summary_html");
frm.trigger("setup_buttons");
frm.trigger("color_tax_items");
frm.trigger("bind_realtime");
},
setup_buttons(frm) {
if (frm.doc.docstatus === 1 || frm.doc.docstatus === 2) return;
if (frm.doc.wizard_status === "Draft" || frm.doc.wizard_status === "Error") {
frm.add_custom_button(__("Calculate Taxes"), () => {
frm.trigger("run_calculate");
}, __("Actions")).addClass("btn-primary");
}
if (frm.doc.wizard_status === "Calculated") {
frm.add_custom_button(__("Create Journal Entries"), () => {
frappe.confirm(
__("This will create journal entries for all calculated taxes. Continue?"),
() => frm.trigger("run_create_entries")
);
}, __("Actions")).addClass("btn-primary");
frm.add_custom_button(__("Re-calculate"), () => {
frm.trigger("run_calculate");
}, __("Actions"));
}
// Verify & Fix buttons — available after calculation
if (frm.doc.wizard_status !== "Draft") {
frm.add_custom_button(__("Verify Entries (Kassa)"), () => {
frm.trigger("run_verify");
}, __("Actions"));
frm.add_custom_button(__("Fix Cash Method Entries"), () => {
frappe.confirm(
__("This will create correction Journal Entries to move VAT from 521.3 to 245.1 and revenue from 601 to 542 for all unpaid Sales Invoices. Continue?"),
() => frm.trigger("run_fix_cash")
);
}, __("Actions"));
}
if (frm.doc.wizard_status === "Entries Created") {
frm.add_custom_button(__("Rollback All Entries"), () => {
frappe.confirm(
__("This will cancel/delete ALL journal entries created by this wizard. Are you sure?"),
() => frm.trigger("run_rollback")
);
}, __("Actions")).addClass("btn-danger");
}
},
// ── Realtime binding ──────────────────────────────────
bind_realtime(frm) {
// Avoid duplicate bindings
if (frm._tpcw_realtime_bound) return;
frm._tpcw_realtime_bound = true;
frappe.realtime.on("tpcw_progress", (data) => {
if (data.name !== frm.doc.name) return;
frm.trigger("update_live_progress", data);
});
},
update_live_progress(frm, data) {
if (!data) return;
const pct = data.percent || 0;
const phase = data.phase || "";
const message = data.message || "";
const level = data.level || "Info";
// Update the progress bar
const $wrapper = frm.fields_dict.progress_bar_html.$wrapper;
const level_colors = {
"Info": { bar: "linear-gradient(90deg, #007bff, #6610f2)", text: "#007bff", pulse: "#007bff" },
"Success": { bar: "linear-gradient(90deg, #28a745, #20c997)", text: "#28a745", pulse: "#28a745" },
"Warning": { bar: "linear-gradient(90deg, #fd7e14, #ffc107)", text: "#e67e22", pulse: "#fd7e14" },
"Error": { bar: "linear-gradient(90deg, #dc3545, #c82333)", text: "#dc3545", pulse: "#dc3545" },
};
const colors = level_colors[level] || level_colors["Info"];
const is_complete = pct >= 100 || phase === "Complete";
const pulse_css = is_complete ? "" : "animation: tpcw-pulse 1.5s ease-in-out infinite;";
$wrapper.html(`
<style>
@keyframes tpcw-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
@keyframes tpcw-bar-stripe {
0% { background-position: 1rem 0; }
100% { background-position: 0 0; }
}
</style>
<div style="margin-bottom: 15px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
<div>
<span style="font-weight: bold; color: ${colors.text}; font-size: 13px; ${pulse_css}">
${is_complete ? "✓" : "⟳"} ${phase}
</span>
<span style="color: #666; font-size: 12px; margin-left: 8px;">
${frappe.utils.escape_html(message)}
</span>
</div>
<span style="font-weight: bold; font-size: 14px; color: ${colors.text};">${pct}%</span>
</div>
<div style="background: #e9ecef; border-radius: 8px; overflow: hidden; height: 26px; position: relative;">
<div style="
width: ${pct}%;
height: 100%;
background: ${colors.bar};
${!is_complete ? `background-image: linear-gradient(45deg, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent);
background-size: 1rem 1rem;
animation: tpcw-bar-stripe 0.8s linear infinite;` : ""}
border-radius: 8px;
transition: width 0.4s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 12px;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
">${pct > 8 ? pct + "%" : ""}</div>
</div>
</div>
`);
// If complete, show a success banner
if (is_complete && level === "Success") {
frappe.show_alert({
message: message,
indicator: "green"
}, 7);
}
},
// ── Run actions ───────────────────────────────────────
run_calculate(frm) {
frm.save().then(() => {
frm.trigger("update_live_progress", {
percent: 2,
phase: "Starting",
message: "Initializing wizard...",
level: "Info"
});
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_period_closing_wizard.tax_period_closing_wizard.run_wizard",
args: { name: frm.doc.name },
freeze: true,
freeze_message: __("Calculating taxes..."),
callback(r) {
if (r && r.message) {
frappe.show_alert({
message: __("Calculation complete. Reloading..."),
indicator: "green"
});
}
// Force full page reload to get all child table data
window.location.reload();
},
error(r) {
frappe.show_alert({
message: __("Calculation failed. Check browser console."),
indicator: "red"
});
window.location.reload();
}
});
});
},
run_create_entries(frm) {
frm.trigger("update_live_progress", {
percent: 81,
phase: "Entry Creation",
message: "Preparing journal entries...",
level: "Info"
});
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_period_closing_wizard.tax_period_closing_wizard.create_entries",
args: { name: frm.doc.name },
freeze: true,
freeze_message: __("Creating journal entries..."),
callback(r) {
if (r && r.message) {
frappe.show_alert({
message: __("Entries created. Reloading..."),
indicator: "green"
});
}
window.location.reload();
},
error() {
frappe.show_alert({
message: __("Entry creation failed."),
indicator: "red"
});
window.location.reload();
}
});
},
run_fix_cash(frm) {
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_period_closing_wizard.tax_period_closing_wizard.fix_cash_method_entries",
args: { name: frm.doc.name },
freeze: true,
freeze_message: __("Re-submitting invoices with cash method accounts..."),
callback(r) {
if (!r || !r.message) return;
const data = r.message;
if (data.status === "skip" || data.status === "error") {
frappe.msgprint({ message: data.message, indicator: "red" });
return;
}
const s = data.summary || {};
const fmt = (v) => parseFloat(v || 0).toLocaleString("az-AZ", {minimumFractionDigits: 2});
let html = `<div style="font-family: -apple-system, sans-serif;">`;
html += `<div style="padding: 12px; background: #2c3e50; color: white; border-radius: 6px; margin-bottom: 12px;">
<strong>${__("Cash method — invoices re-submitted")}</strong>
</div>`;
html += `<table class="table table-bordered" style="font-size: 13px;">
<tr style="background: #d4edda;">
<td><strong>${__("Fixed invoices")}</strong></td>
<td>${s.invoices_fixed}</td>
<td>ƏDV: ${fmt(s.total_vat_moved)} AZN 245.1</td>
<td>Gəlir: ${fmt(s.total_revenue_deferred)} AZN 542</td>
</tr>
<tr><td>${__("Skipped")}</td><td colspan="3">${s.skipped}</td></tr>
<tr style="${s.errors > 0 ? 'background:#f8d7da;' : ''}">
<td>${__("Errors")}</td><td colspan="3">${s.errors}</td></tr>
</table>`;
if ((data.fixed || []).length > 0) {
html += `<table class="table table-sm table-bordered" style="font-size: 12px;">
<thead style="background: #343a40; color: white;">
<tr><th>${__("Invoice")}</th><th style="text-align:right;">ƏDV 245.1</th><th style="text-align:right;">Gəlir 542</th><th>${__("Changes")}</th></tr>
</thead><tbody>`;
for (const f of data.fixed) {
html += `<tr>
<td><a href="/app/sales-invoice/${f.invoice}">${f.invoice}</a></td>
<td style="text-align:right;">${fmt(f.vat)}</td>
<td style="text-align:right;">${fmt(f.revenue)}</td>
<td style="font-size:11px;color:#666;">${(f.changes || []).join("<br>")}</td>
</tr>`;
}
html += `</tbody></table>`;
}
if ((data.errors || []).length > 0) {
for (const e of data.errors) {
html += `<div style="padding:6px;background:#f8d7da;border-radius:3px;margin-bottom:4px;">
${e.invoice}: ${frappe.utils.escape_html(e.error)}</div>`;
}
}
html += `</div>`;
frappe.msgprint({
title: __("Cash Method — Fix Result"),
message: html,
indicator: (s.errors || 0) > 0 ? "orange" : "green",
wide: true,
});
}
});
},
run_verify(frm) {
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_period_closing_wizard.tax_period_closing_wizard.verify_entries",
args: { name: frm.doc.name },
freeze: true,
freeze_message: __("Verifying entries..."),
callback(r) {
if (!r || !r.message) return;
const data = r.message;
const s = data.summary || {};
let html = `<div style="font-family: -apple-system, sans-serif; max-height: 500px; overflow-y: auto;">`;
// Header
html += `<div style="padding: 10px; background: #2c3e50; color: white; border-radius: 6px 6px 0 0; margin-bottom: 0;">
<strong>${__("Accounting method:")} ${data.method}</strong> |
${__("Invoices:")} ${s.total_invoices || 0} |
<span style="color: #2ecc71;">${__("Correct:")} ${s.correct || 0}</span> |
<span style="color: #e74c3c;">${__("Error:")} ${s.errors || 0}</span> |
<span style="color: #f39c12;">${__("Warning:")} ${s.warnings || 0}</span>
</div>`;
// Issues
if ((data.issues || []).length > 0) {
html += `<div style="border: 1px solid #e74c3c; border-top: none; border-radius: 0 0 6px 6px; padding: 10px;">`;
for (const issue of data.issues) {
const color = issue.type === "error" ? "#e74c3c" : "#f39c12";
const icon = issue.type === "error" ? "✗" : "⚠";
html += `<div style="padding: 8px; margin-bottom: 6px; border-left: 4px solid ${color}; background: ${issue.type === 'error' ? '#fdf0ef' : '#fffbeb'}; border-radius: 3px;">
<span style="color: ${color}; font-weight: bold;">${icon}</span>
<a href="/app/sales-invoice/${issue.invoice}"><strong>${issue.invoice}</strong></a>:
${frappe.utils.escape_html(issue.message)}
<div style="margin-top: 4px; font-size: 11px; color: #666;">
${__("Fix:")} ${frappe.utils.escape_html(issue.fix || "")}
</div>
</div>`;
}
if (s.total_wrong_vat > 0) {
html += `<div style="padding: 10px; background: #f8d7da; border-radius: 4px; font-weight: bold; color: #721c24;">
${__("Total incorrect VAT:")} ${parseFloat(s.total_wrong_vat).toLocaleString("az-AZ", {minimumFractionDigits: 2})} AZN
</div>`;
}
html += `</div>`;
}
// OK entries
if ((data.ok || []).length > 0) {
html += `<details style="margin-top: 10px;"><summary style="cursor: pointer; color: #27ae60; font-weight: bold;">
${__("Correct entries")} (${data.ok.length})</summary>
<div style="padding: 8px; font-size: 12px; color: #555;">`;
for (const ok of data.ok) {
html += `<div style="padding: 2px 0;">✓ ${frappe.utils.escape_html(ok)}</div>`;
}
html += `</div></details>`;
}
html += `</div>`;
frappe.msgprint({
title: __("Cash Method — Verification Result"),
message: html,
indicator: (s.errors || 0) > 0 ? "red" : ((s.warnings || 0) > 0 ? "orange" : "green"),
wide: true,
});
}
});
},
run_rollback(frm) {
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_period_closing_wizard.tax_period_closing_wizard.rollback",
args: { name: frm.doc.name },
freeze: true,
freeze_message: __("Rolling back entries..."),
callback(r) {
if (r.message) {
frm.reload_doc();
frappe.show_alert({
message: __("All entries rolled back"),
indicator: "orange"
});
}
}
});
},
company(frm) {
if (frm.doc.company) {
frappe.db.get_value("Company", frm.doc.company, [
"cost_center", "tax_id", "default_currency"
]).then(r => {
if (r.message) {
if (!frm.doc.cost_center) {
frm.set_value("cost_center", r.message.cost_center);
}
frm.set_value("company_voen", r.message.tax_id || "");
}
});
}
},
period_type(frm) {
if (frm.doc.period_type === "Monthly") {
frm.set_value("quarter", "");
} else if (frm.doc.period_type === "Quarterly") {
frm.set_value("month", "");
} else {
frm.set_value("quarter", "");
frm.set_value("month", "");
}
},
// ── Static Progress Bar (for saved state) ─────────────
render_progress_bar(frm) {
const pct = frm.doc.progress_percent || 0;
const status = frm.doc.wizard_status || "Draft";
const status_config = {
"Draft": { color: "#6c757d", icon: "○", bar: "linear-gradient(90deg, #6c757d, #adb5bd)" },
"Calculated": { color: "#fd7e14", icon: "◉", bar: "linear-gradient(90deg, #fd7e14, #ffc107)" },
"Entries Created": { color: "#28a745", icon: "✓", bar: "linear-gradient(90deg, #28a745, #20c997)" },
"Completed": { color: "#28a745", icon: "✓", bar: "linear-gradient(90deg, #28a745, #20c997)" },
"Error": { color: "#dc3545", icon: "✗", bar: "linear-gradient(90deg, #dc3545, #c82333)" },
"Cancelled": { color: "#6c757d", icon: "—", bar: "linear-gradient(90deg, #6c757d, #adb5bd)" }
};
const cfg = status_config[status] || status_config["Draft"];
const html = `
<div style="margin-bottom: 15px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
<span style="font-weight: bold; color: ${cfg.color}; font-size: 13px;">
${cfg.icon} ${status}
</span>
<span style="font-weight: bold; font-size: 14px; color: ${cfg.color};">${pct}%</span>
</div>
<div style="background: #e9ecef; border-radius: 8px; overflow: hidden; height: 26px;">
<div style="
width: ${pct}%;
height: 100%;
background: ${cfg.bar};
border-radius: 8px;
transition: width 0.5s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 12px;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
">${pct > 8 ? pct + "%" : ""}</div>
</div>
</div>
`;
frm.fields_dict.progress_bar_html.$wrapper.html(html);
},
// ── Benefits Display ──────────────────────────────────
render_benefits_html(frm) {
const benefits = frm.doc.detected_benefits || [];
if (!benefits.length) {
if (frm.fields_dict.benefits_html) {
frm.fields_dict.benefits_html.$wrapper.html(
frm.doc.wizard_status === "Draft"
? `<div style="padding: 15px; text-align: center; color: #999;">${__("Benefits will be detected after calculation.")}</div>`
: `<div style="padding: 15px; text-align: center; color: #999;">${__("No special tax benefits detected.")}</div>`
);
}
return;
}
let html = '<div style="display: flex; flex-direction: column; gap: 10px;">';
for (const b of benefits) {
const is_on = b.enabled;
const cond = b.condition_met || "No";
// Colors
let border_color, bg_color, icon, badge_bg, badge_text;
if (cond === "Yes" && is_on) {
border_color = "#28a745"; bg_color = "#f0fdf4"; icon = "✓"; badge_bg = "#28a745"; badge_text = __("Active");
} else if (cond === "No") {
border_color = "#dc3545"; bg_color = "#fef2f2"; icon = "✗"; badge_bg = "#dc3545"; badge_text = __("Not Met");
} else if (cond === "Manual Check") {
border_color = "#fd7e14"; bg_color = "#fffbeb"; icon = "?"; badge_bg = "#fd7e14"; badge_text = __("Manual Check");
} else if (!is_on) {
border_color = "#6c757d"; bg_color = "#f8f9fa"; icon = "—"; badge_bg = "#6c757d"; badge_text = __("Disabled");
} else {
border_color = "#17a2b8"; bg_color = "#f0f9ff"; icon = "~"; badge_bg = "#17a2b8"; badge_text = __("Partial");
}
const reduction_html = b.tax_reduction
? `<div style="margin-top: 6px; font-size: 14px; font-weight: bold; color: #28a745;">
${__("Tax Reduction:")} ${format_currency(b.tax_reduction)}
</div>`
: "";
const law_text = b.law_url
? `<a href="${frappe.utils.escape_html(b.law_url)}" target="_blank" style="color:#4338ca;text-decoration:underline;">${frappe.utils.escape_html(b.law_reference)}</a>`
: frappe.utils.escape_html(b.law_reference);
const law_html = b.law_reference
? `<div style="margin-top: 8px; padding: 8px 10px; background: #eef2ff; border-left: 3px solid #6366f1; border-radius: 3px; font-size: 11px; color: #4338ca; line-height: 1.5;">
<span style="font-weight: bold;">${__("Law:")}</span> ${law_text}
</div>`
: "";
html += `
<div style="border-left: 4px solid ${border_color}; background: ${bg_color}; border-radius: 6px; padding: 12px 16px;">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<span style="font-size: 16px; font-weight: bold; color: ${border_color};">${icon}</span>
<span style="font-weight: bold; font-size: 13px; color: #333;">
${frappe.utils.escape_html(b.article)}
</span>
<span style="background: ${badge_bg}; color: white; padding: 2px 8px; border-radius: 10px; font-size: 11px;">
${badge_text}
</span>
</div>
<div style="font-size: 13px; color: #444; margin-bottom: 4px;">
<strong>${frappe.utils.escape_html(b.benefit_name)}</strong>
</div>
<div style="font-size: 12px; color: #666; margin-bottom: 2px;">
${__("Applies to:")} <strong>${frappe.utils.escape_html(b.applies_to)}</strong>
&nbsp;|&nbsp; ${__("Exemption:")} <strong>${b.exemption_percent}%</strong>
</div>
<div style="font-size: 12px; color: #555; margin-top: 6px; padding: 8px; background: rgba(0,0,0,0.03); border-radius: 4px;">
${frappe.utils.escape_html(b.condition_details || "")}
</div>
${reduction_html}
${law_html}
</div>
</div>
</div>
`;
}
html += "</div>";
if (frm.fields_dict.benefits_html) {
frm.fields_dict.benefits_html.$wrapper.html(html);
}
},
// ── Log Display ────────────────────────────────────────
render_log_html(frm) {
const logs = frm.doc.log_entries || [];
if (!logs.length) {
frm.fields_dict.log_html.$wrapper.html(
`<div style="padding: 20px; text-align: center; color: #999;">${__('No log entries yet. Click "Calculate Taxes" to begin.')}</div>`
);
return;
}
const level_styles = {
"Header": "background: #2c3e50; color: #ecf0f1; font-weight: bold; padding: 6px 10px; margin-top: 8px;",
"Info": "color: #2980b9; padding: 3px 10px;",
"Success": "color: #27ae60; padding: 3px 10px;",
"Warning": "color: #e67e22; padding: 3px 10px; font-weight: 500;",
"Error": "color: #e74c3c; padding: 3px 10px; font-weight: bold; background: #fdf0ef;"
};
const level_icons = {
"Header": "═══",
"Info": "",
"Success": "✓",
"Warning": "⚠",
"Error": "✗"
};
let html = '<div style="font-family: monospace; font-size: 12px; background: #fafbfc; border: 1px solid #e1e4e8; border-radius: 6px; max-height: 600px; overflow-y: auto; padding: 8px;">';
for (const log of logs) {
const style = level_styles[log.level] || level_styles["Info"];
const icon = level_icons[log.level] || "·";
const ts = log.timestamp ? frappe.datetime.str_to_user(log.timestamp).split(" ")[1] || "" : "";
html += `<div style="${style} border-radius: 3px; margin-bottom: 1px;">`;
if (log.level !== "Header") {
html += `<span style="color: #999; margin-right: 8px;">[${ts}]</span>`;
}
html += `<span style="margin-right: 6px;">${icon}</span>`;
html += `${frappe.utils.escape_html(log.message)}`;
html += `</div>`;
}
html += "</div>";
frm.fields_dict.log_html.$wrapper.html(html);
},
// ── Summary Display ───────────────────────────────────
render_summary_html(frm) {
if (frm.doc.wizard_status !== "Entries Created" && frm.doc.wizard_status !== "Completed") {
frm.fields_dict.summary_html.$wrapper.html("");
return;
}
const items = frm.doc.tax_items || [];
const je_list = frm.doc.journal_entries || [];
const payroll = frm.doc.payroll_summary || [];
let html = '<div style="font-family: -apple-system, BlinkMacSystemFont, sans-serif;">';
html += `
<table class="table table-bordered" style="font-size: 13px;">
<thead style="background: #2c3e50; color: white;">
<tr>
<th>${__("Tax Type")}</th>
<th>${__("Period")}</th>
<th style="text-align: right;">${__("Amount (AZN)")}</th>
<th style="text-align: center;">${__("Status")}</th>
<th>${__("Declaration")}</th>
<th>${__("Payment")}</th>
</tr>
</thead>
<tbody>
`;
for (const item of items) {
const status_badge = get_status_badge(item.status);
const amount_display = item.status === "Data Only" ? "—" : format_currency(item.tax_amount);
html += `
<tr>
<td>${frappe.utils.escape_html(item.tax_type)}</td>
<td>${frappe.utils.escape_html(item.sub_period || "")}</td>
<td style="text-align: right; font-weight: bold;">${amount_display}</td>
<td style="text-align: center;">${status_badge}</td>
<td>${item.declaration_deadline || "—"}</td>
<td>${item.payment_deadline || "—"}</td>
</tr>
`;
}
html += `
<tr style="background: #d4edda; font-weight: bold;">
<td colspan="2">${__("Total Tax Entries")}</td>
<td style="text-align: right;">${format_currency(frm.doc.total_tax_amount)}</td>
<td colspan="3"></td>
</tr>
`;
if (frm.doc.total_payroll_taxes) {
html += `
<tr style="background: #d1ecf1;">
<td colspan="2">${__("Payroll Taxes (collected data)")}</td>
<td style="text-align: right;">${format_currency(frm.doc.total_payroll_taxes)}</td>
<td colspan="3"></td>
</tr>
`;
}
html += `
<tr style="background: #f8d7da; font-weight: bold; font-size: 14px;">
<td colspan="2">${__("GRAND TOTAL TAX BURDEN")}</td>
<td style="text-align: right;">${format_currency(frm.doc.grand_total_tax_burden)}</td>
<td colspan="3"></td>
</tr>
</tbody></table>
`;
if (je_list.length) {
html += `
<h5 style="margin-top: 15px;">${__("Created Documents")}</h5>
<table class="table table-sm table-bordered" style="font-size: 12px;">
<thead style="background: #343a40; color: white;">
<tr><th>${__("Journal Entry")}</th><th>${__("Tax Type")}</th><th>${__("Period")}</th><th style="text-align: right;">${__("Amount")}</th><th>${__("Date")}</th></tr>
</thead><tbody>
`;
for (const je of je_list) {
html += `
<tr>
<td><a href="/app/journal-entry/${je.journal_entry}">${je.journal_entry}</a></td>
<td>${frappe.utils.escape_html(je.tax_type || "")}</td>
<td>${frappe.utils.escape_html(je.sub_period || "")}</td>
<td style="text-align: right;">${format_currency(je.amount)}</td>
<td>${je.posting_date || ""}</td>
</tr>
`;
}
html += "</tbody></table>";
}
if (payroll.length) {
html += `
<h5 style="margin-top: 15px;">${__("Payroll Data (Reference Only)")}</h5>
<table class="table table-sm table-bordered" style="font-size: 12px;">
<thead style="background: #17a2b8; color: white;">
<tr><th>${__("Component")}</th><th style="text-align: right;">${__("Amount")}</th><th>${__("Source")}</th></tr>
</thead><tbody>
`;
for (const p of payroll) {
html += `
<tr>
<td>${frappe.utils.escape_html(p.component || "")}</td>
<td style="text-align: right;">${format_currency(p.amount)}</td>
<td>${frappe.utils.escape_html(p.source || "")}</td>
</tr>
`;
}
html += "</tbody></table>";
}
html += '<div style="margin-top: 15px; padding: 12px; background: #fff3cd; border: 1px solid #ffc107; border-radius: 6px;">';
html += `<h5 style="color: #856404; margin-bottom: 8px;">${__("Upcoming Deadlines")}</h5>`;
for (const item of items) {
if (item.enabled && item.declaration_deadline) {
const decl_date = new Date(item.declaration_deadline);
const today = new Date();
const days_left = Math.ceil((decl_date - today) / (1000 * 60 * 60 * 24));
const urgency = days_left <= 7 ? "color: #dc3545; font-weight: bold;" :
days_left <= 30 ? "color: #e67e22;" : "color: #28a745;";
html += `<div style="margin-bottom: 4px; ${urgency}">`;
html += `${frappe.utils.escape_html(item.tax_type)}`;
html += `Declaration: ${item.declaration_deadline} | `;
html += `Payment: ${item.payment_deadline || "N/A"}`;
if (days_left <= 7) html += ` (${days_left} days left!)`;
html += `</div>`;
}
}
html += "</div></div>";
frm.fields_dict.summary_html.$wrapper.html(html);
},
// ── Color Tax Items ───────────────────────────────────
color_tax_items(frm) {
setTimeout(() => {
const status_bg = {
"Entry Created": "#d4edda",
"Calculated": "#d4edda",
"Error": "#f8d7da",
"Skipped": "#e2e3e5",
"Data Only": "#d1ecf1",
"Pending": "#fff3cd"
};
frm.$wrapper.find('.frappe-control[data-fieldname="tax_items"] .rows .row').each(function(i) {
const item = (frm.doc.tax_items || [])[i];
if (!item) return;
const bg = status_bg[item.status];
if (bg) $(this).css({ "background-color": bg, "transition": "background-color 0.3s" });
});
frm.$wrapper.find('.frappe-control[data-fieldname="entry_previews"] .rows .row').each(function(i) {
const item = (frm.doc.entry_previews || [])[i];
if (!item) return;
const bg = {
"Created": "#d4edda",
"Failed": "#f8d7da",
"Pending": "#fff3cd",
"Existing": "#d4edda",
"Difference": "#ffe0b2",
"Skipped": "#e2e3e5"
}[item.status];
if (bg) $(this).css("background-color", bg);
});
frm.$wrapper.find('.frappe-control[data-fieldname="journal_entries"] .rows .row').each(function() {
$(this).css("background-color", "#d4edda");
});
}, 300);
}
});
// ── Helper Functions ──────────────────────────────────────
function get_status_badge(status) {
const badges = {
"Pending": `<span class="badge" style="background: #ffc107; color: #333;">${__("Pending")}</span>`,
"Calculated": `<span class="badge" style="background: #17a2b8; color: white;">${__("Calculated")}</span>`,
"Entry Created": `<span class="badge" style="background: #28a745; color: white;">${__("Created")}</span>`,
"Existing": `<span class="badge" style="background: #28a745; color: white;">${__("Existing")}</span>`,
"Difference": `<span class="badge" style="background: #fd7e14; color: white;">${__("Difference")}</span>`,
"Data Only": `<span class="badge" style="background: #6c757d; color: white;">${__("Data Only")}</span>`,
"Skipped": `<span class="badge" style="background: #adb5bd; color: white;">${__("Skipped")}</span>`,
"Error": `<span class="badge" style="background: #dc3545; color: white;">${__("Error")}</span>`
};
return badges[status] || `<span class="badge badge-secondary">${status}</span>`;
}
function format_currency(val) {
const num = parseFloat(val) || 0;
return num.toLocaleString("az-AZ", {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}) + " AZN";
}

View File

@ -0,0 +1,582 @@
{
"actions": [],
"autoname": "TPCW-.YYYY.-.#####",
"creation": "2026-04-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"wizard_status_section",
"wizard_status",
"column_break_ws1",
"progress_percent",
"progress_bar_html",
"company_section",
"company",
"column_break_c1",
"tax_regime",
"column_break_c2",
"fiscal_year",
"period_section",
"period_type",
"column_break_p1",
"quarter",
"month",
"column_break_p2",
"from_date",
"to_date",
"options_section",
"cost_center",
"column_break_o1",
"auto_submit",
"column_break_o2",
"enable_period_closing_voucher",
"closing_account",
"tab_dashboard",
"company_info_section",
"company_voen",
"company_activity_code",
"column_break_ci1",
"company_classification",
"company_tax_authority",
"column_break_ci2",
"is_vat_payer",
"has_assets",
"has_land",
"has_employees",
"dashboard_financials_section",
"total_income",
"column_break_fd1",
"total_expense",
"column_break_fd2",
"net_profit",
"dashboard_assets_section",
"total_asset_value",
"accumulated_depreciation",
"column_break_ad1",
"net_book_value",
"avg_annual_value",
"benefits_section",
"benefits_html",
"detected_benefits",
"dashboard_totals_section",
"total_tax_amount",
"column_break_tt1",
"total_payroll_taxes",
"column_break_tt2",
"grand_total_tax_burden",
"tab_tax_details",
"taxes_section",
"tax_items",
"entry_preview_section",
"entry_previews",
"tab_results",
"journal_entries_section",
"journal_entries",
"payroll_section",
"payroll_summary",
"summary_section",
"summary_html",
"tab_log",
"log_section",
"log_html",
"log_entries",
"amended_from"
],
"fields": [
{
"fieldname": "wizard_status_section",
"fieldtype": "Section Break",
"label": "Status"
},
{
"default": "Draft",
"fieldname": "wizard_status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Wizard Status",
"options": "Draft\nCalculated\nEntries Created\nCompleted\nError\nCancelled",
"read_only": 1
},
{
"fieldname": "column_break_ws1",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "progress_percent",
"fieldtype": "Percent",
"label": "Progress",
"read_only": 1
},
{
"fieldname": "progress_bar_html",
"fieldtype": "HTML",
"label": "Progress Bar"
},
{
"fieldname": "company_section",
"fieldtype": "Section Break",
"label": "Company"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "column_break_c1",
"fieldtype": "Column Break"
},
{
"fieldname": "tax_regime",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Tax Regime",
"options": "\nGeneral (VAT + Profit)\nSimplified Tax\nSpecial Regime"
},
{
"fieldname": "column_break_c2",
"fieldtype": "Column Break"
},
{
"fieldname": "fiscal_year",
"fieldtype": "Link",
"label": "Fiscal Year",
"options": "Fiscal Year",
"reqd": 1
},
{
"fieldname": "period_section",
"fieldtype": "Section Break",
"label": "Period"
},
{
"default": "Quarterly",
"fieldname": "period_type",
"fieldtype": "Select",
"label": "Period Type",
"options": "Monthly\nQuarterly\nAnnually",
"reqd": 1
},
{
"fieldname": "column_break_p1",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.period_type=='Quarterly'",
"fieldname": "quarter",
"fieldtype": "Select",
"label": "Quarter",
"options": "\nQ1 (Jan-Mar)\nQ2 (Apr-Jun)\nQ3 (Jul-Sep)\nQ4 (Oct-Dec)"
},
{
"depends_on": "eval:doc.period_type=='Monthly'",
"fieldname": "month",
"fieldtype": "Select",
"label": "Month",
"options": "\nJanuary\nFebruary\nMarch\nApril\nMay\nJune\nJuly\nAugust\nSeptember\nOctober\nNovember\nDecember"
},
{
"fieldname": "column_break_p2",
"fieldtype": "Column Break"
},
{
"fieldname": "from_date",
"fieldtype": "Date",
"label": "From Date",
"read_only": 1
},
{
"fieldname": "to_date",
"fieldtype": "Date",
"label": "To Date",
"read_only": 1
},
{
"fieldname": "options_section",
"fieldtype": "Section Break",
"collapsible": 1,
"label": "Options"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "column_break_o1",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "auto_submit",
"fieldtype": "Check",
"label": "Auto-submit Journal Entries"
},
{
"fieldname": "column_break_o2",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "enable_period_closing_voucher",
"fieldtype": "Check",
"label": "Create Period Closing Voucher"
},
{
"depends_on": "enable_period_closing_voucher",
"fieldname": "closing_account",
"fieldtype": "Link",
"label": "Closing Account",
"options": "Account"
},
{
"fieldname": "tab_dashboard",
"fieldtype": "Tab Break",
"label": "Dashboard"
},
{
"fieldname": "company_info_section",
"fieldtype": "Section Break",
"label": "Company Info"
},
{
"fieldname": "company_voen",
"fieldtype": "Data",
"label": "VOEN",
"read_only": 1
},
{
"fieldname": "company_activity_code",
"fieldtype": "Data",
"label": "Activity Code (OKVED)",
"read_only": 1
},
{
"fieldname": "column_break_ci1",
"fieldtype": "Column Break"
},
{
"fieldname": "company_classification",
"fieldtype": "Data",
"label": "Business Classification",
"read_only": 1
},
{
"fieldname": "company_tax_authority",
"fieldtype": "Data",
"label": "Tax Authority",
"read_only": 1
},
{
"fieldname": "column_break_ci2",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "is_vat_payer",
"fieldtype": "Check",
"label": "VAT Payer",
"read_only": 1
},
{
"default": "0",
"fieldname": "has_assets",
"fieldtype": "Check",
"label": "Has Fixed Assets",
"read_only": 1
},
{
"default": "0",
"fieldname": "has_land",
"fieldtype": "Check",
"label": "Has Land",
"read_only": 1
},
{
"default": "0",
"fieldname": "has_employees",
"fieldtype": "Check",
"label": "Has Employees",
"read_only": 1
},
{
"fieldname": "benefits_section",
"fieldtype": "Section Break",
"label": "Detected Tax Benefits & Exemptions"
},
{
"fieldname": "benefits_html",
"fieldtype": "HTML",
"label": "Benefits Display"
},
{
"description": "Auto-detected tax benefits. Uncheck 'Apply' to skip.",
"fieldname": "detected_benefits",
"fieldtype": "Table",
"label": "Benefits Data",
"options": "Tax Closing Benefit",
"hidden": 1
},
{
"fieldname": "tab_tax_details",
"fieldtype": "Tab Break",
"label": "Tax Details"
},
{
"fieldname": "taxes_section",
"fieldtype": "Section Break",
"label": "Applicable Taxes"
},
{
"fieldname": "tax_items",
"fieldtype": "Table",
"label": "Tax Items",
"options": "Tax Closing Tax Item"
},
{
"fieldname": "dashboard_financials_section",
"fieldtype": "Section Break",
"label": "Financial Data"
},
{
"fieldname": "total_income",
"fieldtype": "Currency",
"label": "Total Income",
"precision": "2",
"read_only": 1
},
{
"fieldname": "column_break_fd1",
"fieldtype": "Column Break"
},
{
"fieldname": "total_expense",
"fieldtype": "Currency",
"label": "Total Expense",
"precision": "2",
"read_only": 1
},
{
"fieldname": "column_break_fd2",
"fieldtype": "Column Break"
},
{
"fieldname": "net_profit",
"fieldtype": "Currency",
"label": "Net Profit / Loss",
"precision": "2",
"read_only": 1
},
{
"fieldname": "dashboard_assets_section",
"fieldtype": "Section Break",
"collapsible": 1,
"label": "Asset Data"
},
{
"fieldname": "total_asset_value",
"fieldtype": "Currency",
"label": "Total Asset Value (Gross)",
"precision": "2",
"read_only": 1
},
{
"fieldname": "accumulated_depreciation",
"fieldtype": "Currency",
"label": "Accumulated Depreciation",
"precision": "2",
"read_only": 1
},
{
"fieldname": "column_break_ad1",
"fieldtype": "Column Break"
},
{
"fieldname": "net_book_value",
"fieldtype": "Currency",
"label": "Net Book Value",
"precision": "2",
"read_only": 1
},
{
"fieldname": "avg_annual_value",
"fieldtype": "Currency",
"label": "Avg Annual Value",
"precision": "2",
"read_only": 1
},
{
"fieldname": "payroll_section",
"fieldtype": "Section Break",
"label": "Payroll Summary (Data Only)"
},
{
"fieldname": "payroll_summary",
"fieldtype": "Table",
"label": "Payroll Summary",
"options": "Tax Closing Payroll Summary",
"read_only": 1
},
{
"fieldname": "entry_preview_section",
"fieldtype": "Section Break",
"label": "Entry Preview"
},
{
"fieldname": "entry_previews",
"fieldtype": "Table",
"label": "Entry Previews",
"options": "Tax Closing Entry Preview",
"read_only": 1
},
{
"fieldname": "tab_results",
"fieldtype": "Tab Break",
"label": "Results"
},
{
"fieldname": "journal_entries_section",
"fieldtype": "Section Break",
"label": "Created Journal Entries"
},
{
"fieldname": "journal_entries",
"fieldtype": "Table",
"label": "Journal Entries",
"options": "Tax Closing Journal Entry",
"read_only": 1
},
{
"fieldname": "tab_log",
"fieldtype": "Tab Break",
"label": "Log"
},
{
"fieldname": "log_section",
"fieldtype": "Section Break",
"label": "Execution Log"
},
{
"fieldname": "log_html",
"fieldtype": "HTML",
"label": "Log Display"
},
{
"fieldname": "log_entries",
"fieldtype": "Table",
"label": "Log Entries",
"options": "Tax Closing Log Entry",
"read_only": 1
},
{
"fieldname": "summary_section",
"fieldtype": "Section Break",
"label": "Summary Report"
},
{
"fieldname": "summary_html",
"fieldtype": "HTML",
"label": "Summary Display"
},
{
"fieldname": "dashboard_totals_section",
"fieldtype": "Section Break",
"label": "Totals"
},
{
"bold": 1,
"fieldname": "total_tax_amount",
"fieldtype": "Currency",
"label": "Total Tax Amount (Entries Created)",
"precision": "2",
"read_only": 1
},
{
"fieldname": "column_break_tt1",
"fieldtype": "Column Break"
},
{
"bold": 1,
"fieldname": "total_payroll_taxes",
"fieldtype": "Currency",
"label": "Total Payroll Taxes (Data Only)",
"precision": "2",
"read_only": 1
},
{
"fieldname": "column_break_tt2",
"fieldtype": "Column Break"
},
{
"bold": 1,
"fieldname": "grand_total_tax_burden",
"fieldtype": "Currency",
"label": "Grand Total Tax Burden",
"precision": "2",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Tax Period Closing Wizard",
"print_hide": 1,
"read_only": 1,
"search_index": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-04-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Taxes Az",
"name": "Tax Period Closing Wizard",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
{
"actions": [],
"creation": "2026-04-01 12:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"tax_type",
"column_break_1",
"rate",
"column_break_2",
"description"
],
"fields": [
{
"fieldname": "tax_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Tax Type",
"options": "VAT (ƏDV)\nProfit Tax (Mənfəət)\nSimplified Tax - Trade Baku (Sadələşdirilmiş - Ticarət Bakı)\nSimplified Tax - Catering (Sadələşdirilmiş - İaşə)\nSimplified Tax - Other (Sadələşdirilmiş - Digər)\nProperty Tax (Əmlak)\nUnemployment Insurance Employer (İşsizlik - İşəgötürən)\nUnemployment Insurance Employee (İşsizlik - İşçi)\nDSMF Employer (DSMF - İşəgötürən)\nDSMF Employee (DSMF - İşçi)\nMedical Insurance Employer (Tibbi sığorta - İşəgötürən)\nMedical Insurance Employee (Tibbi sığorta - İşçi)",
"reqd": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "rate",
"fieldtype": "Percent",
"in_list_view": 1,
"label": "Rate (%)",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description"
}
],
"index_web_pages_for_search": 0,
"istable": 1,
"links": [],
"modified": "2026-04-01 12:00:00.000000",
"modified_by": "Administrator",
"module": "Taxes Az",
"name": "Tax Rate Row",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,5 @@
from frappe.model.document import Document
class TaxRateRow(Document):
pass

View File

@ -0,0 +1,149 @@
{
"actions": [],
"creation": "2026-04-01 20:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"step_order",
"step_type",
"period_label",
"column_break_1",
"month_start",
"month_end",
"quarter",
"column_break_2",
"taxes_included",
"column_break_3",
"status",
"tax_amount",
"journal_entries",
"column_break_4",
"deadline_declaration",
"deadline_payment",
"completed_date",
"details"
],
"fields": [
{
"fieldname": "step_order",
"fieldtype": "Int",
"in_list_view": 1,
"label": "#",
"read_only": 1
},
{
"fieldname": "step_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Type",
"options": "Monthly\nQuarterly\nAnnual",
"read_only": 1
},
{
"fieldname": "period_label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Period",
"read_only": 1
},
{
"fieldname": "column_break_1",
"fieldtype": "Column Break"
},
{
"fieldname": "month_start",
"fieldtype": "Date",
"label": "From",
"read_only": 1
},
{
"fieldname": "month_end",
"fieldtype": "Date",
"label": "To",
"read_only": 1
},
{
"fieldname": "quarter",
"fieldtype": "Data",
"label": "Quarter",
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "taxes_included",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Taxes",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Pending\nActive\nCalculated\nCompleted\nSkipped",
"read_only": 1
},
{
"fieldname": "tax_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Tax Amount",
"precision": "2",
"read_only": 1
},
{
"fieldname": "journal_entries",
"fieldtype": "Small Text",
"label": "Journal Entries",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "deadline_declaration",
"fieldtype": "Date",
"label": "Declaration Deadline",
"read_only": 1
},
{
"fieldname": "deadline_payment",
"fieldtype": "Date",
"label": "Payment Deadline",
"read_only": 1
},
{
"fieldname": "completed_date",
"fieldtype": "Date",
"label": "Completed Date",
"read_only": 1
},
{
"fieldname": "details",
"fieldtype": "Small Text",
"label": "Details",
"read_only": 1
}
],
"index_web_pages_for_search": 0,
"istable": 1,
"links": [],
"modified": "2026-04-01 20:00:00.000000",
"modified_by": "Administrator",
"module": "Taxes Az",
"name": "Tax Year Closing Step",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,5 @@
from frappe.model.document import Document
class TaxYearClosingStep(Document):
pass

View File

@ -0,0 +1,956 @@
frappe.ui.form.on("Tax Year Closing Wizard", {
onload(frm) {
// Full width
$(`<style>
[data-doctype="Tax Year Closing Wizard"] .form-section .section-body,
[data-doctype="Tax Year Closing Wizard"] .form-column,
[data-doctype="Tax Year Closing Wizard"] .form-layout,
[data-doctype="Tax Year Closing Wizard"] .layout-main .layout-main-section,
[data-doctype="Tax Year Closing Wizard"] .form-page {
max-width: 100% !important;
}
</style>`).appendTo("head");
},
refresh(frm) {
frm.trigger("setup_buttons");
frm.trigger("render_timeline");
frm.trigger("render_current_step");
frm.trigger("render_benefits");
frm.trigger("render_summary");
if (frm.doc.company && frm.doc.docstatus === 0) {
frm.trigger("check_accounts");
}
},
check_accounts(frm) {
frappe.call({
method: "taxes_az.setup_accounts.check_required_accounts",
args: { company: frm.doc.company },
callback(r) {
if (!r || !r.message) return;
const data = r.message;
if (!data.needs_kassa_accounts || data.missing.length === 0) return;
let msg = `<p><strong>${__("Required accounts for cash method not found:")}</strong></p><ul>`;
for (const m of data.missing) {
msg += `<li><strong>${m.number}</strong> — ${m.name}<br><span style="font-size:11px;color:#666;">${m.description}</span></li>`;
}
msg += `</ul>`;
frappe.msgprint({
title: __("Accounts not found"),
message: msg,
indicator: "orange",
primary_action: {
label: __("Create accounts"),
action() {
frappe.call({
method: "taxes_az.setup_accounts.create_missing_accounts",
args: { company: frm.doc.company },
freeze: true,
callback(r2) {
if (r2 && r2.message && r2.message.created.length > 0) {
frappe.show_alert({
message: __("{0} accounts created", [r2.message.created.length]),
indicator: "green"
});
}
frappe.hide_msgprint();
}
});
}
}
});
}
});
},
setup_buttons(frm) {
if (frm.doc.docstatus >= 1) return;
if (!frm.doc.steps || frm.doc.steps.length === 0) {
frm.add_custom_button(__("Generate Timeline"), () => {
frm.save().then(() => {
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_year_closing_wizard.tax_year_closing_wizard.initialize_wizard",
args: { name: frm.doc.name },
freeze: true,
freeze_message: __("Generating timeline..."),
callback() { window.location.reload(); }
});
});
}).addClass("btn-primary");
}
if ((frm.doc.steps || []).length > 0 && frm.doc.overall_status !== "Completed") {
frm.add_custom_button(__("Close all periods"), () => {
// First check SI accounts
check_and_fix_before_closing(frm, () => {
run_all_steps_sequentially(frm);
});
}).addClass("btn-primary").css({"font-weight": "bold"});
}
if ((frm.doc.steps || []).length > 0) {
frm.add_custom_button(__("FIFO Allocate All"), () => {
frappe.confirm(
__("Run FIFO allocation for ALL months? This will recognize revenue and VAT for all received payments."),
() => {
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_year_closing_wizard.tax_year_closing_wizard.allocate_all",
args: { name: frm.doc.name },
freeze: true,
freeze_message: __("Running FIFO allocation for all periods..."),
callback(r) {
if (r && r.message) {
const res = r.message;
if (res.total_jes === 0) {
frappe.msgprint({
title: __("FIFO Allocation"),
message: __("FIFO allocation already done for all periods. No new allocations."),
indicator: "orange",
});
} else {
frappe.msgprint({
title: __("FIFO Allocation — All periods"),
message: `<div style="font-size:14px;">
<p> <strong>${res.total_jes}</strong> ${__("Journal Entries created")}</p>
<p>${__("VAT recognition:")} <strong>${format_azn(res.total_vat)}</strong> (245.1 521.3)</p>
<p>${__("Revenue recognition:")} <strong>${format_azn(res.total_revenue)}</strong> (542 601)</p>
<p>${__("Periods:")} ${res.periods_processed}</p>
</div>`,
indicator: "green",
wide: true,
});
}
}
window.location.reload();
}
});
}
);
}).addClass("btn-primary");
}
},
// ── Timeline Stepper ─────────────────────────────────
render_timeline(frm) {
const steps = frm.doc.steps || [];
if (!steps.length) {
frm.fields_dict.timeline_html && frm.fields_dict.timeline_html.$wrapper.html(
`<div style="padding:30px;text-align:center;color:#999;">${__('Click "Generate Timeline" to create the year plan.')}</div>`
);
return;
}
const cfg = {
"Completed": { bg: "#d4edda", border: "#28a745", icon: "✓", color: "#155724" },
"Skipped": { bg: "#e2e3e5", border: "#6c757d", icon: "—", color: "#383d41" },
"Active": { bg: "#fff3cd", border: "#ffc107", icon: "◉", color: "#856404" },
"Calculated":{ bg: "#cce5ff", border: "#007bff", icon: "◉", color: "#004085" },
"Pending": { bg: "#f8f9fa", border: "#dee2e6", icon: "○", color: "#6c757d" },
};
let html = '<div style="display:flex;flex-direction:column;gap:2px;">';
for (const step of steps) {
const c = cfg[step.status] || cfg["Pending"];
const is_active = step.status === "Active" || step.status === "Calculated";
const is_quarterly = step.step_type === "Quarterly";
const is_annual = step.step_type === "Annual";
const indent = is_quarterly ? "margin-left:20px;" : (is_annual ? "margin-left:0;border-width:2px;" : "");
const font_weight = (is_quarterly || is_annual) ? "font-weight:bold;" : "";
const amount_html = flt(step.tax_amount) > 0
? `<span style="font-weight:bold;color:#28a745;">${format_azn(step.tax_amount)}</span>`
: (step.status === "Completed" ? '<span style="color:#28a745;">0.00 AZN</span>' : "");
const je_list = (step.journal_entries || "").split(",").map(j => j.trim()).filter(Boolean);
const je_html = je_list.length > 0
? je_list.map(j => `<a href="/app/journal-entry/${j}" style="font-size:11px;">${j}</a>`).join(", ")
: "";
const deadline_html = step.deadline_payment
? (() => {
const d = new Date(step.deadline_payment);
const today = new Date();
const days = Math.ceil((d - today) / 864e5);
const urg = days <= 0 ? "color:#dc3545;font-weight:bold;" :
days <= 14 ? "color:#e67e22;" : "color:#999;";
return `<span style="font-size:11px;${urg}">⏰ ${step.deadline_payment}</span>`;
})()
: "";
// Completed summary mini-card
const is_completed = step.status === "Completed";
let completed_summary = "";
if (is_completed) {
const details_lines = (step.details || "").split("\n").filter(l => l.trim());
const details_short = details_lines.map(l =>
`<div style="font-size:11px;color:#155724;">${frappe.utils.escape_html(l)}</div>`
).join("");
completed_summary = `
<div style="margin-top:6px;display:flex;gap:8px;align-items:flex-start;">
<div style="flex:1;">
${details_short}
${je_list.length > 0 ? `<div style="font-size:11px;margin-top:3px;">📄 ${je_list.map(j => {
if (j.startsWith("PCV:")) {
const pcv = j.replace("PCV:", "");
return `<a href="/app/period-closing-voucher/${pcv}" style="color:#6f42c1;">📋${pcv}</a>`;
}
return `<a href="/app/journal-entry/${j}">${j}</a>`;
}).join(", ")}</div>` : ""}
${step.completed_date ? `<div style="font-size:10px;color:#999;margin-top:2px;">✓ ${step.completed_date}</div>` : ""}
</div>
<button class="btn btn-sm btn-outline-success tycw-report" data-step="${step.step_order}" style="font-size:11px;padding:2px 8px;">📊 ${__("Report")}</button>
</div>`;
}
html += `
<div style="display:flex;align-items:stretch;${indent}">
<div style="width:36px;display:flex;flex-direction:column;align-items:center;">
<div style="width:28px;height:28px;border-radius:50%;background:${c.border};color:white;
display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:bold;
${is_active ? 'animation:pulse 1.5s infinite;' : ''}">
${c.icon}
</div>
<div style="flex:1;width:2px;background:${c.border};margin:2px 0;"></div>
</div>
<div style="flex:1;background:${c.bg};border:1px solid ${c.border};border-radius:6px;
padding:10px 14px;margin-bottom:2px;
${font_weight}" data-step="${step.step_order}" class="tycw-step">
<div style="display:flex;justify-content:space-between;align-items:center;">
<div>
<span style="color:${c.color};font-size:13px;">
${step.period_label}
</span>
<span style="color:#999;font-size:11px;margin-left:8px;">${step.taxes_included || ""}</span>
</div>
<div style="display:flex;align-items:center;gap:12px;">
${amount_html}
${!is_completed ? deadline_html : ""}
</div>
</div>
${completed_summary}
${is_active && step.details ? `<div style="margin-top:6px;font-size:12px;color:#555;white-space:pre-line;">${frappe.utils.escape_html(step.details)}</div>` : ""}
${is_active ? `<div style="margin-top:8px;display:flex;gap:6px;" class="tycw-step-actions">` +
(step.status === "Active" ? `<button class="btn btn-primary btn-sm tycw-calc" data-step="${step.step_order}">${__("Calculate")}</button>` : "") +
(step.status === "Active" || step.status === "Calculated" ? `<button class="btn btn-info btn-sm tycw-allocate" data-step="${step.step_order}">FIFO Allocate</button>` : "") +
(step.status === "Calculated" ? `<button class="btn btn-success btn-sm tycw-confirm" data-step="${step.step_order}">${__("Confirm and create")}</button>` : "") +
(step.status === "Calculated" ? `<button class="btn btn-default btn-sm tycw-recalc" data-step="${step.step_order}">${__("Recalculate")}</button>` : "") +
`<button class="btn btn-warning btn-sm tycw-report" data-step="${step.step_order}">${__("Report")}</button>
<button class="btn btn-default btn-sm tycw-skip" data-step="${step.step_order}">${__("Skip")}</button>
</div>` : ""}
${step.status === "Skipped" ? `<div style="margin-top:4px;"><button class="btn btn-sm btn-outline-secondary tycw-report" data-step="${step.step_order}" style="font-size:11px;padding:2px 8px;">📊 ${__("Report")}</button></div>` : ""}
</div>
</div>`;
}
html += '</div>';
html += `<style>@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.7;} }</style>`;
const $wrapper = frm.fields_dict.timeline_html.$wrapper;
$wrapper.html(html);
// Bind buttons
$wrapper.find(".tycw-calc").on("click", function(e) {
e.stopPropagation();
call_step_action(frm, "calculate_step", $(this).data("step"));
});
$wrapper.find(".tycw-confirm").on("click", function(e) {
e.stopPropagation();
const step_order = $(this).data("step");
frappe.confirm(__("Create journal entries for this step?"), () => {
call_step_action(frm, "complete_step", step_order);
});
});
$wrapper.find(".tycw-recalc").on("click", function(e) {
e.stopPropagation();
call_step_action(frm, "calculate_step", $(this).data("step"));
});
$wrapper.find(".tycw-skip").on("click", function(e) {
e.stopPropagation();
const step_order = $(this).data("step");
frappe.confirm(__("Skip this step? It will be marked as Skipped."), () => {
call_step_action(frm, "skip_step", step_order);
});
});
$wrapper.find(".tycw-allocate").on("click", function(e) {
e.stopPropagation();
show_fifo_allocation(frm, $(this).data("step"));
});
$wrapper.find(".tycw-report").on("click", function(e) {
e.stopPropagation();
show_step_monthly_report(frm, $(this).data("step"));
});
},
// ── Current Step Detail ──────────────────────────────
render_current_step(frm) {
const steps = frm.doc.steps || [];
const active = steps.find(s => s.status === "Active" || s.status === "Calculated");
if (!active) {
frm.fields_dict.current_step_html && frm.fields_dict.current_step_html.$wrapper.html(
frm.doc.overall_status === "Completed"
? `<div style="padding:30px;text-align:center;color:#28a745;font-size:18px;font-weight:bold;">✓ ${__("All steps completed!")}</div>`
: `<div style="padding:30px;text-align:center;color:#999;">${__("No active step. Go to Timeline tab.")}</div>`
);
return;
}
let html = `
<div style="padding:20px;background:#fff;border:2px solid #ffc107;border-radius:8px;">
<h4 style="margin-bottom:15px;"> ${frappe.utils.escape_html(active.period_label)}</h4>
<div style="display:flex;gap:20px;margin-bottom:15px;">
<div><strong>${__("Type:")}</strong> ${active.step_type}</div>
<div><strong>${__("Period:")}</strong> ${active.month_start} ${active.month_end}</div>
<div><strong>${__("Taxes:")}</strong> ${frappe.utils.escape_html(active.taxes_included || "")}</div>
<div><strong>${__("Declaration:")}</strong> ${active.deadline_declaration || ""}</div>
<div><strong>${__("Payment:")}</strong> ${active.deadline_payment || ""}</div>
</div>
`;
if (active.details) {
html += `<div style="background:#f8f9fa;padding:12px;border-radius:4px;font-family:monospace;font-size:12px;white-space:pre-line;margin-bottom:15px;">
${frappe.utils.escape_html(active.details)}</div>`;
}
if (flt(active.tax_amount) > 0 || active.status === "Calculated") {
html += `<div style="font-size:18px;font-weight:bold;color:#28a745;margin-bottom:15px;">
${__("Tax:")} ${format_azn(active.tax_amount)}</div>`;
}
html += `</div>`;
frm.fields_dict.current_step_html && frm.fields_dict.current_step_html.$wrapper.html(html);
},
// ── Benefits ─────────────────────────────────────────
render_benefits(frm) {
const benefits = frm.doc.detected_benefits || [];
if (!benefits.length || !frm.fields_dict.benefits_html) return;
let html = '<div style="display:flex;flex-direction:column;gap:8px;">';
for (const b of benefits) {
html += `
<div style="border-left:4px solid #28a745;background:#f0fdf4;border-radius:4px;padding:10px 14px;">
<div style="font-weight:bold;color:#155724;"> ${frappe.utils.escape_html(b.article)} ${frappe.utils.escape_html(b.benefit_name)}</div>
<div style="font-size:12px;color:#555;margin-top:4px;">${frappe.utils.escape_html(b.condition_details || "")}</div>
${b.law_reference ? `<div style="font-size:11px;color:#4338ca;margin-top:4px;">📖 ${b.law_url ? `<a href="${frappe.utils.escape_html(b.law_url)}" target="_blank" style="color:#4338ca;text-decoration:underline;">${frappe.utils.escape_html(b.law_reference)}</a>` : frappe.utils.escape_html(b.law_reference)}</div>` : ""}
</div>`;
}
html += '</div>';
frm.fields_dict.benefits_html.$wrapper.html(html);
},
// ── Summary ──────────────────────────────────────────
render_summary(frm) {
const steps = frm.doc.steps || [];
if (!steps.length || !frm.fields_dict.summary_html) return;
const completed = steps.filter(s => s.status === "Completed");
const skipped = steps.filter(s => s.status === "Skipped");
const pending = steps.filter(s => s.status === "Pending" || s.status === "Active");
let html = `
<div style="display:flex;gap:15px;margin-bottom:15px;">
<div style="flex:1;padding:15px;background:#d4edda;border-radius:6px;text-align:center;">
<div style="font-size:24px;font-weight:bold;color:#155724;">${completed.length}</div>
<div style="color:#155724;">${__("Completed")}</div>
</div>
<div style="flex:1;padding:15px;background:#e2e3e5;border-radius:6px;text-align:center;">
<div style="font-size:24px;font-weight:bold;color:#383d41;">${skipped.length}</div>
<div style="color:#383d41;">${__("Skipped")}</div>
</div>
<div style="flex:1;padding:15px;background:#fff3cd;border-radius:6px;text-align:center;">
<div style="font-size:24px;font-weight:bold;color:#856404;">${pending.length}</div>
<div style="color:#856404;">${__("Pending")}</div>
</div>
</div>
`;
if (completed.length > 0) {
html += `<table class="table table-sm table-bordered" style="font-size:12px;">
<thead style="background:#2c3e50;color:white;">
<tr><th>${__("Period")}</th><th>${__("Taxes")}</th><th style="text-align:right;">${__("Amount")}</th><th>${__("JE")}</th><th>${__("Date")}</th></tr>
</thead><tbody>`;
for (const s of completed) {
const jes = (s.journal_entries || "").split(",").map(j => {
j = j.trim();
return j ? `<a href="/app/journal-entry/${j}">${j}</a>` : "";
}).filter(Boolean).join(", ");
html += `<tr>
<td>${frappe.utils.escape_html(s.period_label)}</td>
<td>${frappe.utils.escape_html(s.taxes_included || "")}</td>
<td style="text-align:right;font-weight:bold;">${format_azn(s.tax_amount)}</td>
<td>${jes}</td>
<td>${s.completed_date || ""}</td>
</tr>`;
}
html += `</tbody></table>`;
}
frm.fields_dict.summary_html.$wrapper.html(html);
}
});
function format_azn(val) {
return (parseFloat(val) || 0).toLocaleString("az-AZ", {
minimumFractionDigits: 2, maximumFractionDigits: 2
}) + " AZN";
}
function flt(val) {
return parseFloat(val) || 0;
}
function show_step_monthly_report(frm, step_order) {
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_year_closing_wizard.tax_year_closing_wizard.get_step_report",
args: { name: frm.doc.name, step_order: step_order },
freeze: true,
freeze_message: __("Loading report..."),
callback(r) {
if (!r || !r.message) return;
const d = r.message;
const f = (v) => (parseFloat(v) || 0).toLocaleString("az-AZ", {minimumFractionDigits:2, maximumFractionDigits:2});
let html = `<div style="font-family:-apple-system,sans-serif;font-size:13px;">`;
// Header
html += `<div style="background:#2c3e50;color:white;padding:14px;border-radius:6px;margin-bottom:12px;display:flex;justify-content:space-between;">
<div><strong style="font-size:16px;">${d.period}</strong><br>${d.from} ${d.to}</div>
<div style="text-align:right;">${__("Declaration:")} ${d.deadline_declaration || "—"}<br>${__("Payment:")} ${d.deadline_payment || "—"}</div>
</div>`;
// Cash flow
html += `<div style="display:flex;gap:10px;margin-bottom:12px;">
<div style="flex:1;padding:12px;background:#d4edda;border-radius:6px;text-align:center;">
<div style="font-size:11px;color:#155724;">${__("Cash in (PE+226)")}</div>
<div style="font-size:18px;font-weight:bold;color:#155724;">${f(d.total_cash_in)} AZN</div>
</div>
<div style="flex:1;padding:12px;background:#f8d7da;border-radius:6px;text-align:center;">
<div style="font-size:11px;color:#721c24;">${__("Cash out")}</div>
<div style="font-size:18px;font-weight:bold;color:#721c24;">${f(d.payments_made)} AZN</div>
</div>
<div style="flex:1;padding:12px;background:#cce5ff;border-radius:6px;text-align:center;">
<div style="font-size:11px;color:#004085;">ƏDV (226)</div>
<div style="font-size:18px;font-weight:bold;color:#004085;">${f(d.vat_226_received)} AZN</div>
</div>
</div>`;
// Income table
if ((d.income || []).length > 0) {
html += `<h5>${__("Income")}</h5><table class="table table-sm table-bordered">
<thead style="background:#28a745;color:white;"><tr><th>${__("Account")}</th><th>${__("Name")}</th><th style="text-align:right;">${__("Amount")}</th></tr></thead><tbody>`;
for (const i of d.income) {
html += `<tr><td>${i.account}</td><td>${i.name}</td><td style="text-align:right;">${f(i.amount)}</td></tr>`;
}
html += `<tr style="font-weight:bold;background:#d4edda;"><td colspan="2">${__("Total income")}</td><td style="text-align:right;">${f(d.total_income)} AZN</td></tr>`;
html += `</tbody></table>`;
}
// Expense table
if ((d.expenses || []).length > 0) {
html += `<h5>${__("Expenses")}</h5><table class="table table-sm table-bordered">
<thead style="background:#dc3545;color:white;"><tr><th>${__("Account")}</th><th>${__("Name")}</th><th style="text-align:right;">${__("Amount")}</th></tr></thead><tbody>`;
for (const e of d.expenses) {
html += `<tr><td>${e.account}</td><td>${e.name}</td><td style="text-align:right;">${f(e.amount)}</td></tr>`;
}
html += `<tr style="font-weight:bold;background:#f8d7da;"><td colspan="2">${__("Total expense")}</td><td style="text-align:right;">${f(d.total_expense)} AZN</td></tr>`;
html += `</tbody></table>`;
}
// Profit
const profit_color = d.net_profit >= 0 ? "#28a745" : "#dc3545";
html += `<div style="padding:10px;background:#f8f9fa;border-radius:4px;margin-bottom:12px;">
<strong>${__("Net profit/loss:")}</strong>
<span style="font-size:16px;font-weight:bold;color:${profit_color};margin-left:10px;">${f(d.net_profit)} AZN</span>
</div>`;
// VAT
html += `<h5>ƏDV</h5><table class="table table-sm table-bordered">
<tr><td>ƏDV çıxış (521.3)</td><td style="text-align:right;">${f(d.vat_output)}</td></tr>
<tr><td>ƏDV daxil (226)</td><td style="text-align:right;">${f(d.vat_input)}</td></tr>
<tr style="font-weight:bold;"><td>Nett ƏDV</td><td style="text-align:right;">${f(d.vat_net)}</td></tr>
</table>`;
// Payroll
if (d.payroll) {
html += `<h5>${__("Payroll")}</h5><table class="table table-sm table-bordered">
<tr><td>${__("Employee count")}</td><td style="text-align:right;">${d.payroll.count}</td></tr>
<tr><td>${__("Gross salary")}</td><td style="text-align:right;">${f(d.payroll.gross)}</td></tr>
<tr><td>${__("Deductions")}</td><td style="text-align:right;">${f(d.payroll.deductions)}</td></tr>
<tr><td>${__("Net salary")}</td><td style="text-align:right;">${f(d.payroll.net)}</td></tr>
</table>`;
}
// FIFO status
if ((d.fifo_entries || []).length > 0) {
html += `<h5>FIFO Allocations</h5>`;
for (const j of d.fifo_entries) {
html += `<a href="/app/journal-entry/${j.name}">${j.name}</a> (${f(j.amount)}) `;
}
} else {
html += `<div style="padding:8px;background:#fff3cd;border-radius:4px;color:#856404;">
${__("FIFO allocation not done yet. Click the FIFO Allocate button.")}</div>`;
}
// Step JEs + PCV
if ((d.step_jes || []).length > 0) {
html += `<h5 style="margin-top:10px;">${__("Created documents")}</h5>`;
for (const j of d.step_jes) {
if (j.startsWith("PCV:")) {
const pcv_name = j.replace("PCV:", "");
html += `<div style="margin:4px 0;"><a href="/app/period-closing-voucher/${pcv_name}" style="color:#6f42c1;font-weight:bold;">📋 ${pcv_name} (Period Closing Voucher)</a></div>`;
} else {
html += `<a href="/app/journal-entry/${j}">${j}</a> `;
}
}
}
html += `</div>`;
new frappe.ui.Dialog({
title: `📊 ${d.period}${__("Report")}`,
size: "extra-large",
fields: [{ fieldtype: "HTML", options: html }],
secondary_action_label: __("Close"),
}).show();
}
});
}
function show_fifo_allocation(frm, step_order) {
// First preview
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_year_closing_wizard.tax_year_closing_wizard.preview_allocation",
args: { name: frm.doc.name, step_order: step_order },
freeze: true,
freeze_message: __("Analyzing FIFO allocation..."),
callback(r) {
if (!r || !r.message) return;
const data = r.message;
if (data.error) {
frappe.msgprint({ message: data.error, indicator: "red" });
return;
}
const allocs = data.allocations || [];
let total_vat = 0, total_rev = 0;
allocs.forEach(a => { total_vat += a.vat_transfer; total_rev += a.revenue_transfer; });
let html = `<div style="font-family:-apple-system,sans-serif;">`;
// Summary
html += `<div style="display:flex;gap:12px;margin-bottom:14px;">
<div style="flex:1;padding:12px;background:#d4edda;border-radius:6px;text-align:center;">
<div style="font-size:18px;font-weight:bold;color:#155724;">${allocs.length}</div>
<div style="color:#155724;font-size:12px;">${__("Allocation")}</div>
</div>
<div style="flex:1;padding:12px;background:#cce5ff;border-radius:6px;text-align:center;">
<div style="font-size:18px;font-weight:bold;color:#004085;">${format_azn(total_vat)}</div>
<div style="color:#004085;font-size:12px;">ƏDV 521.3</div>
</div>
<div style="flex:1;padding:12px;background:#fff3cd;border-radius:6px;text-align:center;">
<div style="font-size:18px;font-weight:bold;color:#856404;">${format_azn(total_rev)}</div>
<div style="color:#856404;font-size:12px;">Gəlir 601</div>
</div>
</div>`;
// Allocation table
if (allocs.length > 0) {
html += `<table class="table table-sm table-bordered" style="font-size:12px;">
<thead style="background:#2c3e50;color:white;">
<tr><th>${__("Invoice")}</th><th>${__("Customer")}</th><th>${__("Payment date")}</th>
<th style="text-align:right;">${__("Allocation")}</th><th style="text-align:center;">%</th>
<th style="text-align:right;">ƏDV</th><th style="text-align:right;">Gəlir</th></tr>
</thead><tbody>`;
for (const a of allocs) {
html += `<tr>
<td><a href="/app/sales-invoice/${a.invoice}">${a.invoice}</a></td>
<td style="font-size:11px;">${(a.customer || "").substring(0,25)}</td>
<td>${a.payment_date}</td>
<td style="text-align:right;">${format_azn(a.allocated)}</td>
<td style="text-align:center;">${(a.ratio*100).toFixed(0)}%</td>
<td style="text-align:right;color:#004085;">${format_azn(a.vat_transfer)}</td>
<td style="text-align:right;color:#856404;">${format_azn(a.revenue_transfer)}</td>
</tr>`;
}
html += `</tbody></table>`;
} else {
html += `<div style="padding:15px;text-align:center;color:#999;">
${__("No payments found for allocation in this period.")}</div>`;
}
html += `</div>`;
const dialog = new frappe.ui.Dialog({
title: __("FIFO Cash Allocation — Preview"),
size: "extra-large",
fields: [{ fieldtype: "HTML", options: html }],
primary_action_label: allocs.length > 0 ? __("Confirm and create") : null,
primary_action() {
dialog.hide();
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_year_closing_wizard.tax_year_closing_wizard.execute_allocation",
args: { name: frm.doc.name, step_order: step_order },
freeze: true,
freeze_message: __("Creating allocation entries..."),
callback(r2) {
if (r2 && r2.message) {
const res = r2.message;
frappe.msgprint({
title: __("FIFO Allocation completed"),
message: `<div style="font-size:14px;">
<p> <strong>${res.count}</strong> ${__("Journal Entries created")}</p>
<p>${__("VAT recognition:")} <strong>${format_azn(res.total_vat)}</strong> (245.1 521.3)</p>
<p>${__("Revenue recognition:")} <strong>${format_azn(res.total_revenue)}</strong> (542 601)</p>
${(res.created_jes || []).map(j => `<a href="/app/journal-entry/${j}">${j}</a>`).join(", ")}
</div>`,
indicator: "green",
wide: true,
});
}
window.location.reload();
}
});
},
secondary_action_label: __("Close"),
});
dialog.show();
}
});
}
// ── Check SI Accounts Before Closing ─────────────────
function check_and_fix_before_closing(frm, on_continue) {
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_year_closing_wizard.tax_year_closing_wizard.check_si_accounts",
args: { name: frm.doc.name },
callback(r) {
if (!r || !r.message) { on_continue(); return; }
const data = r.message;
if (!data.needs_fix) {
on_continue();
return;
}
// Show fix dialog
const wrong = data.wrong || [];
let html = `<div style="font-family:-apple-system,sans-serif;">
<div style="padding:10px;background:#fff3cd;border-radius:6px;margin-bottom:12px;">
${__("{0} Sales Invoices still on old accounts (521.3/601). For cash method they must be moved to 245.1/542.", [wrong.length])}
</div>
<table class="table table-sm table-bordered" style="font-size:12px;">
<thead style="background:#2c3e50;color:white;">
<tr><th>${__("Invoice")}</th><th>${__("Date")}</th><th style="text-align:right;">ƏDV</th><th style="text-align:right;">${__("Total")}</th></tr>
</thead><tbody>`;
for (const w of wrong.slice(0, 20)) {
html += `<tr>
<td><a href="/app/sales-invoice/${w.invoice}">${w.invoice}</a></td>
<td>${w.date}</td>
<td style="text-align:right;">${format_azn(w.vat)}</td>
<td style="text-align:right;">${format_azn(w.total)}</td>
</tr>`;
}
if (wrong.length > 20) {
html += `<tr><td colspan="4">... ${__("and more")} ${wrong.length - 20}</td></tr>`;
}
html += `</tbody></table></div>`;
const dlg = new frappe.ui.Dialog({
title: __("Invoice account correction required"),
size: "large",
fields: [{ fieldtype: "HTML", options: html }],
primary_action_label: __("Fix and continue"),
primary_action() {
dlg.hide();
frappe.call({
method: "taxes_az.taxes_az.doctype.tax_year_closing_wizard.tax_year_closing_wizard.fix_si_accounts",
args: { name: frm.doc.name },
freeze: true,
freeze_message: __("Fixing invoices..."),
callback(r2) {
if (r2 && r2.message) {
frappe.show_alert({
message: __("{0} invoices fixed", [r2.message.fixed]),
indicator: "green"
});
}
on_continue();
}
});
},
secondary_action_label: __("Continue without fixing"),
secondary_action() {
dlg.hide();
on_continue();
}
});
dlg.show();
}
});
}
// ── Run All Steps Sequentially ───────────────────────
function run_all_steps_sequentially(frm) {
const steps = (frm.doc.steps || []).filter(s => s.status !== "Completed" && s.status !== "Skipped");
if (!steps.length) {
frappe.msgprint(__("All steps already completed."));
return;
}
// Create progress dialog
const dlg = new frappe.ui.Dialog({
title: __("Close all periods"),
size: "large",
static: true,
fields: [{ fieldtype: "HTML", fieldname: "progress_area" }],
secondary_action_label: __("Stop"),
secondary_action() {
dlg._stopped = true;
dlg.hide();
window.location.reload();
}
});
dlg._stopped = false;
dlg.show();
const $area = dlg.fields_dict.progress_area.$wrapper;
function render_progress(current_idx, total, current_step, phase, error) {
const pct = Math.round((current_idx / total) * 100);
let rows = "";
const all_steps = frm.doc.steps || [];
for (const st of all_steps) {
let icon, bg, text_color;
if (st.status === "Completed") {
icon = "✓"; bg = "#d4edda"; text_color = "#155724";
} else if (st.status === "Skipped") {
icon = "—"; bg = "#e2e3e5"; text_color = "#383d41";
} else if (st.step_order === (current_step ? current_step.step_order : -1)) {
if (error) {
icon = "✗"; bg = "#f8d7da"; text_color = "#721c24";
} else {
icon = "⟳"; bg = "#fff3cd"; text_color = "#856404";
}
} else {
icon = "○"; bg = "#f8f9fa"; text_color = "#6c757d";
}
const is_current = current_step && st.step_order === current_step.step_order;
const phase_text = is_current && phase ? ` — <em>${phase}</em>` : "";
const amount_text = st.status === "Completed" && flt(st.tax_amount) > 0
? `<span style="color:#28a745;font-weight:bold;margin-left:8px;">${format_azn(st.tax_amount)}</span>` : "";
const error_text = is_current && error
? `<div style="color:#dc3545;font-size:11px;margin-top:2px;">${error}</div>` : "";
rows += `<div style="display:flex;align-items:center;gap:8px;padding:5px 10px;background:${bg};
border-radius:4px;margin-bottom:2px;${is_current ? 'border-left:4px solid '+text_color+';' : ''}">
<span style="font-size:14px;color:${text_color};width:20px;text-align:center;
${is_current && !error ? 'animation:spin 1s linear infinite;' : ''}">${icon}</span>
<span style="color:${text_color};font-size:12px;flex:1;">${st.period_label}${phase_text}</span>
${amount_text}
</div>${error_text}`;
}
$area.html(`
<style>@keyframes spin { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }</style>
<div style="margin-bottom:12px;">
<div style="display:flex;justify-content:space-between;margin-bottom:4px;">
<span style="font-weight:bold;">${current_step ? current_step.period_label : __("Completed")}</span>
<span style="font-weight:bold;">${pct}%</span>
</div>
<div style="background:#e9ecef;border-radius:8px;overflow:hidden;height:20px;">
<div style="width:${pct}%;height:100%;background:linear-gradient(90deg,#28a745,#20c997);
border-radius:8px;transition:width 0.5s ease;"></div>
</div>
</div>
<div style="max-height:400px;overflow-y:auto;">${rows}</div>
`);
}
// Sequential processing
let step_idx = 0;
const pending_steps = steps.slice();
async function process_next() {
if (dlg._stopped) return;
if (step_idx >= pending_steps.length) {
// All done!
render_progress(pending_steps.length, pending_steps.length, null, null, null);
$area.prepend(`<div style="padding:14px;background:#28a745;color:white;border-radius:6px;
margin-bottom:12px;text-align:center;font-size:16px;font-weight:bold;">
${__("All steps completed successfully!")}</div>`);
dlg.set_secondary_action_label(__("Close"));
dlg.set_secondary_action(() => { dlg.hide(); window.location.reload(); });
return;
}
const step = pending_steps[step_idx];
const total = pending_steps.length;
try {
// Phase 1: Calculate
render_progress(step_idx, total, step, __("Calculating..."), null);
await call_step_async("calculate_step", frm.doc.name, step.step_order);
if (dlg._stopped) return;
// Phase 2: Complete (includes FIFO for kassa, VAT for hesablama)
render_progress(step_idx, total, step, __("Confirming..."), null);
const result = await call_step_async("complete_step", frm.doc.name, step.step_order);
// Update local step data
step.status = "Completed";
if (result && result.step_data) {
step.tax_amount = result.step_data.tax_amount;
step.details = result.step_data.details;
step.journal_entries = result.step_data.journal_entries;
}
step_idx++;
// Small delay for visual effect
setTimeout(process_next, 300);
} catch (err) {
const msg = (err && err.message) || String(err) || "Unknown error";
render_progress(step_idx, total, step, null, msg);
$area.prepend(`<div style="padding:10px;background:#f8d7da;color:#721c24;border-radius:6px;margin-bottom:12px;">
${__("Error:")} ${step.period_label} ${frappe.utils.escape_html(msg)}.
${__("Previous steps have been saved.")}</div>`);
dlg.set_secondary_action_label(__("Close"));
dlg.set_secondary_action(() => { dlg.hide(); window.location.reload(); });
}
}
// Reload doc first to get fresh data
frm.reload_doc().then(() => {
process_next();
});
}
function call_step_async(action, name, step_order) {
return new Promise((resolve, reject) => {
frappe.call({
method: `taxes_az.taxes_az.doctype.tax_year_closing_wizard.tax_year_closing_wizard.${action}`,
args: { name: name, step_order: step_order },
async: true,
callback(r) {
if (r && r.message) resolve(r.message);
else resolve({});
},
error(err) {
reject(err);
}
});
});
}
function call_step_action(frm, action, step_order) {
const labels = {
"calculate_step": __("Calculating..."),
"complete_step": __("Creating journal entries..."),
"skip_step": __("Skipping..."),
};
frappe.call({
method: `taxes_az.taxes_az.doctype.tax_year_closing_wizard.tax_year_closing_wizard.${action}`,
args: { name: frm.doc.name, step_order: step_order },
freeze: true,
freeze_message: __(labels[action] || "Processing..."),
callback(r) {
if (action === "complete_step" && r && r.message) {
show_step_report(r.message, () => window.location.reload());
} else {
window.location.reload();
}
},
error() { window.location.reload(); }
});
}
function show_step_report(data, on_close) {
const s = data.step_data || {};
const jes = (s.journal_entries || "").split(",").filter(j => j.trim());
let html = `<div style="font-family: -apple-system, sans-serif;">`;
// Header
html += `<div style="padding:14px;background:#28a745;color:white;border-radius:6px;margin-bottom:14px;text-align:center;">
<div style="font-size:18px;font-weight:bold;"> ${frappe.utils.escape_html(s.period_label || "")}</div>
<div style="font-size:13px;margin-top:4px;">${__("Step completed successfully")}</div>
</div>`;
// Details
html += `<table class="table table-bordered" style="font-size:13px;">
<tr><td style="width:40%;"><strong>${__("Period")}</strong></td><td>${s.period_label || ""}</td></tr>
<tr><td><strong>${__("Type")}</strong></td><td>${s.step_type || ""}</td></tr>
<tr><td><strong>${__("Date range")}</strong></td><td>${s.month_start || ""} ${s.month_end || ""}</td></tr>
<tr><td><strong>${__("Taxes")}</strong></td><td>${frappe.utils.escape_html(s.taxes_included || "")}</td></tr>
<tr><td><strong>${__("Declaration deadline")}</strong></td><td>${s.deadline_declaration || ""}</td></tr>
<tr><td><strong>${__("Payment deadline")}</strong></td><td>${s.deadline_payment || ""}</td></tr>
<tr style="background:#d4edda;"><td><strong>${__("Tax amount")}</strong></td>
<td style="font-size:16px;font-weight:bold;">${format_azn(s.tax_amount)}</td></tr>
</table>`;
// Calculation details
if (s.details) {
html += `<div style="background:#f8f9fa;padding:10px;border-radius:4px;font-family:monospace;font-size:12px;white-space:pre-line;margin-bottom:14px;">
${frappe.utils.escape_html(s.details)}</div>`;
}
// Journal Entries
if (jes.length > 0) {
html += `<h5>${__("Created documents")}</h5>
<table class="table table-sm table-bordered" style="font-size:13px;">
<thead style="background:#343a40;color:white;">
<tr><th>${__("Journal Entry")}</th><th>${__("Link")}</th></tr>
</thead><tbody>`;
for (const je of jes) {
const name = je.trim();
html += `<tr>
<td>${name}</td>
<td><a href="/app/journal-entry/${name}" target="_blank" style="color:#007bff;">
${__("Click to open")} </a></td>
</tr>`;
}
html += `</tbody></table>`;
} else {
html += `<div style="padding:10px;background:#fff3cd;border-radius:4px;font-size:12px;color:#856404;">
${__("No journal entries created (amount = 0 or data collection only)")}</div>`;
}
// Next step info
if (data.next_step) {
html += `<div style="margin-top:14px;padding:10px;background:#e3f2fd;border-left:4px solid #2196f3;border-radius:4px;">
<strong>${__("Next step:")}</strong> ${frappe.utils.escape_html(data.next_step)}
</div>`;
}
html += `</div>`;
const dialog = frappe.msgprint({
title: __("Step completed"),
message: html,
indicator: "green",
wide: true,
primary_action: {
label: __("Continue"),
action() { dialog.hide(); if (on_close) on_close(); }
}
});
}

View File

@ -0,0 +1,317 @@
{
"actions": [],
"autoname": "TYCW-.YYYY.-.#####",
"creation": "2026-04-01 20:00:00.000000",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"header_section",
"company",
"column_break_h1",
"fiscal_year",
"year",
"column_break_h2",
"tax_regime",
"accounting_method",
"column_break_h3",
"overall_status",
"overall_progress",
"company_info_section",
"company_voen",
"company_classification",
"column_break_ci1",
"company_activity_code",
"company_tax_authority",
"column_break_ci2",
"is_vat_payer",
"has_assets",
"has_employees",
"avg_employees",
"benefits_section",
"benefits_html",
"detected_benefits",
"timeline_tab",
"timeline_html",
"steps",
"step_detail_tab",
"current_step_html",
"summary_tab",
"summary_html",
"total_taxes_created",
"column_break_st1",
"total_payroll_collected",
"column_break_st2",
"grand_total",
"log_tab",
"log_html",
"log_entries",
"amended_from"
],
"fields": [
{
"fieldname": "header_section",
"fieldtype": "Section Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "column_break_h1",
"fieldtype": "Column Break"
},
{
"fieldname": "fiscal_year",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Fiscal Year",
"options": "Fiscal Year",
"reqd": 1
},
{
"fieldname": "year",
"fieldtype": "Int",
"label": "Year",
"read_only": 1
},
{
"fieldname": "column_break_h2",
"fieldtype": "Column Break"
},
{
"fieldname": "tax_regime",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Tax Regime",
"options": "\nGeneral (VAT + Profit)\nSimplified Tax\nSpecial Regime"
},
{
"fieldname": "accounting_method",
"fieldtype": "Data",
"label": "Accounting Method",
"read_only": 1
},
{
"fieldname": "column_break_h3",
"fieldtype": "Column Break"
},
{
"default": "Not Started",
"fieldname": "overall_status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Not Started\nIn Progress\nCompleted",
"read_only": 1
},
{
"default": "0",
"fieldname": "overall_progress",
"fieldtype": "Percent",
"label": "Progress",
"read_only": 1
},
{
"fieldname": "company_info_section",
"fieldtype": "Section Break",
"collapsible": 1,
"label": "Company Info"
},
{
"fieldname": "company_voen",
"fieldtype": "Data",
"label": "VOEN",
"read_only": 1
},
{
"fieldname": "company_classification",
"fieldtype": "Data",
"label": "Classification",
"read_only": 1
},
{
"fieldname": "column_break_ci1",
"fieldtype": "Column Break"
},
{
"fieldname": "company_activity_code",
"fieldtype": "Data",
"label": "OKVED",
"read_only": 1
},
{
"fieldname": "company_tax_authority",
"fieldtype": "Data",
"label": "Tax Authority",
"read_only": 1
},
{
"fieldname": "column_break_ci2",
"fieldtype": "Column Break"
},
{
"fieldname": "is_vat_payer",
"fieldtype": "Check",
"label": "VAT Payer",
"read_only": 1
},
{
"fieldname": "has_assets",
"fieldtype": "Check",
"label": "Has Assets",
"read_only": 1
},
{
"fieldname": "has_employees",
"fieldtype": "Check",
"label": "Has Employees",
"read_only": 1
},
{
"fieldname": "avg_employees",
"fieldtype": "Float",
"label": "Avg Monthly Employees",
"read_only": 1
},
{
"fieldname": "benefits_section",
"fieldtype": "Section Break",
"collapsible": 1,
"label": "Detected Benefits"
},
{
"fieldname": "benefits_html",
"fieldtype": "HTML"
},
{
"fieldname": "detected_benefits",
"fieldtype": "Table",
"options": "Tax Closing Benefit",
"hidden": 1
},
{
"fieldname": "timeline_tab",
"fieldtype": "Tab Break",
"label": "Timeline"
},
{
"fieldname": "timeline_html",
"fieldtype": "HTML"
},
{
"fieldname": "steps",
"fieldtype": "Table",
"options": "Tax Year Closing Step",
"hidden": 1
},
{
"fieldname": "step_detail_tab",
"fieldtype": "Tab Break",
"label": "Current Step"
},
{
"fieldname": "current_step_html",
"fieldtype": "HTML"
},
{
"fieldname": "summary_tab",
"fieldtype": "Tab Break",
"label": "Summary"
},
{
"fieldname": "summary_html",
"fieldtype": "HTML"
},
{
"fieldname": "total_taxes_created",
"fieldtype": "Currency",
"label": "Total Taxes (Entries Created)",
"precision": "2",
"read_only": 1,
"bold": 1
},
{
"fieldname": "column_break_st1",
"fieldtype": "Column Break"
},
{
"fieldname": "total_payroll_collected",
"fieldtype": "Currency",
"label": "Total Payroll (Data Only)",
"precision": "2",
"read_only": 1
},
{
"fieldname": "column_break_st2",
"fieldtype": "Column Break"
},
{
"fieldname": "grand_total",
"fieldtype": "Currency",
"label": "Grand Total Tax Burden",
"precision": "2",
"read_only": 1,
"bold": 1
},
{
"fieldname": "log_tab",
"fieldtype": "Tab Break",
"label": "Log"
},
{
"fieldname": "log_html",
"fieldtype": "HTML"
},
{
"fieldname": "log_entries",
"fieldtype": "Table",
"options": "Tax Closing Log Entry",
"hidden": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Tax Year Closing Wizard",
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-04-01 20:00:00.000000",
"modified_by": "Administrator",
"module": "Taxes Az",
"name": "Tax Year Closing Wizard",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"cancel": 1, "create": 1, "delete": 1, "email": 1, "export": 1,
"print": 1, "read": 1, "report": 1, "role": "System Manager",
"share": 1, "submit": 1, "write": 1
},
{
"cancel": 1, "create": 1, "delete": 1, "email": 1, "export": 1,
"print": 1, "read": 1, "report": 1, "role": "Accounts Manager",
"share": 1, "submit": 1, "write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

File diff suppressed because it is too large Load Diff

1101
taxes_az/translations/az.csv Normal file

File diff suppressed because it is too large Load Diff

1207
taxes_az/translations/ru.csv Normal file

File diff suppressed because it is too large Load Diff