Compare commits

...

7 Commits

Author SHA1 Message Date
Stéphan Sainléger
1d75a0f9db [CLN] finalize_db: purge `ir.ui.view images orphaned and unreferenced in any arch_db`
Delete image attachments linked to deleted ``ir.ui.view`` records only when
no other view's HTML references them via ``/web/image/<id>`` or
``/web/content/<id>``.  This avoids the aggressive approach of deleting all
``res_id = 0`` attachments, which can break website images that survived
their parent view's deletion but are still embedded in live pages.

The ``NOT EXISTS`` subquery scans all ``arch_db`` columns and is expensive
but runs once at migration time.
2026-08-11 09:52:51 +02:00
Stéphan Sainléger
134b935fa4 [ADD] finalize_db: run `_gc_file_store_unsafe` after attachment cleanup
Add ``lib/python/file_gc.py``, a minimal Odoo-shell script that calls
``ir.attachment._gc_file_store_unsafe()`` to remove filestore files
no longer referenced by any ``ir_attachment`` record.

Invoke it in ``finalize_db.sh`` after the SQL attachment purges so
that the deleted CSS/JS asset bundles, resized partner thumbnails,
and orphaned ``ir_attachment`` rows also have their backing files
cleaned from disk.
2026-08-10 17:07:08 +02:00
Stéphan Sainléger
b6fd05104a [CLN] finalize_db: purge resized partner images and orphaned `ir_attachment`
Delete the four thumbnail sizes (``image_128``, ``image_256``,
``image_512``, ``image_1024``) from ``ir_attachment`` for
``res.partner`` records.  Odoo 13+ regenerates all thumbnails
automatically from ``image_1920`` on the next partner write.

Also delete attachments whose ``res_model`` is ``NULL`` (orphaned
``factur-x.xml``, SCSS, ICS files) — these have no associated record
and consume ~48 MB of filestore space.
2026-08-10 17:06:34 +02:00
Stéphan Sainléger
8cb2fe5fd2 [CLN] finalize_db: purge `ir_logging`
Truncate the ``ir_logging`` table which accumulates Python logger
messages (63 MB of ``INFO``-level ``ir.actions.server`` traces) with
no business value after migration.
2026-08-10 17:06:14 +02:00
Stéphan Sainléger
7c680b17a4 [REF] pre_upgrade: rename `account_factoring to account_factoring_oca` for 18.0
``account_factoring`` (depends on ``account_payment_partner`` +
``account_payment_mode``) is replaced by ``account_factoring_oca``
(depends on ``account_payment_base_oca`` +
``account_payment_base_oca_sale``) in v18. Both modules share rigorously
identical stored fields on ``account_journal``, ``account_move`` and
``res_partner``, so a rename (not an uninstall) preserves the 4910
invoice factor statuses, 311 partner credit limits and journal factor
config; OpenUpgrade then treats ``account_factoring_oca`` as already
installed and adopts the existing columns via ORM ``_auto_init``.

Redirects dependencies pointing to the old name, drops the obsolete
``account_payment_partner``/``account_payment_mode`` dependencies and
inserts the new OCA equivalents. Must run AFTER the bank-payment
renaming block which creates ``account_payment_base_oca`` in the DB.
The orphaned ``base.module_account_factoring`` xmlid is renamed so
OpenUpgrade can rediscover the physical ``account_factoring`` addon dir
without conflicting on ``(base, module_account_factoring)``.
2026-08-07 10:36:12 +02:00
Stéphan Sainléger
803ae858ea [REM] pre_upgrade: delete obsolete `contract_payment_mode` before 18.0 load
``contract_payment_mode`` v18 still depends on ``account_payment_partner``
(removed by the bank-payment renaming) and uses the obsolete
``account.payment.mode`` model; no v18 replacement provides contract
integration, and only 2 contracts carried a ``payment_mode_id``.

Must run BEFORE the bank-payment renaming and BEFORE OpenUpgrade's
``button_upgrade()`` which would otherwise crash on the missing
dependency.

The module is DELETED (module row + ``ir_model_data`` + dependencies)
rather than marked 'to remove', because Odoo's "Transient module states
were reset" in ``load_modules()`` converts 'to remove' -> 'installed',
making ``button_upgrade()`` re-parse the missing
``account_payment_partner`` dependency and abort the registry load. The
orphaned ``base.module_contract_payment_mode`` xmlid is renamed so
``update_list()`` rediscovers the physical addon as a fresh uninstalled
module that ``button_upgrade()`` never touches.
2026-08-07 10:35:59 +02:00
Stéphan Sainléger
6890d19524 [FIX] pre_upgrade: drop orphan `theme.ir.ui.view` records before 17.0 load
The migrated 16.0 database still holds ``theme_ir_ui_view`` rows whose
``inherit_id`` points to ``ir.ui.view`` ids removed by the 17.0 asset
bundle rework (``web._assets_utils``, ``website.assets_editor``,
``website._assets_frontend_helpers`` replaced by the ``ir.asset`` model).

