882 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			882 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # This file is part of Tryton.  The COPYRIGHT file at the top level of
 | |
| # this repository contains the full copyright notices and license terms.
 | |
| import json
 | |
| import os
 | |
| 
 | |
| from lxml import etree
 | |
| from sql import Literal, Null
 | |
| 
 | |
| from trytond.cache import Cache, MemoryCache
 | |
| from trytond.i18n import gettext
 | |
| from trytond.model import Index, ModelSQL, ModelView, fields, sequence_ordered
 | |
| from trytond.model.exceptions import ValidationError
 | |
| from trytond.pool import Pool
 | |
| from trytond.pyson import PYSON, Bool, Eval, If, PYSONDecoder
 | |
| from trytond.rpc import RPC
 | |
| from trytond.tools import file_open
 | |
| from trytond.transaction import Transaction
 | |
| from trytond.wizard import Button, StateView, Wizard
 | |
| 
 | |
| from ..action import DomainError, ViewError
 | |
| 
 | |
| # Numbers taken from Bootstrap's breakpoints
 | |
| WIDTH_BREAKPOINTS = [
 | |
|     1400,
 | |
|     1200,
 | |
|     992,
 | |
|     768,
 | |
|     576,
 | |
|     ]
 | |
| 
 | |
| 
 | |
| class XMLError(ValidationError):
 | |
|     pass
 | |
| 
 | |
| 
 | |
| class View(
 | |
|         fields.fmany2one(
 | |
|             'model_ref', 'model', 'ir.model,name', "Model",
 | |
|             ondelete='CASCADE'),
 | |
|         fields.fmany2one(
 | |
|             'field_children', 'field_childs,model',
 | |
|             'ir.model.field,name,model',
 | |
|             "Children Field",
 | |
|             domain=[
 | |
|                 ('model', '=', Eval('model')),
 | |
|                 ],
 | |
|             states={
 | |
|                 'invisible': Eval('type') != 'tree',
 | |
|                 }),
 | |
|         fields.fmany2one(
 | |
|             'module_ref', 'module', 'ir.module,name', "Module",
 | |
|             readonly=True, ondelete='CASCADE'),
 | |
|         sequence_ordered('priority', "Priority"), ModelSQL, ModelView):
 | |
|     "View"
 | |
|     __name__ = 'ir.ui.view'
 | |
|     model = fields.Char('Model', states={
 | |
|             'required': Eval('type') != 'board',
 | |
|             })
 | |
|     type = fields.Selection([
 | |
|             (None, ''),
 | |
|             ('tree', 'Tree'),
 | |
|             ('form', 'Form'),
 | |
|             ('graph', 'Graph'),
 | |
|             ('calendar', 'Calendar'),
 | |
|             ('board', 'Board'),
 | |
|             ('list-form', "List Form"),
 | |
|             ], 'View Type',
 | |
|         domain=[
 | |
|             If(Bool(Eval('inherit')),
 | |
|                 ('type', '=', None),
 | |
|                 ('type', '!=', None)),
 | |
|             ],
 | |
|         depends=['inherit'])
 | |
|     type_string = type.translated('type')
 | |
|     data = fields.Text('Data')
 | |
|     name = fields.Char('Name', states={
 | |
|             'invisible': ~(Eval('module') & Eval('name')),
 | |
|             }, depends=['module'], readonly=True)
 | |
|     arch = fields.Function(fields.Text('View Architecture', states={
 | |
|                 'readonly': Bool(Eval('name')),
 | |
|                 }, depends=['name']), 'get_arch', setter='set_arch')
 | |
|     basis = fields.Function(fields.Boolean("Basis"), 'get_basis')
 | |
|     inherit = fields.Many2One('ir.ui.view', 'Inherited View',
 | |
|             ondelete='CASCADE')
 | |
|     extensions = fields.One2Many(
 | |
|         'ir.ui.view', 'inherit', "Extensions",
 | |
|         filter=[
 | |
|             ('basis', '=', False),
 | |
|             ],
 | |
|         domain=[
 | |
|             ('model', '=', Eval('model')),
 | |
|             ('type', '=', None),
 | |
|             ],
 | |
|         states={
 | |
|             'invisible': ~Eval('type'),
 | |
|             },
 | |
|         order=[('id', None)])
 | |
|     field_childs = fields.Char('Children Field', states={
 | |
|             'invisible': Eval('type') != 'tree',
 | |
|             }, depends=['type'])
 | |
|     module = fields.Char('Module', states={
 | |
|             'invisible': ~Eval('module'),
 | |
|             }, readonly=True)
 | |
|     domain = fields.Char('Domain', states={
 | |
|             'invisible': ~Eval('inherit'),
 | |
|             }, depends=['inherit'])
 | |
