diff --git a/invoice_az/hooks.py b/invoice_az/hooks.py index a587a2b..ff61ecd 100644 --- a/invoice_az/hooks.py +++ b/invoice_az/hooks.py @@ -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 # ------------------------ diff --git a/invoice_az/migrate_utils.py b/invoice_az/migrate_utils.py index d3db114..a0cf997 100644 --- a/invoice_az/migrate_utils.py +++ b/invoice_az/migrate_utils.py @@ -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) diff --git a/invoice_az/patches/v1_0/migrate_etaxes_settings_to_single.py b/invoice_az/patches/v1_0/migrate_etaxes_settings_to_single.py index d58e77d..5de7f03 100644 --- a/invoice_az/patches/v1_0/migrate_etaxes_settings_to_single.py +++ b/invoice_az/patches/v1_0/migrate_etaxes_settings_to_single.py @@ -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()