When ``website._theme_load`` runs during the registry load,
``ThemeView._convert_to_base_model`` accesses ``inherit.website_id`` on
the ghost record and raises ``MissingError``, aborting the whole
registry build.

Delete the ``ir_model_data`` + ``theme_ir_ui_view`` rows whose
``inherit_id`` targets a missing ``ir.ui.view``. Generic (covers any
theme), idempotent, scoped to ``theme_ir_ui_view`` only; no
``ir.ui.view`` copies exist for the affected templates.
2026-08-07 10:35:34 +02:00
4 changed files with 250 additions and 0 deletions

1
lib/python/file_gc.py Normal file
View File

@@ -0,0 +1 @@
env['ir.attachment']._gc_file_store_unsafe()

View File

@@ -23,6 +23,58 @@ EOF
)
query_postgres_container "$CLEANUP_SQL" "$DB_NAME"
# ────────────────────────────────────────────────────────────
# Purge orphaned ``ir.ui.view`` images whose parent view was
# deleted AND whose ID is not referenced in any other view's
# HTML (``arch_db``). An image with ``res_id = 0`` can still
# be used by a live page via ``/web/image/<id>``, so we only
# remove it when no ``arch_db`` contains that URL.
# ────────────────────────────────────────────────────────────
echo "Purging ir.ui.view images orphaned of their parent view and unreferenced in any arch_db..."
PURGE_VIEW_IMAGES_SQL=$(cat <<'EOF'
DELETE FROM ir_attachment a
WHERE a.res_model = 'ir.ui.view'
AND a.mimetype LIKE 'image/%'
AND a.store_fname IS NOT NULL
AND a.res_id NOT IN (SELECT id FROM ir_ui_view)
AND NOT EXISTS (
SELECT 1 FROM ir_ui_view v
WHERE v.arch_db::text ILIKE '%web/image/' || a.id || '%'
OR v.arch_db::text ILIKE '%web/content/' || a.id || '%'
);
EOF
)
query_postgres_container "$PURGE_VIEW_IMAGES_SQL" "$DB_NAME"
# ────────────────────────────────────────────────────────────
# Purge ir_logging (Python logger messages, no business value).
# ────────────────────────────────────────────────────────────
echo "Purging ir_logging..."
query_postgres_container "TRUNCATE ir_logging;" "$DB_NAME"
# ────────────────────────────────────────────────────────────
# Purge resized partner images (128, 256, 512, 1024) and
# attachments with no res_model (orphaned factur-x, CSS, ICS).
# Odoo 13+ regenerates thumbnails automatically from
# image_1920 on the next partner write, so only the full-size
# source needs to be kept.
# ────────────────────────────────────────────────────────────
echo "Purging resized partner images and orphaned attachments..."
PURGE_ATTACHMENTS_SQL=$(cat <<'EOF'
DELETE FROM ir_attachment
WHERE res_model = 'res.partner'
AND (name ILIKE '%image_128%'
OR name ILIKE '%image_256%'
OR name ILIKE '%image_512%'
OR name ILIKE '%image_1024%');
DELETE FROM ir_attachment
WHERE (res_model IS NULL OR res_model = '')
AND store_fname IS NOT NULL;
EOF
)
query_postgres_container "$PURGE_ATTACHMENTS_SQL" "$DB_NAME"
# ────────────────────────────────────────────────────────────
# Fix orphan ir_filters pointing to deleted ir_actions
#
@@ -93,7 +145,15 @@ SET state = 'installed'
WHERE state = 'to upgrade';
" "$DB_NAME" || true
# ────────────────────────────────────────────────────────────
# Clean orphaned filestore files left behind by the attachment
# deletions above. ``_gc_file_store_unsafe`` scans the filestore
# and removes files that no longer have a matching
# ``ir_attachment`` record.
# ────────────────────────────────────────────────────────────
echo "Cleaning orphaned filestore files..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
exec_python_script_in_odoo_shell "$ODOO_SERVICE" "$DB_NAME" "$SCRIPT_DIR/lib/python/file_gc.py"
PYTHON_SCRIPT="${SCRIPT_DIR}/lib/python/fix_duplicated_views.py"
echo "Remove duplicated views with script $PYTHON_SCRIPT ..."

View File