|     _get_rng_cache = MemoryCache('ir_ui_view.get_rng', context=False)
 | |
|     _view_get_cache = Cache('ir_ui_view.view_get')
 | |
|     __module_index = None
 | |
| 
 | |
|     @classmethod
 | |
|     def __setup__(cls):
 | |
|         super().__setup__()
 | |
|         table = cls.__table__()
 | |
|         cls.priority.required = True
 | |
| 
 | |
|         cls.__rpc__['view_get'] = RPC(instantiate=0, cache=dict(days=1))
 | |
|         cls._buttons.update({
 | |
|                 'show': {
 | |
|                     'readonly': Eval('type') != 'form',
 | |
|                     'invisible': ~Eval('basis', False),
 | |
|                     'depends': ['type', 'basis'],
 | |
|                     },
 | |
|                 })
 | |
|         cls._sql_indexes.update({
 | |
|                 Index(table,
 | |
|                     (table.model, Index.Equality()),
 | |
|                     (table.inherit, Index.Range())),
 | |
|                 Index(
 | |
|                     table,
 | |
|                     (table.id, Index.Range()),
 | |
|                     (table.inherit, Index.Range())),
 | |
|                 })
 | |
| 
 | |
|     @staticmethod
 | |
|     def default_priority():
 | |
|         return 16
 | |
| 
 | |
|     @staticmethod
 | |
|     def default_module():
 | |
|         return Transaction().context.get('module')
 | |
| 
 | |
|     def get_basis(self, name):
 | |
|         return not self.inherit or self.model != self.inherit.model
 | |
| 
 | |
|     @classmethod
 | |
|     def domain_basis(cls, domain, tables):
 | |
|         table, _ = tables[None]
 | |
|         if 'inherit' not in tables:
 | |
|             inherit = cls.__table__()
 | |
|             tables['inherit'] = {
 | |
|                 None: (inherit, table.inherit == inherit.id),
 | |
|                 }
 | |
|         else:
 | |
|             inherit, _ = tables['inherit'][None]
 | |
|         expression = (table.inherit == Null) | (table.model != inherit.model)
 | |
| 
 | |
|         _, operator, value = domain
 | |
|         if operator in {'=', '!='}:
 | |
|             if (operator == '=') != value:
 | |
|                 expression = ~expression
 | |
|         elif operator in {'in', 'not in'}:
 | |
|             if True in value and False not in value:
 | |
|                 pass
 | |
|             elif False in value and True not in value:
 | |
|                 expression = ~expression
 | |
|             else:
 | |
|                 expression = Literal(True)
 | |
|         else:
 | |
|             expression = Literal(True)
 | |
|         return expression
 | |
| 
 | |
|     def get_rec_name(self, name):
 | |
|         return ' '.join(filter(None, [
 | |
|                     self.model_ref.rec_name if self.model_ref else '',
 | |
|                     '(%s)' % (
 | |
|                         self.inherit.rec_name if self.inherit else
 | |
|                         self.type_string),
 | |
|                     ]))
 | |
| 
 | |
|     @classmethod
 | |
|     def search_rec_name(cls, name, clause):
 | |
|         return [('model_ref.rec_name', *clause[1:])]
 | |
| 
 | |
|     @classmethod
 | |
|     @ModelView.button_action('ir.act_view_show')
 | |
|     def show(cls, views):
 | |
|         pass
 | |
| 
 | |
|     @classmethod
 | |
|     def get_rng(cls, type_):
 | |
|         key = (cls.__name__, type_)
 | |
|         rng = cls._get_rng_cache.get(key)
 | |
|         if rng is None:
 | |
|             if type_ == 'list-form':
 | |
|                 type_ = 'form'
 | |
|             rng_name = os.path.join(os.path.dirname(__file__), type_ + '.rng')
 | |
|             with open(rng_name, 'rb') as fp:
 | |
|                 rng = etree.fromstring(fp.read())
 | |
|             cls._get_rng_cache.set(key, rng)
 | |
|         return rng
 | |
| 
 | |
|     @property
 | |
|     def rng_type(self):
 | |
|         if self.inherit:
 | |
|             return self.inherit.rng_type
 | |
|         return self.type
 | |
| 
 | |
|     @classmethod
 | |
|     def validate(cls, views):
 | |
|         super().validate(views)
 | |
|         cls.check_xml(views)
 | |
| 
 | |
|     @classmethod
 | |
|     def check_xml(cls, views):
 | |
|         "Check XML"
 | |
|         for view in views:
 | |
|             if not view.arch:
 | |
|                 continue
 | |
|             xml = view.arch.strip()
 | |
