diff --git a/trytond/CHANGELOG b/trytond/CHANGELOG index 5b1220cd17..7444bb3546 100644 --- a/trytond/CHANGELOG +++ b/trytond/CHANGELOG @@ -1,3 +1,4 @@ +* Add fmany2one field * Allow creation of foreign key on non primary key * Include context information in access error * Remove trytond-worker max tasks per child option for Python <= 3.10 diff --git a/trytond/doc/ref/fields.rst b/trytond/doc/ref/fields.rst index 3678353baf..9a3770d5a4 100644 --- a/trytond/doc/ref/fields.rst +++ b/trytond/doc/ref/fields.rst @@ -1174,3 +1174,17 @@ Instance methods: following ``type_``. The descriptor must be used on the same class as the field. Default ``type_`` is ``values``. + +fmany2one +--------- + +.. function:: fmany2one(name, sources, target[, string[, ondelete[, \**options]]]) + +Return a mixin_ class which adds a :class:`Function` field to a +:class:`~trytond.model.ModelSQL` class that emulate a :class:`Many2One` that +refers to the ``target`` record having the ``sources`` as value. +``target`` is composed of the name of the model and the name of the fields +separated by a comma. +The target fields must be unique. + +.. _mixin: http://en.wikipedia.org/wiki/Mixin diff --git a/trytond/trytond/model/fields/__init__.py b/trytond/trytond/model/fields/__init__.py index bb8853aac2..271c241965 100644 --- a/trytond/trytond/model/fields/__init__.py +++ b/trytond/trytond/model/fields/__init__.py @@ -10,6 +10,7 @@ from .field import ( SQL_OPERATORS, Field, context_validate, depends, domain_validate, get_eval_fields, on_change_result, states_validate) from .float import Float +from .fmany2one import fmany2one from .function import Function, MultiValue from .integer import Integer from .many2many import Many2Many @@ -28,4 +29,4 @@ __all__ = [ Boolean, Integer, Char, Text, FullText, Float, Numeric, Date, Timestamp, DateTime, Time, TimeDelta, Binary, Selection, Reference, Many2One, One2Many, Many2Many, Function, MultiValue, One2One, Dict, - MultiSelection] + MultiSelection, fmany2one] diff --git a/trytond/trytond/model/fields/fmany2one.py b/trytond/trytond/model/fields/fmany2one.py new file mode 100644 index 0000000000..3c7ee5e8ef --- /dev/null +++ b/trytond/trytond/model/fields/fmany2one.py @@ -0,0 +1,87 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. + +from sql import Column, Literal + +from trytond.pool import Pool +from trytond.tools import grouped_slice + +from .function import Function +from .many2one import Many2One + + +def fmany2one( + name, sources, target, string="", ondelete='SET NULL', **kwargs): + sources = sources.split(',') + target_model, target_fields = target.split(',', 1) + target_fields = target_fields.split(',') + assert len(sources) == len(target_fields) + + class Mixin: + __slots__ = () + + @classmethod + def __register__(cls, module): + pool = Pool() + Target = pool.get(target_model) + table_h = cls.__table_handler__(module) + super().__register__(module) + table_h.add_fk( + sources, Target._table, target_fields, + on_delete=getattr(cls, name).ondelete) + + @classmethod + def getter(cls, records, name): + pool = Pool() + Target = pool.get(target_model) + values = set(filter(all, + (tuple(getattr(r, s) for s in sources) for r in records))) + values2id = {} + for sub_values in grouped_slice(values): + domain = ['OR'] + for values in sub_values: + domain.append( + [(f, '=', v) for f, v in zip(target_fields, values)]) + targets = Target.search(domain) + values2id.update( + (tuple(getattr(t, f) for f in target_fields), t.id) + for t in targets) + return {r.id: values2id.get( + tuple(getattr(r, s) for s in sources)) for r in records} + + @classmethod + def setter(cls, records, name, value): + pool = Pool() + Target = pool.get(target_model) + if value: + value = getattr(Target(value), target_fields[0]) + else: + value = None + cls.write(records, {sources[0]: value}) + + @classmethod + def searcher(cls, clause, tables): + pool = Pool() + Target = pool.get(target_model) + table, _ = tables[None] + if name not in tables: + target = Target.__table__() + join = Literal(True) + for source, target_field in zip(sources, target_fields): + join &= Column(table, source) == Column(target, target_field) + tables[name] = { + None: (target, join), + } + nested = clause[0][len(name) + 1:] + domain = [(nested or 'rec_name', *clause[1:])] + tables, clause = Target.search_domain( + domain, tables=tables[name]) + return clause + + setattr(Mixin, name, Function( + Many2One(target_model, string, ondelete=ondelete, **kwargs), + f'get_{name}', setter=f'set_{name}')) + setattr(Mixin, f'get_{name}', getter) + setattr(Mixin, f'set_{name}', setter) + setattr(Mixin, f'domain_{name}', searcher) + return Mixin