from collections import defaultdict from operator import attrgetter from odoo.tools import OrderedSet, unique from odoo.tools.sql import pg_varchar from .fields import Field from .fields_numeric import Integer from .fields_selection import Selection from .models import BaseModel class Reference(Selection): """ Pseudo-relational field (no FK in database). The field value is stored as a :class:`string ` following the pattern ``"res_model,res_id"`` in database. """ type = 'reference' _column_type = ('varchar', pg_varchar()) def convert_to_column(self, value, record, values=None, validate=True): return Field.convert_to_column(self, value, record, values, validate) def convert_to_cache(self, value, record, validate=True): # cache format: str ("model,id") or None if isinstance(value, BaseModel): if not validate or (value._name in self.get_values(record.env) and len(value) <= 1): return "%s,%s" % (value._name, value.id) if value else None elif isinstance(value, str): res_model, res_id = value.split(',') if not validate or res_model in self.get_values(record.env): if record.env[res_model].browse(int(res_id)).exists(): return value else: return None elif not value: return None raise ValueError("Wrong value for %s: %r" % (self, value)) def convert_to_record(self, value, record): if value: res_model, res_id = value.split(',') return record.env[res_model].browse(int(res_id)) return None def convert_to_read(self, value, record, use_display_name=True): return "%s,%s" % (value._name, value.id) if value else False def convert_to_export(self, value, record): return value.display_name if value else '' def convert_to_display_name(self, value, record): return value.display_name if value else False class Many2oneReference(Integer): """ Pseudo-relational field (no FK in database). The field value is stored as an :class:`integer ` id in database. Contrary to :class:`Reference` fields, the model has to be specified in a :class:`Char` field, whose name has to be specified in the `model_field` attribute for the current :class:`Many2oneReference` field. :param str model_field: name of the :class:`Char` where the model name is stored. """ type = 'many2one_reference' model_field = None aggregator = None _related_model_field = property(attrgetter('model_field')) _description_model_field = property(attrgetter('model_field')) def convert_to_cache(self, value, record, validate=True): # cache format: id or None if isinstance(value, BaseModel): value = value._ids[0] if value._ids else None return super().convert_to_cache(value, record, validate) def _update_inverses(self, records: BaseModel, value): """ Add `records` to the cached values of the inverse fields of `self`. """ if not value: return model_ids = self._record_ids_per_res_model(records) for invf in records.pool.field_inverses[self]: records = records.browse(model_ids[invf.model_name]) if not records: continue corecord = records.env[invf.model_name].browse(value) records = records.filtered_domain(invf.get_comodel_domain(corecord)) if not records: continue ids0 = invf._get_cache(corecord.env).get(corecord.id) # if the value for the corecord is not in cache, but this is a new # record, assign it anyway, as you won't be able to fetch it from # database (see `test_sale_order`) if ids0 is not None or not corecord.id: ids1 = tuple(unique((ids0 or ()) + records._ids)) invf._update_cache(corecord, ids1) def _record_ids_per_res_model(self, records: BaseModel) -> dict[str, OrderedSet]: model_ids = defaultdict(OrderedSet) for record in records: model = record[self.model_field] if not model and record._fields[self.model_field].compute: # fallback when the model field is computed :-/ record._fields[self.model_field].compute_value(record) model = record[self.model_field] if not model: continue model_ids[model].add(record.id) return model_ids