|             if not xml:
 | |
|                 continue
 | |
|             tree = etree.fromstring(xml)
 | |
| 
 | |
|             if hasattr(etree, 'RelaxNG'):
 | |
|                 validator = etree.RelaxNG(etree=cls.get_rng(view.rng_type))
 | |
|                 if not validator.validate(tree):
 | |
|                     error_log = '\n'.join(map(str,
 | |
|                             validator.error_log.filter_from_errors()))
 | |
|                     raise XMLError(
 | |
|                         gettext('ir.msg_view_invalid_xml', name=view.rec_name),
 | |
|                         error_log)
 | |
|             root_element = tree.getroottree().getroot()
 | |
| 
 | |
|             # validate pyson attributes
 | |
|             validates = {
 | |
|                 'states': fields.states_validate,
 | |
|             }
 | |
| 
 | |
|             def encode(element):
 | |
|                 for attr in ('states', 'domain', 'spell'):
 | |
|                     if not element.get(attr):
 | |
|                         continue
 | |
|                     try:
 | |
|                         value = PYSONDecoder().decode(element.get(attr))
 | |
|                         validates.get(attr, lambda a: True)(value)
 | |
|                     except Exception as e:
 | |
|                         error_log = '%s: <%s %s="%s"/>' % (
 | |
|                             e, element.get('id') or element.get('name'), attr,
 | |
|                             element.get(attr))
 | |
|                         raise XMLError(
 | |
|                             gettext(
 | |
|                                 'ir.msg_view_invalid_xml', name=view.rec_name),
 | |
|                             error_log) from e
 | |
|                 for child in element:
 | |
|                     encode(child)
 | |
|             encode(root_element)
 | |
| 
 | |
|     def get_arch(self, name):
 | |
|         value = None
 | |
|         if self.name and self.module:
 | |
|             path = os.path.join(self.module, 'view', self.name + '.xml')
 | |
|             try:
 | |
|                 with file_open(path,
 | |
|                         subdir='modules', mode='r', encoding='utf-8') as fp:
 | |
|                     value = fp.read()
 | |
|             except IOError:
 | |
|                 pass
 | |
|         if not value:
 | |
|             value = self.data
 | |
|         return value
 | |
| 
 | |
|     @classmethod
 | |
|     def set_arch(cls, views, name, value):
 | |
|         cls.write(views, {'data': value})
 | |
| 
 | |
|     @classmethod
 | |
|     def on_modification(cls, mode, records, field_names=None):
 | |
|         super().on_modification(mode, records, field_names=field_names)
 | |
|         cls._view_get_cache.clear()
 | |
|         ModelView._fields_view_get_cache.clear()
 | |
| 
 | |
|     @property
 | |
|     def _module_index(self):
 | |
|         from trytond.modules import create_graph, get_modules
 | |
|         if self.__class__.__module_index is None:
 | |
|             graph = create_graph(get_modules(with_test=Pool.test))
 | |
|             modules = [m.name for m in graph]
 | |
|             self.__class__.__module_index = {
 | |
|                 m: i for i, m in enumerate(reversed(modules))}
 | |
|         return self.__class__.__module_index
 | |
| 
 | |
|     def view_get(self, model=None):
 | |
|         key = (self.id, model)
 | |
|         result = self._view_get_cache.get(key)
 | |
|         if result:
 | |
|             return result
 | |
|         if self.inherit:
 | |
|             if self.inherit.model == model:
 | |
|                 return self.inherit.view_get(model=model)
 | |
|             else:
 | |
|                 arch = self.inherit.view_get(self.inherit.model)['arch']
 | |
|         else:
 | |
|             arch = self.arch
 | |
| 
 | |
|         views = self.__class__.search(['OR', [
 | |
|                     ('inherit', '=', self.id),
 | |
|                     ('model', '=', model),
 | |
|                     ], [
 | |
|                     ('id', '=', self.id),
 | |
|                     ('inherit', '!=', None),
 | |
|                     ],
 | |
|                 ])
 | |
|         views.sort(
 | |
|             key=lambda v: self._module_index.get(v.module, -1), reverse=True)
 | |
|         parser = etree.XMLParser(remove_comments=True, resolve_entities=False)
 | |
|         tree = etree.fromstring(arch, parser=parser)
 | |
|         decoder = PYSONDecoder({'context': Transaction().context})
 | |
|         for view in views:
 | |
|             if view.domain and not decoder.decode(view.domain):
 | |
|                 continue
 | |
|             if not view.arch or not view.arch.strip():
 | |
|                 continue
 | |
|             tree_inherit = etree.fromstring(view.arch, parser=parser)
 | |
|             tree = self.inherit_apply(tree, tree_inherit)
 | |
