e-taxes settings is single doctype now

This commit is contained in:
Ali 2026-03-03 16:32:44 +04:00
parent 61ec366149
commit 74bd74b7ba
3 changed files with 143 additions and 82 deletions

View File

@ -75,6 +75,9 @@ def after_install():
setup_token_renewal() setup_token_renewal()
before_migrate = ["invoice_az.migrate_utils.backup_etaxes_settings"]
def after_migrate(): def after_migrate():
"""Run migration tasks""" """Run migration tasks"""
from invoice_az.auth import setup_token_renewal from invoice_az.auth import setup_token_renewal

132
invoice_az/migrate_utils.py Normal file
View File

@ -0,0 +1,132 @@
"""
Migration utilities for E-Taxes Settings Single doctype conversion.
backup_etaxes_settings() is called by the before_migrate hook at that point
the doctype is still a regular doctype, so frappe.db works normally.
restore_etaxes_settings() is called by the patch at that point the doctype
is already Single, so we read from the backup file and write via raw SQL.
"""
import json
import os
import frappe
BACKUP_FILENAME = "etaxes_settings_migration.json"
SCALAR_FIELDS = [
"similarity_threshold",
"consider_azeri_chars",
"default_item_group",
"default_uom",
"default_item_tax_template",
"default_supplier_group",
"default_customer_group",
"default_payment_terms",
]
CHILD_TABLES = [
"tabE-Taxes Item Mapping",
"tabE-Taxes Customer Mappings",
"tabE-Taxes Supplier Mappings",
"tabE-Taxes Unit Mapping",
"tabE-Taxes VAT Account Mapping",
]
def _backup_path():
return frappe.get_site_path("private", BACKUP_FILENAME)
def backup_etaxes_settings():
"""
Called before_migrate.
Dumps the active E-Taxes Settings record (scalar fields + child rows) to a JSON file.
"""
if not frappe.db.table_exists("tabE-Taxes Settings"):
return
# Find the best record — ORM still works normally here
rows = frappe.db.sql(
"""
SELECT * FROM `tabE-Taxes Settings`
WHERE name != 'E-Taxes Settings'
ORDER BY is_default DESC, is_active DESC, modified DESC
LIMIT 1
""",
as_dict=True,
)
if not rows:
return
old_row = rows[0]
old_name = old_row["name"]
backup = {
"old_name": old_name,
"scalar": {f: old_row.get(f) for f in SCALAR_FIELDS},
"children": {},
}
for table in CHILD_TABLES:
if frappe.db.table_exists(table):
child_rows = frappe.db.sql(
f"SELECT * FROM `{table}` WHERE parent = %s ORDER BY idx",
(old_name,),
as_dict=True,
)
backup["children"][table] = [dict(r) for r in child_rows]
with open(_backup_path(), "w", encoding="utf-8") as f:
json.dump(backup, f, default=str, ensure_ascii=False, indent=2)
def restore_etaxes_settings():
"""
Called from the migration patch (after schema sync).
Reads the backup file, writes scalar fields into tabSingles,
re-parents child rows, then deletes the file.
"""
path = _backup_path()
if not os.path.exists(path):
return
with open(path, encoding="utf-8") as f:
backup = json.load(f)
# Write scalar fields into tabSingles
for field, value in backup.get("scalar", {}).items():
if value is None:
continue
frappe.db.sql(
"""
INSERT INTO `tabSingles` (doctype, field, value)
VALUES ('E-Taxes Settings', %s, %s)
ON DUPLICATE KEY UPDATE value = VALUES(value)
""",
(field, str(value)),
)
# Re-parent child rows
old_name = backup.get("old_name")
if old_name:
for table in CHILD_TABLES:
if frappe.db.table_exists(table):
frappe.db.sql(
f"""
UPDATE `{table}`
SET parent = 'E-Taxes Settings',
parenttype = 'E-Taxes Settings'
WHERE parent = %s
""",
(old_name,),
)
frappe.db.commit()
frappe.clear_cache(doctype="E-Taxes Settings")
# Clean up — file is no longer needed
os.remove(path)

View File

@ -1,92 +1,18 @@
""" """
Migrate E-Taxes Settings from multi-record to Single doctype. Migrate E-Taxes Settings from multi-record to Single doctype.
IMPORTANT: patches run after schema sync, so Frappe's ORM already treats Data is backed up to a JSON file by the before_migrate hook
E-Taxes Settings as a Single. All reads from the old table use raw SQL. (invoice_az.migrate_utils.backup_etaxes_settings) which runs before schema
changes at that point the ORM still works normally.
This patch runs after schema sync (when the doctype is already Single)
and restores the data from the backup file via raw SQL.
""" """
import frappe import frappe
SCALAR_FIELDS = [
"similarity_threshold",
"consider_azeri_chars",
"default_item_group",
"default_uom",
"default_item_tax_template",
"default_supplier_group",
"default_customer_group",
"default_payment_terms",
]
CHILD_TABLES = [
"tabE-Taxes Item Mapping",
"tabE-Taxes Customer Mappings",
"tabE-Taxes Supplier Mappings",
"tabE-Taxes Unit Mapping",
"tabE-Taxes VAT Account Mapping",
]
def execute(): def execute():
if not frappe.db.table_exists("tabE-Taxes Settings"): from invoice_az.migrate_utils import restore_etaxes_settings
return
# Find the best old record via raw SQL. restore_etaxes_settings()
# Frappe ORM treats the doctype as Single by now, so we must bypass it.
# Prefer is_default=1, then is_active=1, then any record with a non-standard name.
# Wrap each attempt in try/except in case old columns were already dropped.
old_row = None
for query in [
"SELECT * FROM `tabE-Taxes Settings` WHERE `is_default` = 1 LIMIT 1",
"SELECT * FROM `tabE-Taxes Settings` WHERE `is_active` = 1 LIMIT 1",
"SELECT * FROM `tabE-Taxes Settings` WHERE `name` != 'E-Taxes Settings' LIMIT 1",
]:
try:
old_row = _raw_get(query)
except Exception:
pass
if old_row:
break
if not old_row:
return # Fresh install or already migrated
old_name = old_row.get("name")
# Write scalar fields into tabSingles via raw SQL
for field in SCALAR_FIELDS:
value = old_row.get(field)
if value is None:
continue
frappe.db.sql(
"""
INSERT INTO `tabSingles` (doctype, field, value)
VALUES ('E-Taxes Settings', %s, %s)
ON DUPLICATE KEY UPDATE value = VALUES(value)
""",
(field, str(value)),
)
# Re-parent child table rows from old name → "E-Taxes Settings"
for table in CHILD_TABLES:
if not frappe.db.table_exists(table):
continue
frappe.db.sql(
f"""
UPDATE `{table}`
SET parent = 'E-Taxes Settings',
parenttype = 'E-Taxes Settings'
WHERE parent = %s
""",
(old_name,),
)
frappe.db.commit()
frappe.clear_cache(doctype="E-Taxes Settings")
def _raw_get(query):
rows = frappe.db.sql(query, as_dict=True)
return rows[0] if rows else None