@@ -103,6 +103,69 @@ EOF
echo "SQL command = $PRE_MIGRATE_SQL_4"
query_postgres_container "$PRE_MIGRATE_SQL_4" ou17 || exit 1
# ────────────────────────────────────────────────────────────
# Remove orphan theme.ir.ui.view templates pointing to deleted ir.ui.view
#
# In Odoo 17, the asset bundle system was reworked: `web._assets_utils`,
# `website.assets_editor`, and `website._assets_frontend_helpers` were
# replaced by the new `ir.asset` model. The theme_common/theme_clean 17.0
# modules no longer define those templates, but the migrated database still
# holds the old `theme_ir_ui_view` records (kept by ir_model_data noupdate).
#
# Their `inherit_id` Reference field points to ir.ui.view ids that no longer
# exist. When website's `_theme_load(website)` runs (ir_module_module.py:95)
# during the registry load, `ThemeView._convert_to_base_model`
# (theme_models.py:85) accesses `inherit.website_id` on the ghost record and
# raises MissingError, aborting the whole registry:
#
# odoo.exceptions.MissingError: Record does not exist or has been deleted.
# (Record: ir.ui.view(174,), User: 1)
#
# This deletes the ir_model_data + theme_ir_ui_view rows whose inherit_id
# targets a missing ir.ui.view. Generic (covers any theme, idempotent).
# Scoped to theme_ir_ui_view only. Verified: no ir.ui.view copies exist for
# the affected templates, so no side effects.
# ────────────────────────────────────────────────────────────
PRE_MIGRATE_SQL_5=$(cat <<'EOF'
DO $$
DECLARE
deleted_imd INTEGER;
deleted_tpl INTEGER;
BEGIN
WITH targets AS (
SELECT id FROM theme_ir_ui_view
WHERE inherit_id LIKE 'ir.ui.view,%'
AND NOT EXISTS (
SELECT 1 FROM ir_ui_view v
WHERE v.id = split_part(theme_ir_ui_view.inherit_id, ',', 2)::int
)
),
del AS (
DELETE FROM ir_model_data
WHERE model = 'theme.ir.ui.view'
AND res_id IN (SELECT id FROM targets)
RETURNING id
)
SELECT count(*) INTO deleted_imd FROM del;
WITH del AS (
DELETE FROM theme_ir_ui_view
WHERE inherit_id LIKE 'ir.ui.view,%'
AND NOT EXISTS (
SELECT 1 FROM ir_ui_view v
WHERE v.id = split_part(theme_ir_ui_view.inherit_id, ',', 2)::int
)
RETURNING id
)
SELECT count(*) INTO deleted_tpl FROM del;
RAISE NOTICE 'Cleaned % stale theme_ir_ui_view record(s) and % ir_model_data entry(ies)', deleted_tpl, deleted_imd;
END $$;
EOF
)
echo "SQL command = $PRE_MIGRATE_SQL_5"
query_postgres_container "$PRE_MIGRATE_SQL_5" ou17 || exit 1
# Copy filestores
copy_filestore ou16 ou16 ou17 ou17 || exit 1

View File