|         if model:
 | |
|             root = tree.getroottree().getroot()
 | |
|             self._translate(root, model, Transaction().language)
 | |
|         arch = etree.tostring(tree, encoding='utf-8').decode('utf-8')
 | |
|         result = {
 | |
|             'type': self.rng_type,
 | |
|             'view_id': self.id,
 | |
|             'arch': arch,
 | |
|             'field_childs': self.field_childs,
 | |
|             }
 | |
|         self._view_get_cache.set(key, result)
 | |
|         return result
 | |
| 
 | |
|     @classmethod
 | |
|     def inherit_apply(cls, tree, inherit):
 | |
|         root_inherit = inherit.getroottree().getroot()
 | |
|         for element in root_inherit:
 | |
|             expr = element.get('expr')
 | |
|             targets = tree.xpath(expr)
 | |
|             assert targets, "No elements found for expression %r" % expr
 | |
|             for target in targets:
 | |
|                 position = element.get('position', 'inside')
 | |
|                 new_tree = getattr(cls, '_inherit_apply_%s' % position)(
 | |
|                     tree, element, target)
 | |
|                 if new_tree:
 | |
|                     tree = new_tree
 | |
|         return tree
 | |
| 
 | |
|     @classmethod
 | |
|     def _inherit_apply_replace(cls, tree, element, target):
 | |
|         parent = target.getparent()
 | |
|         if parent is None:
 | |
|             tree, = element
 | |
|             return tree
 | |
|         cls._inherit_apply_after(tree, element, target)
 | |
|         parent.remove(target)
 | |
| 
 | |
|     @classmethod
 | |
|     def _inherit_apply_replace_attributes(cls, tree, element, target):
 | |
|         child, = element
 | |
|         for attr in child.attrib:
 | |
|             target.set(attr, child.get(attr))
 | |
| 
 | |
|     @classmethod
 | |
|     def _inherit_apply_inside(cls, tree, element, target):
 | |
|         target.extend(list(element))
 | |
| 
 | |
|     @classmethod
 | |
|     def _inherit_apply_after(cls, tree, element, target):
 | |
|         parent = target.getparent()
 | |
|         next_ = target.getnext()
 | |
|         if next_ is not None:
 | |
|             for child in element:
 | |
|                 index = parent.index(next_)
 | |
|                 parent.insert(index, child)
 | |
|         else:
 | |
|             parent.extend(list(element))
 | |
| 
 | |
|     @classmethod
 | |
|     def _inherit_apply_before(cls, tree, element, target):
 | |
|         parent = target.getparent()
 | |
|         for child in element:
 | |
|             index = parent.index(target)
 | |
|             parent.insert(index, child)
 | |
| 
 | |
|     @classmethod
 | |
|     def _translate(cls, element, model, language):
 | |
|         pool = Pool()
 | |
|         Translation = pool.get('ir.translation')
 | |
|         for attr in ['string', 'sum', 'confirm', 'help']:
 | |
|             if element.get(attr):
 | |
|                 translation = Translation.get_source(
 | |
|                     model, 'view', language, element.get(attr))
 | |
|                 if translation:
 | |
|                     element.set(attr, translation)
 | |
|         for child in element:
 | |
|             cls._translate(child, model, language)
 | |
| 
 | |
| 
 | |
| class ShowViewStart(ModelView):
 | |
|     'Show view'
 | |
|     __name__ = 'ir.ui.view.show.start'
 | |
|     __no_slots__ = True
 | |
| 
 | |
| 
 | |
| class ShowView(Wizard):
 | |
|     'Show view'
 | |
|     __name__ = 'ir.ui.view.show'
 | |
| 
 | |
|     class ShowStateView(StateView):
 | |
| 
 | |
|         def __init__(self, model_name, buttons):
 | |
|             StateView.__init__(self, model_name, None, buttons)
 | |
| 
 | |
|         def get_view(self, wizard, state_name):
 | |
|             pool = Pool()
 | |
|             View = pool.get('ir.ui.view')
 | |
|             view_id = Transaction().context.get('active_id')
 | |
|             if not view_id:
 | |
|                 # Set type to please ModuleTestCase.test_wizards
 | |
|                 return {'type': 'form'}
 | |
|             view = View(view_id)
 | |
|             Model = pool.get(view.model)
 | |
|             return Model.fields_view_get(view_id=view.id)
 | |
| 
 | |
|         def get_defaults(self, wizard, state_name, fields):
 | |
|             return {}
 | |
| 
 | |
|     start = ShowStateView('ir.ui.view.show.start', [
 | |
|             Button('Close', 'end', 'tryton-close', default=True),
 | |
|             ])
 | |
