diff --git a/sao/src/view/tree.js b/sao/src/view/tree.js index 2382f11d80..441c37eb8f 100644 --- a/sao/src/view/tree.js +++ b/sao/src/view/tree.js @@ -72,8 +72,7 @@ this.view.columns.push(column); if (attributes.optional && (name !== this.exclude_field)) { - Sao.setdefault( - this.view.optionals, column.attributes.name, []).push(column); + this.view.optionals.push(column); } if (parseInt(attributes.sum || '0', 10)) { @@ -112,7 +111,7 @@ display_size: null, init: function(view_id, screen, xml, children_field) { this.children_field = children_field; - this.optionals = {}; + this.optionals = []; this.sum_widgets = new Map(); this.columns = []; this.selection_mode = (screen.attributes.selection_mode || @@ -365,19 +364,18 @@ }, tree_menu: function(evt) { const toggle = evt => { - let columns = evt.data; + let column = evt.data; let visible = jQuery(evt.delegateTarget).prop('checked'); - columns.forEach(c => c.set_visible(visible)); + column.set_visible(visible); this.save_optional(); this.display(); this.update_visible(); }; var menu = evt.data; menu.empty(); - for (let columns of Object.values(this.optionals)) { - let visible = columns.some(c => c.get_visible()); - let string = [...new Set(columns.map(c => c.attributes.string))] - .join(' / '); + for (let column of this.optionals) { + let visible = column.get_visible(); + let string = column.attributes.string; menu.append(jQuery('
  • ', { 'role': 'presentation', }).append(jQuery('', { @@ -388,7 +386,7 @@ }).append(jQuery('', { 'type': 'checkbox', 'checked': visible, - }).change(columns, toggle)) + }).change(column, toggle)) .append(' ' + string))))); } if (!jQuery.isEmptyObject(this.optionals)) { @@ -449,12 +447,9 @@ }))); }, save_optional: function(store=true) { - if (jQuery.isEmptyObject(this.optionals)) { - return; - } - var fields = {}; - for (let [name, columns] of Object.entries(this.optionals)) { - fields[name] = columns.every(c => !c.get_visible()); + let fields = {}; + for (let column of this.optionals) { + Sao.setdefault(fields, column.attributes.name, []).push(!column.get_visible()); } if (store) { var tree_optional_model = new Sao.Model( @@ -1042,7 +1037,7 @@ domain = inversion.simplify(domain); var decoder = new Sao.PYSON.Decoder(this.screen.context); var min_width = []; - var tree_column_optional = ( + var tree_column_optional = structuredClone( Sao.Screen.tree_column_optional[this.view_id] || {}); for (const column of this.columns) { visible_columns += 1; @@ -1053,8 +1048,9 @@ var optional; if ((column.attributes.optional) && Object.prototype.hasOwnProperty.call( - tree_column_optional, name)) { - optional = tree_column_optional[name]; + tree_column_optional, name) && + !jQuery.isEmptyObject(tree_column_optional[name])) { + optional = tree_column_optional[name].shift(); } else { optional = Boolean(parseInt( column.attributes.optional || '0', 10)); diff --git a/tryton/tryton/gui/window/view_form/view/list.py b/tryton/tryton/gui/window/view_form/view/list.py index 2abd38bd79..ff13c1587e 100644 --- a/tryton/tryton/gui/window/view_form/view/list.py +++ b/tryton/tryton/gui/window/view_form/view/list.py @@ -1,5 +1,7 @@ # 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 copy import csv import gettext import json @@ -369,7 +371,7 @@ class TreeXMLViewParser(XMLViewParser): self.view.treeview.append_column(column) if 'optional' in attributes and name != self.exclude_field: - self.view.optionals[column.name].append(column) + self.view.optionals.append(column) def _parse_button(self, node, attributes): if self.view.screen.screen_readonly: @@ -499,7 +501,7 @@ class ViewTree(View): def __init__(self, view_id, screen, xml, children_field): self.children_field = children_field - self.optionals = defaultdict(list) + self.optionals = [] self.sum_widgets = [] self.sum_box = Gtk.HBox() self.hbuttonbox = Gtk.HButtonBox() @@ -590,20 +592,19 @@ class ViewTree(View): self.treeview.append_column(column) def optional_menu(self, column): - def toggle(menuitem, columns): + def toggle(menuitem, column): visible = menuitem.get_active() - for column in columns: - column.set_visible(visible) + column.set_visible(visible) self.save_optional() widget = column.get_widget() menu = Gtk.Menu() - for name, columns in self.optionals.items(): - visible = any(c.get_visible() for c in columns) - title = ' / '.join({c.get_title() for c in columns}) + for column in self.optionals: + visible = column.get_visible() + title = column.get_title() menuitem = Gtk.CheckMenuItem(label=title) menuitem.set_active(visible) - menuitem.connect('toggled', toggle, columns) + menuitem.connect('toggled', toggle, column) menu.add(menuitem) if self.optionals: menu.add(Gtk.SeparatorMenuItem()) @@ -613,9 +614,9 @@ class ViewTree(View): popup(menu, widget) def save_optional(self): - fields = {} - for name, columns in self.optionals.items(): - fields[name] = all(not c.get_visible() for c in columns) + fields = defaultdict(list) + for column in self.optionals: + fields[column.name].append(not column.get_visible()) try: RPCExecute( 'model', 'ir.ui.view_tree_optional', 'set_optional', @@ -1287,8 +1288,8 @@ class ViewTree(View): domain.append(tab_domain) domain = simplify(domain) decoder = PYSONDecoder(self.screen.context) - tree_column_optional = self.screen.tree_column_optional.get( - self.view_id, {}) + tree_column_optional = copy.deepcopy( + self.screen.tree_column_optional.get(self.view_id, {})) for column in self.treeview.get_columns(): name = column.name if not name: @@ -1296,8 +1297,8 @@ class ViewTree(View): widget = self.get_column_widget(column) widget.set_editable() if ('optional' in widget.attrs - and column.name in tree_column_optional): - optional = tree_column_optional[column.name] + and tree_column_optional.get(column.name)): + optional = tree_column_optional[column.name].pop(0) else: optional = bool(int(widget.attrs.get('optional', '0'))) invisible = decoder.decode(widget.attrs.get('tree_invisible', '0')) diff --git a/trytond/trytond/ir/message.xml b/trytond/trytond/ir/message.xml index 4dfbe8d975..b4e84cd775 100644 --- a/trytond/trytond/ir/message.xml +++ b/trytond/trytond/ir/message.xml @@ -294,6 +294,10 @@ this repository contains the full copyright notices and license terms. --> A user can set only one width per occurrence of field. + + A user can set only one optional per occurrence of field on the same view. + + Wrong wizard model in keyword action "%(name)s". diff --git a/trytond/trytond/ir/ui/view.py b/trytond/trytond/ir/ui/view.py index 76d2028a01..1558ba8947 100644 --- a/trytond/trytond/ir/ui/view.py +++ b/trytond/trytond/ir/ui/view.py @@ -16,7 +16,7 @@ from sql.operators import Equal from trytond.cache import Cache, MemoryCache from trytond.i18n import gettext from trytond.model import ( - Exclude, Index, ModelSQL, ModelView, fields, sequence_ordered) + Exclude, Index, ModelSQL, ModelView, Unique, fields, sequence_ordered) from trytond.model.exceptions import ValidationError from trytond.pool import Pool from trytond.pyson import PYSON, Bool, Eval, If, PYSONDecoder @@ -466,7 +466,11 @@ class ViewTreeWidth( __name__ = 'ir.ui.view_tree_width' model = fields.Char('Model', required=True) field = fields.Char('Field', required=True) - occurrence = fields.Integer("Occurrence", required=True) + occurrence = fields.Integer( + "Occurrence", required=True, + domain=[ + ('occurrence', '>', 0), + ]) user = fields.Many2One('res.user', 'User', required=True, ondelete='CASCADE') screen_width = fields.Integer( @@ -611,7 +615,7 @@ class ViewTreeWidth( for tree_width in records: if tree_width.screen_width == screen_width: index = tree_width.occurrence - 1 - if index <= len(fields[tree_width.field]): + if index < len(fields[tree_width.field]): width = fields[tree_width.field][index] fields[tree_width.field][index] = None if width is not None: @@ -671,6 +675,11 @@ class ViewTreeOptional( 'res.user', "User", required=True, ondelete='CASCADE') model = fields.Char("Model", required=True) field = fields.Char("Field", required=True) + occurrence = fields.Integer( + "Occurrence", required=True, + domain=[ + ('occurrence', '>', 0), + ]) value = fields.Boolean("Value") @classmethod @@ -680,6 +689,12 @@ class ViewTreeOptional( 'set_optional': RPC(readonly=False), }) table = cls.__table__() + cls._sql_constraints += [ + ('model_field_occurrence_user_unique', + Unique(table, + table.view, table.field, table.occurrence, table.user), + 'ir.msg_view_tree_optional_field_occurrence_user_unique'), + ] cls._sql_indexes.add( Index( table, @@ -706,6 +721,10 @@ class ViewTreeOptional( [view.select(view.model, where=view.id == table.view)], where=table.model == Null)) + @classmethod + def default_occurrence(cls): + return 1 + @classmethod def validate_fields(cls, records, fields_names): super().validate_fields(records, fields_names) @@ -726,9 +745,34 @@ class ViewTreeOptional( super().on_modification(mode, record, field_names=field_names) ModelView._fields_view_get_cache.clear() + @classmethod + def get_optional(cls, view_id): + user = Transaction().user + + records = cls.search([ + ('view', '=', view_id), + ('user', '=', user), + ], + order=[('occurrence', 'ASC')]) + + optionals = defaultdict(list) + for optional in records: + if len(optionals[optional.field]) + 1 < optional.occurrence: + for _ in range( + optional.occurrence + - len(optionals[optional.field]) - 1): + optionals[optional.field].append(None) + optionals[optional.field].insert( + optional.occurrence, optional.value) + return optionals + @classmethod def set_optional(cls, view_id, fields): - "Store optional field that must be displayed" + ''' + Store optional fields that must be displayed. + fields is dictionary with field name as key and a list of boolean as + value. + ''' pool = Pool() View = pool.get('ir.ui.view') user = Transaction().user @@ -736,20 +780,32 @@ class ViewTreeOptional( 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) + ('field', 'in', list(fields.keys())), + ], + order=[('occurrence', 'DESC')]) + + fields = copy.deepcopy(fields) + to_save = [] + + for tree_optional in records: + index = tree_optional.occurrence - 1 + if index < len(fields[tree_optional.field]): + tree_optional.value = fields[tree_optional.field][index] + fields[tree_optional.field][index] = None + to_save.append(tree_optional) + + for name, optionals in fields.items(): + for occurrence, optional in enumerate(optionals, start=1): + if optional is not None: + to_save.append(cls( + view=view, + user=user, + model=view.model, + field=name, + occurrence=occurrence, + value=optional)) + if to_save: + cls.save(to_save) class ViewTreeState( diff --git a/trytond/trytond/ir/view/ui_view_tree_optional_form.xml b/trytond/trytond/ir/view/ui_view_tree_optional_form.xml index bc0cb2edff..2a2cba43ed 100644 --- a/trytond/trytond/ir/view/ui_view_tree_optional_form.xml +++ b/trytond/trytond/ir/view/ui_view_tree_optional_form.xml @@ -10,7 +10,10 @@ this repository contains the full copyright notices and license terms. -->