Skip to content

Commit 3c2488e

Browse files
committed
[IMP] core: jsonb company dependent field
support jsonb company dependent field See odoo/odoo#175627 See odoo/enterprise#67925 See odoo/upgrade#6366 closes #124 Task: 3954610 Signed-off-by: Christophe Simonis (chs) <[email protected]>
1 parent 062d11a commit 3c2488e

File tree

5 files changed

+175
-10
lines changed

5 files changed

+175
-10
lines changed

src/util/fields.py

+69-3
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,8 @@ def rename_field(cr, model, old, new, update_references=True, domain_adapter=Non
506506
# (before saas~11.2, where pattern changed)
507507
name = "%s_%s" % (name, fid)
508508
cr.execute("UPDATE ir_model_data SET name=%s WHERE model='ir.model.fields' AND res_id=%s", [name, fid])
509-
cr.execute("UPDATE ir_property SET name=%s WHERE fields_id=%s", [new, fid])
509+
if table_exists(cr, "ir_property"):
510+
cr.execute("UPDATE ir_property SET name=%s WHERE fields_id=%s", [new, fid])
510511
# Updates custom field relation name to match the renamed standard field during upgrade.
511512
cr.execute(
512513
"""
@@ -652,7 +653,66 @@ def convert_field_to_html(cr, model, field, skip_inherit=()):
652653
convert_field_to_html(cr, inh.model, field, skip_inherit=skip_inherit)
653654

654655

655-
def convert_field_to_property(
656+
def _convert_field_to_company_dependent(
657+
cr, model, field, type, target_model=None, default_value=None, default_value_ref=None, company_field="company_id"
658+
):
659+
_validate_model(model)
660+
if target_model:
661+
_validate_model(target_model)
662+
663+
type2field = {"char", "float", "boolean", "integer", "text", "many2one", "date", "datetime", "selection", "html"}
664+
assert type in type2field
665+
666+
table = table_of_model(cr, model)
667+
668+
# update "ir_model_fields"."company_dependent" as an identifier for indirect reference
669+
cr.execute(
670+
"""
671+
UPDATE ir_model_fields
672+
SET company_dependent = TRUE
673+
WHERE model = %s
674+
AND name = %s
675+
RETURNING id
676+
""",
677+
(model, field),
678+
)
679+
if not cr.rowcount:
680+
# no ir_model_fields, no column
681+
remove_column(cr, table, field, cascade=True)
682+
return
683+
[field_id] = cr.fetchone()
684+
685+
if default_value is None: # noqa: SIM108
686+
where_condition = "{0} IS NOT NULL"
687+
else:
688+
where_condition = cr.mogrify("{0} != %s", [default_value]).decode()
689+
where_condition += format_query(cr, " AND {} IS NOT NULL", company_field)
690+
691+
using = format_query(cr, "jsonb_build_object({}, {{0}})", company_field)
692+
alter_column_type(cr, table, field, "jsonb", using=using, where=where_condition)
693+
694+
# delete all old default
695+
cr.execute("DELETE FROM ir_default WHERE field_id = %s", [field_id])
696+
697+
# add fallback
698+
if default_value:
699+
cr.execute(
700+
"INSERT INTO ir_default(field_id, json_value) VALUES (%s, %s) RETURNING id",
701+
(field_id, json.dumps(default_value)),
702+
)
703+
[default_id] = cr.fetchone()
704+
if default_value_ref:
705+
module, _, xid = default_value_ref.partition(".")
706+
cr.execute(
707+
"""
708+
INSERT INTO ir_model_data(module, name, model, res_id, noupdate)
709+
VALUES (%s, %s, 'ir.default', %s, True)
710+
""",
711+
[module, xid, default_id],
712+
)
713+
714+
715+
def _convert_field_to_property(
656716
cr, model, field, type, target_model=None, default_value=None, default_value_ref=None, company_field="company_id"
657717
):
658718
"""
@@ -777,7 +837,13 @@ def convert_field_to_property(
777837

778838

779839
# alias with a name related to the new API to declare property fields (company_dependent=True attribute)
780-
make_field_company_dependent = convert_field_to_property
840+
if version_gte("saas~17.5"):
841+
make_field_company_dependent = _convert_field_to_company_dependent
842+
else:
843+
make_field_company_dependent = _convert_field_to_property
844+
845+
# retro-compatibility
846+
convert_field_to_property = make_field_company_dependent
781847

782848

783849
def convert_binary_field_to_attachment(cr, model, field, encoded=True, name_field=None):

src/util/indirect_references.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
from .pg import column_exists, table_exists
66

77

8-
class IndirectReference(collections.namedtuple("IndirectReference", "table res_model res_id res_model_id set_unknown")):
8+
class IndirectReference(
9+
collections.namedtuple(
10+
"IndirectReference", "table res_model res_id res_model_id set_unknown company_dependent_comodel"
11+
)
12+
):
913
def model_filter(self, prefix="", placeholder="%s"):
1014
if prefix and prefix[-1] != ".":
1115
prefix += "."
@@ -109,12 +113,25 @@ def indirect_references(cr, bound_only=False):
109113

110114
yield ir
111115

116+
if column_exists(cr, "ir_model_fields", "company_dependent"):
117+
cr.execute(
118+
"""
119+
SELECT model, name, relation
120+
FROM ir_model_fields
121+
WHERE company_dependent IS TRUE
122+
AND ttype = 'many2one'
123+
""",
124+
)
125+
for table_name, column_name, comodel_name in cr.fetchall():
126+
yield IR(table_name, None, column_name, company_dependent_comodel=comodel_name)
127+
112128
# XXX Once we will get the model field of `many2one_reference` fields in the database, we should get them also
113129
# (and filter the one already hardcoded)
114130

115131

116132
def generate_indirect_reference_cleaning_queries(cr, ir):
117133
"""Yield queries to clean an `IndirectReference`."""
134+
assert not ir.company_dependent_comodel # not supported for now
118135
if ir.res_model:
119136
query = """
120137
SELECT {ir.res_model}

src/util/models.py

+29
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
column_updatable,
2323
explode_execute,
2424
explode_query_range,
25+
format_query,
2526
get_m2m_tables,
2627
get_value_or_en_translation,
2728
parallel_execute,
@@ -76,6 +77,34 @@ def remove_model(cr, model, drop_table=True, ignore_m2m=()):
7677

7778
# remove references
7879
for ir in indirect_references(cr):
80+
if ir.company_dependent_comodel:
81+
if ir.company_dependent_comodel == "ir.model":
82+
# clean on delete set null
83+
cr.execute("SELECT id FROM ir_model WHERE name = %s", [model])
84+
[mod_id] = cr.fetchone() or [None]
85+
if mod_id:
86+
cr.execute(
87+
format_query(
88+
"""
89+
UPDATE {table}
90+
SET {field} = (
91+
SELECT jsonb_object_agg(
92+
key,
93+
CASE
94+
WHEN value::int4 = %s THEN NULL
95+
ELSE value::int4
96+
END)
97+
FROM jsonb_each_text({field})
98+
)
99+
WHERE {field} IS NOT NULL
100+
AND {field} @? %s
101+
""",
102+
table=ir.table,
103+
field=ir.res_id,
104+
),
105+
[mod_id, "$.* ? (@ == {})".format(mod_id)],
106+
)
107+
continue
79108
if ir.table in ("ir_model", "ir_model_fields", "ir_model_data"):
80109
continue
81110
ref_model = model_of_table(cr, ir.table)

src/util/pg.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ def remove_column(cr, table, column, cascade=False):
570570
cr.execute('ALTER TABLE "{0}" DROP COLUMN "{1}"{2}'.format(table, column, drop_cascade))
571571

572572

573-
def alter_column_type(cr, table, column, type, using=None, logger=_logger):
573+
def alter_column_type(cr, table, column, type, using=None, where=None, logger=_logger):
574574
# remove the existing linked `ir_model_fields_selection` recods in case it was a selection field
575575
if table_exists(cr, "ir_model_fields_selection"):
576576
cr.execute(
@@ -604,9 +604,13 @@ def alter_column_type(cr, table, column, type, using=None, logger=_logger):
604604
cr.execute(format_query(cr, "ALTER TABLE {} ADD COLUMN {} {}", table, column, sql.SQL(type)))
605605

606606
using = sql.SQL(format_query(cr, using, tmp_column))
607-
where_clause = sql.SQL("")
608-
if column_type(cr, table, tmp_column) != "bool":
609-
where_clause = sql.SQL(format_query(cr, "WHERE {} IS NOT NULL", tmp_column))
607+
if where is None:
608+
where_clause = sql.SQL("")
609+
if column_type(cr, table, tmp_column) != "bool":
610+
where_clause = sql.SQL(format_query(cr, "WHERE {} IS NOT NULL", tmp_column))
611+
else:
612+
where_clause = sql.SQL(format_query(cr, "WHERE " + where, tmp_column))
613+
610614
explode_execute(
611615
cr,
612616
format_query(cr, "UPDATE {} SET {} = {} {}", table, column, using, where_clause),

src/util/records.py

+51-2
Original file line numberDiff line numberDiff line change
@@ -410,8 +410,34 @@ def remove_records(cr, model, ids):
410410
table = table_of_model(cr, model)
411411
cr.execute('DELETE FROM "{}" WHERE id IN %s'.format(table), [ids])
412412
for ir in indirect_references(cr, bound_only=True):
413-
query = 'DELETE FROM "{}" WHERE {} AND "{}" IN %s'.format(ir.table, ir.model_filter(), ir.res_id)
414-
cr.execute(query, [model, ids])
413+
if not ir.company_dependent_comodel:
414+
query = 'DELETE FROM "{}" WHERE {} AND "{}" IN %s'.format(ir.table, ir.model_filter(), ir.res_id)
415+
cr.execute(query, [model, ids])
416+
elif ir.company_dependent_comodel == model:
417+
query = cr.mogrify(
418+
format_query(
419+
cr,
420+
"""
421+
UPDATE {table}
422+
SET {column} = (
423+
SELECT jsonb_object_agg(
424+
key,
425+
CASE
426+
WHEN value::int4 IN %s THEN NULL
427+
ELSE value::int4
428+
END)
429+
FROM jsonb_each_text({column})
430+
)
431+
WHERE {column} IS NOT NULL
432+
AND {column} @? %s
433+
AND {{parallel_filter}}
434+
""",
435+
table=ir.table,
436+
column=ir.res_id,
437+
),
438+
[ids, "$.* ? ({})".format(" || ".join(map("@ == {}".format, ids)))],
439+
).decode()
440+
explode_execute(cr, query, table=ir.table)
415441
_rm_refs(cr, model, ids)
416442

417443
if model == "res.groups":
@@ -1440,6 +1466,29 @@ def replace_record_references_batch(cr, id_mapping, model_src, model_dst=None, r
14401466
for ir in indirect_references(cr, bound_only=True):
14411467
if ir.table in ignores:
14421468
continue
1469+
if ir.company_dependent_comodel:
1470+
if ir.company_dependent_comodel == model_src:
1471+
assert model_src == model_dst
1472+
query = cr.mogrify(
1473+
format_query(
1474+
cr,
1475+
"""
1476+
UPDATE {table}
1477+
SET {column} = (
1478+
SELECT jsonb_object_agg(key, COALESCE((%s::jsonb->>value)::int, value::int))
1479+
FROM jsonb_each_text({column})
1480+
)
1481+
WHERE {column} IS NOT NULL
1482+
AND {column} @? %s
1483+
AND {{parallel_filter}}
1484+
""",
1485+
table=ir.table,
1486+
column=ir.res_id,
1487+
),
1488+
[Json(id_mapping), "$.* ? ({})".format(" || ".join(map("@ == {}".format, id_mapping)))],
1489+
).decode()
1490+
explode_execute(cr, query, table=ir.table)
1491+
continue
14431492
res_model_upd = []
14441493
if ir.res_model:
14451494
res_model_upd.append('"{ir.res_model}" = %(model_dst)s')

0 commit comments

Comments
 (0)