add cash method
This commit is contained in:
parent
e86e8c841b
commit
6afb8ca3d3
|
|
@ -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()
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TaxClosingAccountMap(Document):
|
||||
pass
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TaxClosingBenefit(Document):
|
||||
pass
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TaxClosingEntryPreview(Document):
|
||||
pass
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TaxClosingJournalEntry(Document):
|
||||
pass
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TaxClosingLogEntry(Document):
|
||||
pass
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TaxClosingPayrollSummary(Document):
|
||||
pass
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TaxClosingTaxItem(Document):
|
||||
pass
|
||||
|
|
@ -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 => ` • ${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"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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>
|
||||
| ${__("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";
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TaxRateRow(Document):
|
||||
pass
|
||||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TaxYearClosingStep(Document):
|
||||
pass
|
||||
|
|
@ -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(); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue