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()
before_migrate = ["invoice_az.migrate_utils.backup_etaxes_settings"]
def after_migrate():
"""Run migration tasks"""
from invoice_az.auth import setup_token_renewal
from invoice_az.migrate_utils import migrate_etaxes_settings_to_single
setup_token_renewal()
migrate_etaxes_settings_to_single()
# Fixtures for master data
# ------------------------

View File

@ -1,20 +1,9 @@
"""
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.
Migration utilities for E-Taxes Settings Single doctype.
"""
import json
import os
import frappe
BACKUP_FILENAME = "etaxes_settings_migration.json"
SCALAR_FIELDS = [
"similarity_threshold",
"consider_azeri_chars",
@ -26,83 +15,56 @@ SCALAR_FIELDS = [
"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",
}
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():
def migrate_etaxes_settings_to_single():
"""
Called before_migrate (before schema sync).
Dumps scalar fields + all child table rows to a JSON file.
Called from after_migrate hook.
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"):
return
# Find the best old record — ORM still works normally here
rows = None
# Get the old record — try any row (old name could even be "E-Taxes Settings")
old_row = 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",
"SELECT * FROM `tabE-Taxes Settings` WHERE name != 'E-Taxes Settings' LIMIT 1",
"SELECT * FROM `tabE-Taxes Settings` LIMIT 1",
]:
try:
rows = frappe.db.sql(query, as_dict=True)
if rows:
old_row = rows[0]
break
except Exception:
pass
if rows:
break
if not rows:
if not old_row:
return
old_row = rows[0]
old_name = old_row["name"]
old_name = old_row.get("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():
# Write scalar fields to tabSingles
for field in SCALAR_FIELDS:
value = old_row.get(field)
if value is None:
continue
frappe.db.sql(
@ -114,29 +76,36 @@ def restore_etaxes_settings():
(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:
# Restore child table rows
for table in CHILD_TABLES:
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
# 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'"
)
if not child_rows:
continue
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["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()),
)
try:
frappe.db.sql(
f"INSERT INTO `{table}` ({columns}) VALUES ({placeholders})",
list(row.values()),
)
except Exception:
pass
frappe.db.commit()
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
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()