| 
 | |
| 
 | |
| class ViewTreeWidth(
 | |
|         fields.fmany2one(
 | |
|             'model_ref', 'model', 'ir.model,name', "Model",
 | |
|             required=True, ondelete='CASCADE'),
 | |
|         fields.fmany2one(
 | |
|             'field_ref', 'field,model', 'ir.model.field,name,model', "Field",
 | |
|             required=True, ondelete='CASCADE',
 | |
|             domain=[
 | |
|                 ('model', '=', Eval('model')),
 | |
|                 ]),
 | |
|         ModelSQL, ModelView):
 | |
|     "View Tree Width"
 | |
|     __name__ = 'ir.ui.view_tree_width'
 | |
|     model = fields.Char('Model', required=True)
 | |
|     field = fields.Char('Field', required=True)
 | |
|     user = fields.Many2One('res.user', 'User', required=True,
 | |
|         ondelete='CASCADE')
 | |
|     screen_width = fields.Integer("Screen Width")
 | |
|     width = fields.Integer('Width', required=True)
 | |
| 
 | |
|     @classmethod
 | |
|     def __setup__(cls):
 | |
|         super().__setup__()
 | |
|         table = cls.__table__()
 | |
|         cls.__rpc__.update({
 | |
|                 'set_width': RPC(readonly=False),
 | |
|                 'reset_width': RPC(readonly=False),
 | |
|                 })
 | |
|         cls._sql_indexes.add(
 | |
|             Index(
 | |
|                 table,
 | |
|                 (table.user, Index.Range()),
 | |
|                 (table.model, Index.Equality()),
 | |
|                 (table.field, Index.Equality())))
 | |
| 
 | |
|     def get_rec_name(self, name):
 | |
|         return f'{self.field_ref.rec_name} @ {self.model_ref.rec_name}'
 | |
| 
 | |
|     @classmethod
 | |
|     def search_rec_name(cls, name, clause):
 | |
|         operator = clause[1]
 | |
|         if operator.startswith('!') or operator.startswith('not '):
 | |
|             bool_op = 'AND'
 | |
|         else:
 | |
|             bool_op = 'OR'
 | |
|         return [bool_op,
 | |
|             ('model_ref.rec_name', *clause[1:]),
 | |
|             ('field_ref.rec_name', *clause[1:]),
 | |
|             ]
 | |
| 
 | |
|     @classmethod
 | |
|     def on_modification(cls, mode, records, field_names=None):
 | |
|         super().on_modification(mode, records, field_names=field_names)
 | |
|         ModelView._fields_view_get_cache.clear()
 | |
| 
 | |
|     @classmethod
 | |
|     def get_width(cls, model, width):
 | |
|         for screen_width in WIDTH_BREAKPOINTS:
 | |
|             if width >= screen_width:
 | |
|                 break
 | |
|         else:
 | |
|             screen_width = 0
 | |
| 
 | |
|         user = Transaction().user
 | |
|         records = cls.search([
 | |
|             ('user', '=', user),
 | |
|             ('model', '=', model),
 | |
|             ('screen_width', '=', screen_width),
 | |
|             ])
 | |
| 
 | |
|         if not records:
 | |
|             records = cls.search([
 | |
|                 ('user', '=', user),
 | |
|                 ('model', '=', model),
 | |
|                 ['OR',
 | |
|                     ('screen_width', '<=', screen_width),
 | |
|                     ('screen_width', '=', None),
 | |
|                     ],
 | |
|                 ],
 | |
|                 order=[
 | |
|                     ('screen_width', 'DESC NULLS LAST'),
 | |
|                     ])
 | |
|         widths = {}
 | |
|         for width in records:
 | |
|             if width.field not in widths:
 | |
|                 widths[width.field] = width.width
 | |
|         return widths
 | |
| 
 | |
|     @classmethod
 | |
|     def set_width(cls, model, fields, width):
 | |
|         '''
 | |
|         Set width for the current user on the model.
 | |
|         fields is a dictionary with key: field name and value: width.
 | |
|         '''
 | |
|         for screen_width in WIDTH_BREAKPOINTS:
 | |
|             if width >= screen_width:
 | |
|                 break
 | |
|         else:
 | |
|             screen_width = 0
 | |
| 
 | |
|         user_id = Transaction().user
 | |
|         records = cls.search([
 | |
|                 ('user', '=', user_id),
 | |
|                 ('model', '=', model),
 | |
|                 ('field', 'in', list(fields.keys())),
 | |
|                 ['OR',
 | |
|                     ('screen_width', '=', screen_width),
 | |
|                     ('screen_width', '=', None),
 | |
|                     ],
 | |
|                 ])
 | |
| 
 | |
