invoice_az/invoice_az/migrate_utils.py

143 lines
3.7 KiB
Python

"""
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 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
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",
]
# SQL table name → parentfield name in E-Taxes Settings
CHILD_TABLES = {
"tabE-Taxes Item Mapping": "item_mappings",
"tabE-Taxes Customer Mappings": "customer_mappings",
"tabE-Taxes Supplier Mappings": "supplier_mappings",
"tabE-Taxes Unit Mapping": "unit_mappings",
"tabE-Taxes VAT Account Mapping": "vat_account_mappings",
}
def _backup_path():
return frappe.get_site_path("private", BACKUP_FILENAME)
def backup_etaxes_settings():
"""
Called before_migrate (before schema sync).
Dumps scalar fields + all child table rows to a JSON file.
"""
if not frappe.db.table_exists("tabE-Taxes Settings"):
return
# Find the best old record — ORM still works normally here
rows = None
for query in [
"SELECT * FROM `tabE-Taxes Settings` WHERE name != 'E-Taxes Settings' ORDER BY modified DESC LIMIT 1",
"SELECT * FROM `tabE-Taxes Settings` ORDER BY modified DESC LIMIT 1",
]:
try:
rows = frappe.db.sql(query, as_dict=True)
except Exception:
pass
if rows:
break
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": {},
}
# 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:
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)),
)
# --- Child tables: delete existing rows, then re-insert from backup ---
for table, rows in backup.get("children", {}).items():
if not frappe.db.table_exists(table) or not rows:
continue
# Remove any rows already parented to the Single (e.g., from a broken previous run)
frappe.db.sql(
f"DELETE FROM `{table}` WHERE parent = 'E-Taxes Settings'"
)
for row in rows:
row["parent"] = "E-Taxes Settings"
row["parenttype"] = "E-Taxes Settings"
columns = ", ".join(f"`{col}`" for col in row)
placeholders = ", ".join(["%s"] * len(row))
frappe.db.sql(
f"INSERT INTO `{table}` ({columns}) VALUES ({placeholders})",
list(row.values()),
)
frappe.db.commit()
frappe.clear_cache(doctype="E-Taxes Settings")
os.remove(path)