From 74bd74b7ba06b3f71fbd3ffd6ffc9dd029b193d8 Mon Sep 17 00:00:00 2001 From: Ali <010109ali@gmail.com> Date: Tue, 3 Mar 2026 16:32:44 +0400 Subject: [PATCH] e-taxes settings is single doctype now --- invoice_az/hooks.py | 3 + invoice_az/migrate_utils.py | 132 ++++++++++++++++++ .../v1_0/migrate_etaxes_settings_to_single.py | 90 ++---------- 3 files changed, 143 insertions(+), 82 deletions(-) create mode 100644 invoice_az/migrate_utils.py diff --git a/invoice_az/hooks.py b/invoice_az/hooks.py index 7e393e3..a587a2b 100644 --- a/invoice_az/hooks.py +++ b/invoice_az/hooks.py @@ -75,6 +75,9 @@ 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 diff --git a/invoice_az/migrate_utils.py b/invoice_az/migrate_utils.py new file mode 100644 index 0000000..ebd5ed3 --- /dev/null +++ b/invoice_az/migrate_utils.py @@ -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) 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 d88ebd2..d58e77d 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,92 +1,18 @@ """ Migrate E-Taxes Settings from multi-record to Single doctype. -IMPORTANT: patches run after schema sync, so Frappe's ORM already treats -E-Taxes Settings as a Single. All reads from the old table use raw SQL. +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 -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(): - if not frappe.db.table_exists("tabE-Taxes Settings"): - return + from invoice_az.migrate_utils import restore_etaxes_settings - # Find the best old record via raw SQL. - # 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 + restore_etaxes_settings()