|         fields = fields.copy()
 | |
|         to_save, to_delete = [], []
 | |
|         for tree_width in records:
 | |
|             if tree_width.screen_width == screen_width:
 | |
|                 if tree_width.field in fields:
 | |
|                     tree_width.width = fields.pop(tree_width.field)
 | |
|                     to_save.append(tree_width)
 | |
|                 else:
 | |
|                     to_delete.append(tree_width)
 | |
| 
 | |
|         for name, width in fields.items():
 | |
|             to_save.append(cls(
 | |
|                     user=user_id,
 | |
|                     model=model,
 | |
|                     field=name,
 | |
|                     screen_width=screen_width,
 | |
|                     width=width))
 | |
| 
 | |
|         if to_save:
 | |
|             cls.save(to_save)
 | |
|         if to_delete:
 | |
|             cls.delete(to_delete)
 | |
| 
 | |
|     @classmethod
 | |
|     def reset_width(cls, model, width):
 | |
|         for screen_width in WIDTH_BREAKPOINTS:
 | |
|             if width >= screen_width:
 | |
|                 break
 | |
|         else:
 | |
|             screen_width = 0
 | |
| 
 | |
|         user_id = Transaction().user
 | |
|         records = cls.search([
 | |
|                 ('user', '=', user_id),
 | |
|                 ('model', '=', model),
 | |
|                 ['OR',
 | |
|                     ('screen_width', '=', screen_width),
 | |
|                     ('screen_width', '=', None),
 | |
|                     ],
 | |
|                 ])
 | |
|         cls.delete(records)
 | |
| 
 | |
| 
 | |
| class ViewTreeOptional(
 | |
|         fields.fmany2one(
 | |
|             'model_ref', 'model', 'ir.model,name', "Model",
 | |
|             required=True, ondelete='CASCADE'),
 | |
|         fields.fmany2one(
 | |
|             'field_ref', 'field,model', 'ir.model.field,name,model', "Field",
 | |
|             required=True, ondelete='CASCADE',
 | |
|             domain=[
 | |
|                 ('model', '=', Eval('model')),
 | |
|                 ]),
 | |
|         ModelSQL, ModelView):
 | |
|     "View Tree Optional"
 | |
|     __name__ = 'ir.ui.view_tree_optional'
 | |
|     view = fields.Many2One(
 | |
|         'ir.ui.view', "View", required=True, ondelete='CASCADE')
 | |
|     user = fields.Many2One(
 | |
|         'res.user', "User", required=True, ondelete='CASCADE')
 | |
|     model = fields.Char("Model", required=True)
 | |
|     field = fields.Char("Field", required=True)
 | |
|     value = fields.Boolean("Value")
 | |
| 
 | |
|     @classmethod
 | |
|     def __setup__(cls):
 | |
|         super().__setup__()
 | |
|         cls.__rpc__.update({
 | |
|                 'set_optional': RPC(readonly=False),
 | |
|                 })
 | |
|         table = cls.__table__()
 | |
|         cls._sql_indexes.add(
 | |
|             Index(
 | |
|                 table,
 | |
|                 (table.user, Index.Range()),
 | |
|                 (table.view, Index.Range())))
 | |
| 
 | |
|     @classmethod
 | |
|     def __register__(cls, module):
 | |
|         pool = Pool()
 | |
|         View = pool.get('ir.ui.view')
 | |
|         table = cls.__table__()
 | |
|         view = View.__table__()
 | |
|         table_h = cls.__table_handler__(module)
 | |
|         cursor = Transaction().connection.cursor()
 | |
| 
 | |
|         # Migration from 7.2: rename view_id into view
 | |
|         table_h.column_rename('view_id', 'view')
 | |
| 
 | |
|         super().__register__(module)
 | |
| 
 | |
|         # Migration from 7.2: add model
 | |
|         cursor.execute(*table.update(
 | |
|                 [table.model],
 | |
|                 [view.select(view.model, where=view.id == table.view)],
 | |
|                 where=table.model == Null))
 | |
| 
 | |
|     @classmethod
 | |
|     def validate_fields(cls, records, fields_names):
 | |
|         super().validate_fields(records, fields_names)
 | |
|         cls.check_view(records, fields_names)
 | |
| 
 | |
|     @classmethod
 | |
|     def check_view(cls, records, fields_names=None):
 | |
|         if fields_names and 'view' not in fields_names:
 | |
|             return
 | |
|         for record in records:
 | |
|             if record.view and record.view.rng_type != 'tree':
 | |
|                 raise ViewError(gettext(
 | |
|                         'ir.msg_view_tree_optional_type',
 | |
|                         view=record.view.rec_name))
 | |
| 
 | |
