e-taxes settings is single doctype now

This commit is contained in:
Ali 2026-03-03 17:03:52 +04:00
parent 0b46271628
commit 5ee0badd00
3 changed files with 61 additions and 104 deletions

View File

@ -75,14 +75,13 @@ 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
from invoice_az.migrate_utils import migrate_etaxes_settings_to_single
setup_token_renewal() setup_token_renewal()
migrate_etaxes_settings_to_single()
# Fixtures for master data # Fixtures for master data
# ------------------------ # ------------------------

View File

@ -1,20 +1,9 @@
""" """
Migration utilities for E-Taxes Settings Single doctype conversion. Migration utilities for E-Taxes Settings Single doctype.
backup_etaxes_settings() is called by the before_migrate hook at that point
the doctype is still a regular doctype and ORM works normally.
restore_etaxes_settings() is called by the patch at that point the doctype
is already Single, so we use raw SQL and restore everything from the backup file.
""" """
import json
import os
import frappe import frappe
BACKUP_FILENAME = "etaxes_settings_migration.json"
SCALAR_FIELDS = [ SCALAR_FIELDS = [
"similarity_threshold", "similarity_threshold",
"consider_azeri_chars", "consider_azeri_chars",
@ -26,83 +15,56 @@ SCALAR_FIELDS = [
"default_payment_terms", "default_payment_terms",
] ]
# SQL table name → parentfield name in E-Taxes Settings CHILD_TABLES = [
CHILD_TABLES = { "tabE-Taxes Item Mapping",
"tabE-Taxes Item Mapping": "item_mappings", "tabE-Taxes Customer Mappings",
"tabE-Taxes Customer Mappings": "customer_mappings", "tabE-Taxes Supplier Mappings",
"tabE-Taxes Supplier Mappings": "supplier_mappings", "tabE-Taxes Unit Mapping",
"tabE-Taxes Unit Mapping": "unit_mappings", "tabE-Taxes VAT Account Mapping",
"tabE-Taxes VAT Account Mapping": "vat_account_mappings", ]
}
def _backup_path(): def migrate_etaxes_settings_to_single():
return frappe.get_site_path("private", BACKUP_FILENAME)
def backup_etaxes_settings():
""" """
Called before_migrate (before schema sync). Called from after_migrate hook.
Dumps scalar fields + all child table rows to a JSON file. Reads the old tabE-Taxes Settings (Frappe never drops old tables during sync)
and writes scalar fields to tabSingles + re-parents child table rows.
Safe to call multiple times skips if tabSingles already has data.
""" """
# Skip if already migrated
already_migrated = frappe.db.sql(
"SELECT COUNT(*) as cnt FROM `tabSingles` WHERE doctype = 'E-Taxes Settings'",
as_dict=True,
)
if already_migrated and already_migrated[0].get("cnt", 0) > 0:
return
# Old table must exist
if not frappe.db.table_exists("tabE-Taxes Settings"): if not frappe.db.table_exists("tabE-Taxes Settings"):
return return
# Find the best old record — ORM still works normally here # Get the old record — try any row (old name could even be "E-Taxes Settings")
rows = None old_row = None
for query in [ for query in [
"SELECT * FROM `tabE-Taxes Settings` WHERE name != 'E-Taxes Settings' ORDER BY modified DESC LIMIT 1", "SELECT * FROM `tabE-Taxes Settings` WHERE name != 'E-Taxes Settings' LIMIT 1",
"SELECT * FROM `tabE-Taxes Settings` ORDER BY modified DESC LIMIT 1", "SELECT * FROM `tabE-Taxes Settings` LIMIT 1",
]: ]:
try: try:
rows = frappe.db.sql(query, as_dict=True) rows = frappe.db.sql(query, as_dict=True)
if rows:
old_row = rows[0]
break
except Exception: except Exception:
pass pass
if rows:
break
if not rows: if not old_row:
return return
old_row = rows[0] old_name = old_row.get("name")
old_name = old_row["name"]
backup = { # Write scalar fields to tabSingles
"old_name": old_name, for field in SCALAR_FIELDS:
"scalar": {f: old_row.get(f) for f in SCALAR_FIELDS}, value = old_row.get(field)
"children": {},
}
# Save ALL columns of each child table row so we can re-insert them if needed
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 backup file writes scalar fields to tabSingles
deletes + re-inserts child table rows 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)
# --- Scalar fields → tabSingles ---
for field, value in backup.get("scalar", {}).items():
if value is None: if value is None:
continue continue
frappe.db.sql( frappe.db.sql(
@ -114,29 +76,36 @@ def restore_etaxes_settings():
(field, str(value)), (field, str(value)),
) )
# --- Child tables: delete existing rows, then re-insert from backup --- # Restore child table rows
for table, rows in backup.get("children", {}).items(): for table in CHILD_TABLES:
if not frappe.db.table_exists(table) or not rows: if not frappe.db.table_exists(table):
continue
try:
child_rows = frappe.db.sql(
f"SELECT * FROM `{table}` WHERE parent = %s ORDER BY idx",
(old_name,),
as_dict=True,
)
except Exception:
continue continue
# Remove any rows already parented to the Single (e.g., from a broken previous run) if not child_rows:
frappe.db.sql( continue
f"DELETE FROM `{table}` WHERE parent = 'E-Taxes Settings'"
)
for row in rows: frappe.db.sql(f"DELETE FROM `{table}` WHERE parent = 'E-Taxes Settings'")
for row in child_rows:
row["parent"] = "E-Taxes Settings" row["parent"] = "E-Taxes Settings"
row["parenttype"] = "E-Taxes Settings" row["parenttype"] = "E-Taxes Settings"
columns = ", ".join(f"`{col}`" for col in row) columns = ", ".join(f"`{col}`" for col in row)
placeholders = ", ".join(["%s"] * len(row)) placeholders = ", ".join(["%s"] * len(row))
try:
frappe.db.sql( frappe.db.sql(
f"INSERT INTO `{table}` ({columns}) VALUES ({placeholders})", f"INSERT INTO `{table}` ({columns}) VALUES ({placeholders})",
list(row.values()), list(row.values()),
) )
except Exception:
pass
frappe.db.commit() frappe.db.commit()
frappe.clear_cache(doctype="E-Taxes Settings") frappe.clear_cache(doctype="E-Taxes Settings")
os.remove(path)

View File

@ -1,18 +1,7 @@
"""
Migrate E-Taxes Settings from multi-record to Single doctype.
Data is backed up to a JSON file by the before_migrate hook
(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
def execute(): def execute():
from invoice_az.migrate_utils import restore_etaxes_settings from invoice_az.migrate_utils import migrate_etaxes_settings_to_single
restore_etaxes_settings() migrate_etaxes_settings_to_single()