Store column optional per occurrence

Closes #14832
This commit is contained in:
Cédric Krier
2026-05-13 15:08:23 +02:00
parent c92f64c1d6
commit 25d1b72c64
7 changed files with 124 additions and 65 deletions
+15 -19
View File
@@ -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('<li/>', {
'role': 'presentation',
}).append(jQuery('<a/>', {
@@ -388,7 +386,7 @@
}).append(jQuery('<input/>', {
'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));
+17 -16
View File
@@ -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'))
+4
View File
@@ -294,6 +294,10 @@ this repository contains the full copyright notices and license terms. -->
<field name="text">A user can set only one width per occurrence of field.</field>
</record>
<record model="ir.message" id="msg_view_tree_optional_field_occurrence_user_unique">
<field name="text">A user can set only one optional per occurrence of field on the same view.</field>
</record>
<record model="ir.message" id="msg_action_wrong_wizard_model">
<field name="text">Wrong wizard model in keyword action "%(name)s".</field>
</record>
+74 -18
View File
@@ -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(
@@ -10,7 +10,10 @@ this repository contains the full copyright notices and license terms. -->
<label name="model_ref"/>
<field name="model_ref"/>
<label name="field_ref"/>
<field name="field_ref"/>
<group id="field" col="-1">
<field name="field_ref"/>
<field name="occurrence"/>
</group>
<label name="value"/>
<field name="value"/>
@@ -6,5 +6,6 @@ this repository contains the full copyright notices and license terms. -->
<field name="user" expand="1"/>
<field name="model_ref" expand="1"/>
<field name="field_ref" expand="1"/>
<field name="occurrence" optional="1"/>
<field name="value"/>
</tree>
+9 -11
View File
@@ -494,7 +494,6 @@ class ModelView(Model):
if field._type in {'one2many', 'many2many'}})
if type == 'tree':
user = Transaction().user
width, _ = Transaction().context.get('screen_size', (None, None))
if Transaction().context.get('view_tree_width'):
ViewTreeWidth = pool.get('ir.ui.view_tree_width')
@@ -502,11 +501,7 @@ class ModelView(Model):
if view_id:
ViewTreeOptional = pool.get('ir.ui.view_tree_optional')
viewtreeoptionals = ViewTreeOptional.search([
('view', '=', view_id),
('user', '=', user),
])
fields_optional = {o.field: o.value for o in viewtreeoptionals}
fields_optional = ViewTreeOptional.get_optional(view_id)
fields_def = cls.__parse_fields(
tree_root, type,
@@ -571,7 +566,7 @@ class ModelView(Model):
if fields_width is None:
fields_width = collections.defaultdict(list)
if fields_optional is None:
fields_optional = {}
fields_optional = collections.defaultdict(list)
if _fields_attrs is None:
fields_attrs = {}
else:
@@ -645,10 +640,13 @@ class ModelView(Model):
if width is not None:
element.set('width', str(width))
if element.get('optional'):
if element.get('name') in fields_optional:
optional = str(int(
fields_optional[element.get('name')]))
element.set('optional', optional)
try:
value = fields_optional[element.get('name')].pop(0)
except IndexError:
pass
else:
if value is not None:
element.set('optional', str(int(value)))
encoder = PYSONEncoder()
if element.tag == 'button':