|     @classmethod
 | |
|     def on_modification(cls, mode, record, field_names=None):
 | |
|         super().on_modification(mode, record, field_names=field_names)
 | |
|         ModelView._fields_view_get_cache.clear()
 | |
| 
 | |
|     @classmethod
 | |
|     def set_optional(cls, view_id, fields):
 | |
|         "Store optional field that must be displayed"
 | |
|         pool = Pool()
 | |
|         View = pool.get('ir.ui.view')
 | |
|         user = Transaction().user
 | |
|         view = View(view_id)
 | |
|         records = cls.search([
 | |
|                 ('view', '=', view.id),
 | |
|                 ('user', '=', user),
 | |
|                 ('field', 'in', list(fields)),
 | |
|                 ])
 | |
|         cls.delete(records)
 | |
|         to_create = []
 | |
|         for field, value in fields.items():
 | |
|             to_create.append({
 | |
|                     'view': view,
 | |
|                     'user': user,
 | |
|                     'model': view.model,
 | |
|                     'field': field,
 | |
|                     'value': bool(value),
 | |
|                     })
 | |
|         if to_create:
 | |
|             cls.create(to_create)
 | |
| 
 | |
| 
 | |
| class ViewTreeState(
 | |
|         fields.fmany2one(
 | |
|             'model_ref', 'model', 'ir.model,name', "Model",
 | |
|             required=True, ondelete='CASCADE'),
 | |
|         fields.fmany2one(
 | |
|             'child_field', 'child_name,model', 'ir.model.field,name,model',
 | |
|             "Child Field", ondelete='CASCADE',
 | |
|             domain=[
 | |
|                 ('model', '=', Eval('model')),
 | |
|                 ]),
 | |
|         ModelSQL, ModelView):
 | |
|     'View Tree State'
 | |
|     __name__ = 'ir.ui.view_tree_state'
 | |
|     _rec_name = 'model'
 | |
|     model = fields.Char('Model', required=True)
 | |
|     domain = fields.Char('Domain', required=True)
 | |
|     user = fields.Many2One('res.user', 'User', required=True,
 | |
|             ondelete='CASCADE')
 | |
|     child_name = fields.Char('Child Name')
 | |
|     nodes = fields.Text('Expanded Nodes')
 | |
|     selected_nodes = fields.Text('Selected Nodes')
 | |
| 
 | |
|     @classmethod
 | |
|     def __setup__(cls):
 | |
|         super().__setup__()
 | |
|         cls.__rpc__.update({
 | |
|                 'set': RPC(readonly=False, check_access=False),
 | |
|                 'get': RPC(check_access=False, cache=dict(days=1)),
 | |
|                 })
 | |
| 
 | |
|         table = cls.__table__()
 | |
|         cls._sql_indexes.add(
 | |
|             Index(
 | |
|                 table,
 | |
|                 (table.user, Index.Range()),
 | |
|                 (table.model, Index.Equality()),
 | |
|                 (table.child_name, Index.Equality()),
 | |
|                 (table.domain, Index.Equality())))
 | |
| 
 | |
|     @staticmethod
 | |
|     def default_nodes():
 | |
|         return '[]'
 | |
| 
 | |
|     @staticmethod
 | |
|     def default_selected_nodes():
 | |
|         return '[]'
 | |
| 
 | |
|     @classmethod
 | |
|     def set(cls, model, domain, child_name, nodes, selected_nodes):
 | |
|         # Normalize the json domain
 | |
|         domain = json.dumps(json.loads(domain), separators=(',', ':'))
 | |
|         current_user = Transaction().user
 | |
|         records = cls.search([
 | |
|                 ('user', '=', current_user),
 | |
|                 ('model', '=', model),
 | |
|                 ('domain', '=', domain),
 | |
|                 ('child_name', '=', child_name),
 | |
|                 ])
 | |
|         cls.delete(records)
 | |
|         cls.create([{
 | |
|                     'user': current_user,
 | |
|                     'model': model,
 | |
|                     'domain': domain,
 | |
|                     'child_name': child_name,
 | |
|                     'nodes': nodes,
 | |
|                     'selected_nodes': selected_nodes,
 | |
|                     }])
 | |
| 
 | |
|     @classmethod
 | |
|     def get(cls, model, domain, child_name):
 | |
|         # Normalize the json domain
 | |
|         domain = json.dumps(json.loads(domain), separators=(',', ':'))
 | |
|         current_user = Transaction().user
 | |
|         try:
 | |
|             expanded_info, = cls.search([
 | |
|                     ('user', '=', current_user),
 | |
|                     ('model', '=', model),
 | |
|                     ('domain', '=', domain),
 | |
|                     ('child_name', '=', child_name),
 | |
|                     ],
 | |
|                 limit=1)
 | |