@@ -6,6 +6,56 @@ echo "Prepare migration to 18.0..."
# Copy database
copy_database ou17 ou18 ou18 || exit 1
# ============================================================================
# REMOVE contract_payment_mode (depends on obsolete account_payment_partner)
#
# contract_payment_mode v18 still depends on account_payment_partner (deleted
# by the bank-payment renaming below) and uses the obsolete account.payment.mode
# model. No v18 replacement exists — account_payment_base_oca provides no
# contract integration. Only 2 contracts had payment_mode_id set, and the
# migration to the new payment.method.line model is not supported.
#
# MUST run BEFORE the bank-payment renaming and BEFORE OpenUpgrade's
# button_upgrade() which would otherwise crash on the missing dependency.
#
# We DELETE the module entirely (like the bank-payment merged_modules pattern)
# rather than marking it 'to remove', because Odoo's "Transient module states
# were reset" in load_modules() converts 'to remove' -> 'installed', causing
# button_upgrade() to re-parse the missing account_payment_partner dependency
# and abort the registry load. Deleting the row + ir_model_data + dependencies
# ensures OpenUpgrade's update_list() rediscovers the physical addon as a fresh
# 'uninstalled' module that button_upgrade() never touches.
# ============================================================================
contract_payment_mode_sql=$(cat <<'EOF'
DO $$
DECLARE
mod_id INTEGER;
BEGIN
SELECT id INTO mod_id FROM ir_module_module WHERE name = 'contract_payment_mode';
IF mod_id IS NOT NULL THEN
DELETE FROM ir_module_module_dependency WHERE module_id = mod_id;
DELETE FROM ir_model_data WHERE module = 'contract_payment_mode';
DELETE FROM ir_module_module WHERE id = mod_id;
-- Rename the module's own external ID (base.module_contract_payment_mode)
-- so update_list() can rediscover the physical contract_payment_mode addon
-- (which still exists in v18 addons path) without conflicting on
-- (base, module_contract_payment_mode). The orphaned xmlid pointed to
-- the deleted ir.module.module row and has no other references.
UPDATE ir_model_data
SET name = 'module_contract_payment_mode_removed'
WHERE module = 'base' AND name = 'module_contract_payment_mode';
RAISE NOTICE 'Removed contract_payment_mode module (depends on obsolete account_payment_partner)';
ELSE
RAISE NOTICE 'contract_payment_mode not found, skipping';
END IF;
END $$;
EOF
)
echo "Removing contract_payment_mode module..."
query_postgres_container "$contract_payment_mode_sql" ou18 || exit 1
# ============================================================================
# BANK-PAYMENT -> BANK-PAYMENT-ALTERNATIVE MODULE RENAMING
# Migration from OCA/bank-payment to OCA/bank-payment-alternative
@@ -96,6 +146,82 @@ else
echo "Module account_payment_mode not installed, skipping bank-payment migration."
fi
# ============================================================================
# ACCOUNT_FACTORING -> ACCOUNT_FACTORING_OCA MODULE RENAMING
#
# account_factoring (depends on account_payment_partner + account_payment_mode)
# is replaced by account_factoring_oca (depends on account_payment_base_oca
# + account_payment_base_oca_sale) in v18.
#
# Both modules have rigorously identical stored fields:
# - account_journal: is_factor, factor_fee, factor_holdback_percent,
# factor_partner_id, factor_fee_account_id, factor_holdback_account_id,
# factor_limit_holdback_account_id, factor_tax_id, factor_validation
# - account_move: factor_transfer_id, factor_payment_id,
# payment_state_with_factor
# - res_partner: factor_credit_limit
#
# Renaming (not uninstalling) preserves the 4910 invoice factor statuses,
# 311 partner credit limits, and journal factor config. OpenUpgrade will
# treat account_factoring_oca as already installed and adopt the existing
# columns via ORM _auto_init.
#
# MUST run AFTER the bank-payment renaming block above, which creates
# account_payment_base_oca in the DB.
# ============================================================================
ACCOUNT_FACTORING_RENAME_SQL=$(cat <<'EOF'
DO $$
DECLARE
factor_module_id INTEGER;
BEGIN
SELECT id INTO factor_module_id FROM ir_module_module WHERE name = 'account_factoring';
IF factor_module_id IS NOT NULL THEN
-- Rename the module itself
UPDATE ir_module_module SET name = 'account_factoring_oca' WHERE name = 'account_factoring';
UPDATE ir_model_data SET module = 'account_factoring_oca' WHERE module = 'account_factoring';
-- Rename the module's own external ID (base.module_account_factoring)
-- so OpenUpgrade can rediscover the physical account_factoring addon dir
-- (which still exists in v18 addons path) without conflicting on
-- (base, module_account_factoring) — that xmlid now points to the
-- renamed module under its new name.
UPDATE ir_model_data
SET name = 'module_account_factoring_oca'
WHERE module = 'base' AND name = 'module_account_factoring';
-- Redirect dependencies pointing TO the old name
UPDATE ir_module_module_dependency SET name = 'account_factoring_oca' WHERE name = 'account_factoring';
-- Replace obsolete dependencies (account_payment_partner + account_payment_mode
-- no longer exist in v18 addons) with the new OCA equivalents
DELETE FROM ir_module_module_dependency
WHERE module_id = factor_module_id
AND name IN ('account_payment_partner', 'account_payment_mode');
INSERT INTO ir_module_module_dependency (name, module_id)
SELECT 'account_payment_base_oca', factor_module_id
WHERE NOT EXISTS (
SELECT 1 FROM ir_module_module_dependency
WHERE module_id = factor_module_id AND name = 'account_payment_base_oca'
);
INSERT INTO ir_module_module_dependency (name, module_id)
SELECT 'account_payment_base_oca_sale', factor_module_id
WHERE NOT EXISTS (
SELECT 1 FROM ir_module_module_dependency
WHERE module_id = factor_module_id AND name = 'account_payment_base_oca_sale'
);
RAISE NOTICE 'Renamed account_factoring -> account_factoring_oca (data preserved)';
ELSE
RAISE NOTICE 'account_factoring not found, skipping rename';
END IF;
END $$;
EOF
)
echo "Executing account_factoring -> account_factoring_oca renaming..."
query_postgres_container "$ACCOUNT_FACTORING_RENAME_SQL" ou18 || exit 1
# ============================================================================
# FIX: Rename company-dependent columns before OpenUpgrade runs
# In Odoo 18, company-dependent fields are stored as JSONB columns.