[MIG] account_invoice_default_journal: migrate to 18.0
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
# Copyright 2026 Elabore
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
"""
|
||||
Carry 16.0 ``ir.property`` values over to the 18.0 ``jsonb`` storage.
|
||||
|
||||
Odoo 17.0 removed the ``ir.property`` model: the value of a
|
||||
``company_dependent`` field now lives in a ``jsonb`` column on the model's own
|
||||
table, keyed by company id, with the cross-record fallback stored in
|
||||
``ir.default``. Nothing converts the values of a *custom* company-dependent
|
||||
field automatically, so a 16.0 database upgraded to 18.0 would silently lose
|
||||
every forced journal configured on a product.
|
||||
|
||||
This script reads the legacy ``ir_property`` rows of the two fields this module
|
||||
owns and rewrites them in the 18.0 format. It is idempotent and a no-op on a
|
||||
fresh 18.0 install, where the ``ir_property`` table no longer exists.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
MODEL = "product.template"
|
||||
TABLE = "product_template"
|
||||
FIELDS = ("forced_journal_id", "forced_purchase_journal_id")
|
||||
|
||||
|
||||
def _legacy_table_exists(cr):
|
||||
"""
|
||||
Tell whether the 16.0 ``ir_property`` table is still present.
|
||||
|
||||
:param cr: database cursor.
|
||||
:return: ``True`` when the legacy table can be read, ``False`` on a fresh
|
||||
18.0 install or once the table has been dropped.
|
||||
"""
|
||||
cr.execute("SELECT to_regclass('public.ir_property') IS NOT NULL")
|
||||
return cr.fetchone()[0]
|
||||
|
||||
|
||||
def _field_id(cr, field_name):
|
||||
"""
|
||||
Return the ``ir_model_fields`` id of a field of :data:`MODEL`.
|
||||
|
||||
Only the ``product.template`` definition is looked up on purpose: the
|
||||
``product.product`` counterpart is a delegated related field and never
|
||||
carries ``ir.property`` rows of its own.
|
||||
|
||||
:param cr: database cursor.
|
||||
:param str field_name: name of the field to look up.
|
||||
:return: the field id, or ``None`` when the field is unknown to the
|
||||
database (module never installed before the upgrade).
|
||||
"""
|
||||
cr.execute(
|
||||
"SELECT id FROM ir_model_fields WHERE model = %s AND name = %s",
|
||||
(MODEL, field_name),
|
||||
)
|
||||
row = cr.fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
def _migrate_per_record_values(cr, field_name, field_id):
|
||||
"""
|
||||
Move per-product legacy values into the ``jsonb`` column.
|
||||
|
||||
Existing keys in the target column win, which makes the operation
|
||||
idempotent and safe to re-run after a partial upgrade. Properties pointing
|
||||
at a journal that no longer exists are dropped, since the ``jsonb``
|
||||
storage of a company-dependent many2one has no foreign key to protect it.
|
||||
|
||||
:param cr: database cursor.
|
||||
:param str field_name: name of the company-dependent field to fill.
|
||||
:param int field_id: ``ir_model_fields`` id of that field.
|
||||
:return: number of products updated.
|
||||
"""
|
||||
cr.execute(
|
||||
f"""
|
||||
WITH legacy AS (
|
||||
SELECT
|
||||
split_part(ip.res_id, ',', 2)::integer AS product_tmpl_id,
|
||||
ip.company_id AS company_id,
|
||||
split_part(ip.value_reference, ',', 2)::integer AS journal_id
|
||||
FROM ir_property ip
|
||||
WHERE ip.fields_id = %s
|
||||
AND ip.type = 'many2one'
|
||||
AND ip.value_reference IS NOT NULL
|
||||
AND ip.res_id IS NOT NULL
|
||||
AND ip.company_id IS NOT NULL
|
||||
AND split_part(ip.res_id, ',', 1) = %s
|
||||
),
|
||||
checked AS (
|
||||
SELECT legacy.*
|
||||
FROM legacy
|
||||
JOIN account_journal aj ON aj.id = legacy.journal_id
|
||||
JOIN {TABLE} pt ON pt.id = legacy.product_tmpl_id
|
||||
JOIN res_company rc ON rc.id = legacy.company_id
|
||||
),
|
||||
grouped AS (
|
||||
SELECT
|
||||
product_tmpl_id,
|
||||
jsonb_object_agg(company_id::text, journal_id) AS value
|
||||
FROM checked
|
||||
GROUP BY product_tmpl_id
|
||||
)
|
||||
UPDATE {TABLE} pt
|
||||
SET {field_name} = grouped.value || COALESCE(pt.{field_name}, '{{}}'::jsonb)
|
||||
FROM grouped
|
||||
WHERE pt.id = grouped.product_tmpl_id
|
||||
""",
|
||||
(field_id, MODEL),
|
||||
)
|
||||
return cr.rowcount
|
||||
|
||||
|
||||
def _migrate_fallback_values(cr, field_name, field_id):
|
||||
"""
|
||||
Move legacy cross-record defaults into ``ir_default``.
|
||||
|
||||
An ``ir_property`` row without ``res_id`` was a default applying to every
|
||||
record of the model. In 18.0 that role belongs to ``ir.default``, which is
|
||||
what :meth:`odoo.fields.Field.get_company_dependent_fallback` reads.
|
||||
|
||||
Rows are only inserted when no default exists yet for the same field and
|
||||
company, keeping the operation idempotent.
|
||||
|
||||
:param cr: database cursor.
|
||||
:param str field_name: name of the company-dependent field.
|
||||
:param int field_id: ``ir_model_fields`` id of that field.
|
||||
:return: number of defaults created.
|
||||
"""
|
||||
cr.execute(
|
||||
"""
|
||||
INSERT INTO ir_default (field_id, user_id, company_id, condition, json_value)
|
||||
SELECT %s, NULL, ip.company_id, NULL,
|
||||
split_part(ip.value_reference, ',', 2)
|
||||
FROM ir_property ip
|
||||
JOIN account_journal aj
|
||||
ON aj.id = split_part(ip.value_reference, ',', 2)::integer
|
||||
JOIN res_company rc ON rc.id = ip.company_id
|
||||
WHERE ip.fields_id = %s
|
||||
AND ip.type = 'many2one'
|
||||
AND ip.value_reference IS NOT NULL
|
||||
AND ip.res_id IS NULL
|
||||
AND ip.company_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ir_default d
|
||||
WHERE d.field_id = %s
|
||||
AND d.company_id = ip.company_id
|
||||
AND d.user_id IS NULL
|
||||
AND d.condition IS NULL
|
||||
)
|
||||
""",
|
||||
(field_id, field_id, field_id),
|
||||
)
|
||||
if cr.rowcount:
|
||||
_logger.info(
|
||||
"%s: converted %s company fallback(s) from ir.property to ir.default",
|
||||
field_name,
|
||||
cr.rowcount,
|
||||
)
|
||||
return cr.rowcount
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
"""
|
||||
Convert the module's legacy ``ir.property`` rows to the 18.0 storage.
|
||||
|
||||
Runs after the module schema has been updated, so the ``jsonb`` columns
|
||||
this script writes into already exist.
|
||||
|
||||
:param cr: database cursor.
|
||||
:param str version: version the module is upgraded from. Falsy on a fresh
|
||||
install, in which case there is nothing to convert.
|
||||
"""
|
||||
if not version:
|
||||
return
|
||||
if not _legacy_table_exists(cr):
|
||||
_logger.info(
|
||||
"ir_property table is gone, nothing to convert for %s",
|
||||
", ".join(FIELDS),
|
||||
)
|
||||
return
|
||||
|
||||
for field_name in FIELDS:
|
||||
field_id = _field_id(cr, field_name)
|
||||
if not field_id:
|
||||
_logger.warning(
|
||||
"%s.%s not found in ir_model_fields, skipping its conversion",
|
||||
MODEL,
|
||||
field_name,
|
||||
)
|
||||
continue
|
||||
updated = _migrate_per_record_values(cr, field_name, field_id)
|
||||
_migrate_fallback_values(cr, field_name, field_id)
|
||||
_logger.info(
|
||||
"%s.%s: %s product(s) migrated from ir.property to jsonb storage",
|
||||
MODEL,
|
||||
field_name,
|
||||
updated,
|
||||
)
|
||||
Reference in New Issue
Block a user