|         except ValueError:
 | |
|             return (cls.default_nodes(), cls.default_selected_nodes())
 | |
|         state = cls(expanded_info)
 | |
|         return (state.nodes or cls.default_nodes(),
 | |
|             state.selected_nodes or cls.default_selected_nodes())
 | |
| 
 | |
| 
 | |
| class ViewSearch(
 | |
|         fields.fmany2one(
 | |
|             'model_ref', 'model', 'ir.model,name', "Model",
 | |
|             required=True, ondelete='CASCADE'),
 | |
|         ModelSQL, ModelView):
 | |
|     "View Search"
 | |
|     __name__ = 'ir.ui.view_search'
 | |
| 
 | |
|     name = fields.Char('Name', required=True)
 | |
|     model = fields.Char('Model', required=True)
 | |
|     domain = fields.Char('Domain', help="The PYSON domain.")
 | |
|     user = fields.Many2One('res.user', 'User', ondelete='CASCADE')
 | |
| 
 | |
|     @classmethod
 | |
|     def __setup__(cls):
 | |
|         super().__setup__()
 | |
|         cls.__rpc__.update({
 | |
|                 'get': RPC(check_access=False),
 | |
|                 'set': RPC(check_access=False, readonly=False),
 | |
|                 'unset': RPC(check_access=False, readonly=False),
 | |
|                 })
 | |
| 
 | |
|     @staticmethod
 | |
|     def default_user():
 | |
|         return Transaction().user
 | |
| 
 | |
|     @classmethod
 | |
|     def validate_fields(cls, searches, field_names):
 | |
|         super().validate_fields(searches, field_names)
 | |
|         cls.check_domain(searches, field_names)
 | |
| 
 | |
|     @classmethod
 | |
|     def check_domain(cls, searches, field_names):
 | |
|         decoder = PYSONDecoder()
 | |
|         if field_names and 'domain' not in field_names:
 | |
|             return
 | |
|         for search in searches:
 | |
|             try:
 | |
|                 value = decoder.decode(search.domain)
 | |
|             except Exception as exception:
 | |
|                 raise DomainError(
 | |
|                     gettext('ir.msg_view_search_invalid_domain',
 | |
|                         domain=search.domain,
 | |
|                         search=search.rec_name)) from exception
 | |
|             if isinstance(value, PYSON):
 | |
|                 if not value.types() == set([list]):
 | |
|                     raise DomainError(
 | |
|                         gettext('ir.msg_view_search_invalid_domain',
 | |
|                             domain=search.domain,
 | |
|                             search=search.rec_name))
 | |
|             elif not isinstance(value, list):
 | |
|                 raise DomainError(
 | |
|                     gettext('ir.msg_view_search_invalid_domain',
 | |
|                         domain=search.domain,
 | |
|                         search=search.rec_name))
 | |
|             else:
 | |
|                 try:
 | |
|                     fields.domain_validate(value)
 | |
|                 except Exception as exception:
 | |
|                     raise DomainError(
 | |
|                         gettext('ir.msg_view_search_invalid_domain',
 | |
|                             domain=search.domain,
 | |
|                             search=search.rec_name)) from exception
 | |
| 
 | |
|     @classmethod
 | |
|     def get(cls):
 | |
|         decoder = PYSONDecoder()
 | |
|         user = Transaction().user
 | |
|         searches = cls.search_read(['OR',
 | |
|                 ('user', '=', user),
 | |
|                 ('user', '=', None),
 | |
|                 ],
 | |
|             order=[('model', 'ASC'), ('name', 'ASC')],
 | |
|             fields_names=['id', 'name', 'model', 'domain', '_delete'])
 | |
|         result = {}
 | |
|         for search in searches:
 | |
|             result.setdefault(search['model'], []).append((
 | |
|                     search['id'],
 | |
|                     search['name'],
 | |
|                     decoder.decode(search['domain']),
 | |
|                     search['_delete']))
 | |
|         return result
 | |
| 
 | |
|     @classmethod
 | |
|     def set(cls, name, model, domain):
 | |
|         user = Transaction().user
 | |
|         search, = cls.create([{
 | |
|                     'name': name,
 | |
|                     'model': model,
 | |
|                     'domain': domain,
 | |
|                     'user': user,
 | |
|                     }])
 | |
|         return search.id
 | |
| 
 | |
|     @classmethod
 | |
|     def unset(cls, id):
 | |
|         user = Transaction().user
 | |
|         cls.delete(cls.search([
 | |
|                     ('id', '=', id),
 | |
|                     ('user', '=', user),
 | |
|                     ]))
 |