From 752c1ebe597559e556105c5cb721b09df2aedc3e Mon Sep 17 00:00:00 2001 From: git_admin Date: Fri, 1 May 2026 15:01:01 +0000 Subject: [PATCH] Tower: upload laundry_management 19.0.19.0.4 (via marketplace) --- .../migrations/19.0.11.0.0/pre_migrate.py | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 addons/laundry_management/migrations/19.0.11.0.0/pre_migrate.py diff --git a/addons/laundry_management/migrations/19.0.11.0.0/pre_migrate.py b/addons/laundry_management/migrations/19.0.11.0.0/pre_migrate.py new file mode 100644 index 0000000..ff0e8a5 --- /dev/null +++ b/addons/laundry_management/migrations/19.0.11.0.0/pre_migrate.py @@ -0,0 +1,228 @@ +""" +Pre-migration script for laundry_management 19.0.11.0.0 + +Architecture change: standalone laundry.order / laundry.order.line / laundry.payment +models are replaced by _inherit = 'sale.order' / 'sale.order.line'. + +This script runs BEFORE Odoo's model sync so that: +1. FK constraints from old wizard tables referencing laundry_order are dropped +2. Old wizard transient records are purged (they reference non-existent rows) +3. Stale ir.model.fields records pointing to old models are removed +4. Old ir.model entries are deleted (unblocking the ORM delete check) +5. Old physical tables are dropped + +Without this, the ORM raises: + "Another model is using the record you are trying to delete. + Blocking model: Laundry Print Wizard (laundry.print.wizard), + Blocking field: order_id" +""" + +import logging + +_logger = logging.getLogger(__name__) + + +# Old standalone models being removed in this version +_OLD_MODELS = [ + 'laundry.order', + 'laundry.order.line', + 'laundry.payment', + 'laundry.order.line.addon', + 'laundry.register.payment.wizard', + 'laundry.product.wizard', + 'laundry.product.wizard.line', + 'laundry.category', + 'laundry.item.type', +] + +# Physical tables that correspond to the old models +_OLD_TABLES = [ + 'laundry_order', + 'laundry_order_line', + 'laundry_payment', + 'laundry_order_line_addon', + 'laundry_register_payment_wizard', + 'laundry_product_wizard', + 'laundry_product_wizard_line', + 'laundry_category', + 'laundry_item_type', +] + +# Transient/wizard tables that may hold rows with FK refs to laundry_order +_WIZARD_TABLES = [ + 'laundry_print_wizard', + 'laundry_session_wizard', + 'laundry_whatsapp_wizard', + 'laundry_register_payment_wizard', + 'laundry_product_wizard', + 'laundry_product_wizard_line', +] + + +def _table_exists(cr, table): + cr.execute( + "SELECT 1 FROM information_schema.tables " + "WHERE table_schema = 'public' AND table_name = %s", + (table,), + ) + return bool(cr.fetchone()) + + +def _column_exists(cr, table, column): + cr.execute( + "SELECT 1 FROM information_schema.columns " + "WHERE table_schema = 'public' AND table_name = %s AND column_name = %s", + (table, column), + ) + return bool(cr.fetchone()) + + +def _drop_fk_constraints_referencing(cr, referenced_table): + """Drop all FK constraints in the DB that point at referenced_table.""" + cr.execute( + """ + SELECT tc.table_name, tc.constraint_name + FROM information_schema.table_constraints tc + JOIN information_schema.referential_constraints rc + ON tc.constraint_name = rc.constraint_name + AND tc.table_schema = rc.constraint_schema + JOIN information_schema.table_constraints tc2 + ON rc.unique_constraint_name = tc2.constraint_name + AND rc.unique_constraint_schema = tc2.table_schema + WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc2.table_name = %s + AND tc.table_schema = 'public' + """, + (referenced_table,), + ) + rows = cr.fetchall() + for src_table, constraint_name in rows: + _logger.info( + 'pre_migrate: dropping FK constraint %s on %s (referenced %s)', + constraint_name, src_table, referenced_table, + ) + cr.execute( + 'ALTER TABLE "%s" DROP CONSTRAINT IF EXISTS "%s"' % + (src_table, constraint_name) + ) + + +def migrate(cr, version): + if not version: + # Fresh install — nothing to clean up + return + + _logger.info('pre_migrate laundry_management %s → 19.0.11.0.0 : start', version) + + # ── Step 1: Purge all transient wizard records ──────────────────────────── + # TransientModel rows expire naturally but DB rows persist during upgrade; + # they hold FK refs that block constraint drops and table drops. + for tbl in _WIZARD_TABLES: + if _table_exists(cr, tbl): + _logger.info('pre_migrate: truncating wizard table %s', tbl) + cr.execute('TRUNCATE TABLE "%s" CASCADE' % tbl) + + # ── Step 2: Drop FK constraints pointing at old tables ──────────────────── + for tbl in _OLD_TABLES: + if _table_exists(cr, tbl): + _drop_fk_constraints_referencing(cr, tbl) + + # Also drop FKs on wizard tables that point at laundry_order (the primary blocker) + # Example: laundry_print_wizard.order_id -> laundry_order.id + for wizard_tbl in _WIZARD_TABLES: + if not _table_exists(cr, wizard_tbl): + continue + # Find and drop any FK on this wizard table that references an old table + cr.execute( + """ + SELECT kcu.column_name, ccu.table_name AS foreign_table_name, tc.constraint_name + FROM information_schema.table_constraints AS tc + JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage AS ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema + WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema = 'public' + AND tc.table_name = %s + AND ccu.table_name = ANY(%s) + """, + (wizard_tbl, _OLD_TABLES), + ) + for col, ref_tbl, constraint_name in cr.fetchall(): + _logger.info( + 'pre_migrate: dropping FK %s on %s.%s -> %s', + constraint_name, wizard_tbl, col, ref_tbl, + ) + cr.execute( + 'ALTER TABLE "%s" DROP CONSTRAINT IF EXISTS "%s"' % + (wizard_tbl, constraint_name) + ) + + # ── Step 3: Remove stale ir.model.fields that ref old models ───────────── + # These are the records that cause "Blocking model: laundry.print.wizard, + # Blocking field: order_id" — the field record itself still has + # relation = 'laundry.order', which makes the ORM think laundry.print.wizard + # still depends on laundry.order. + cr.execute( + """ + DELETE FROM ir_model_fields + WHERE relation = ANY(%s) + """, + (_OLD_MODELS,), + ) + deleted = cr.rowcount + _logger.info('pre_migrate: deleted %d stale ir.model.fields rows', deleted) + + # Also remove fields whose model itself is one of the old models + cr.execute( + """ + DELETE FROM ir_model_fields + WHERE model IN %s + """, + (tuple(_OLD_MODELS),), + ) + _logger.info('pre_migrate: deleted %d ir.model.fields for old models', cr.rowcount) + + # ── Step 4: Remove stale ir.model.fields_by_name cache ─────────────────── + # In some Odoo versions there is a separate constraint/index table. + # Safe to attempt; ignore if table doesn't exist. + if _table_exists(cr, 'ir_model_constraint'): + cr.execute( + """ + DELETE FROM ir_model_constraint imc + USING ir_model im + WHERE imc.model = im.id + AND im.model = ANY(%s) + """, + (_OLD_MODELS,), + ) + _logger.info('pre_migrate: removed %d ir.model.constraint rows', cr.rowcount) + + if _table_exists(cr, 'ir_model_relation'): + cr.execute( + """ + DELETE FROM ir_model_relation imr + USING ir_model im + WHERE imr.model = im.id + AND im.model = ANY(%s) + """, + (_OLD_MODELS,), + ) + _logger.info('pre_migrate: removed %d ir.model.relation rows', cr.rowcount) + + # ── Step 5: Remove ir.model entries for the old models ─────────────────── + cr.execute( + "DELETE FROM ir_model WHERE model = ANY(%s)", + (_OLD_MODELS,), + ) + _logger.info('pre_migrate: deleted %d ir.model rows', cr.rowcount) + + # ── Step 6: Drop old physical tables ───────────────────────────────────── + for tbl in reversed(_OLD_TABLES): # reverse to respect FK order + if _table_exists(cr, tbl): + _logger.info('pre_migrate: dropping table %s', tbl) + cr.execute('DROP TABLE IF EXISTS "%s" CASCADE' % tbl) + + _logger.info('pre_migrate laundry_management: complete')