Modify some styles of the SAO of Tryton to match the style of Naliia

This commit is contained in:
2025-09-17 22:28:55 -05:00
parent a6e2dd969c
commit 80ec51d689
11 changed files with 19553 additions and 2 deletions

View File

@@ -6,6 +6,7 @@ ARG TRYTOND_VERSION
ENV SERIES=${TRYTOND_VERSION}
RUN npm install -g bower
RUN curl https://downloads.tryton.org/${SERIES}/tryton-sao-last.tgz | tar zxf - -C /
COPY naliia_sao_custom/sao.less /package/src/sao.less
RUN cd /package && bower install --allow-root
FROM python:3.11-bullseye
@@ -15,4 +16,4 @@ RUN apt-get update && apt-get install -y postgresql-client
# TOMADO DE: https://hg.tryton.org/tryton-docker/file/tip/6.6/Dockerfile
COPY --from=builder-node /package /var/lib/trytond/www
COPY naliia_sao_custom/ /var/lib/trytond/www/

11051
naliia_sao_custom/custom.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
//Mudar cores dos ícones do menu lateral
Sao.config.icon_colors = {
'toolbar_icons':'white',
'default':'#487b50'
};
//Configuração do título do programa (canto superior esquerdo)
Sao.config.bug_url = 'https://ecovida.org.br';
Sao.config.title = 'RedeEcovida';

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

3010
naliia_sao_custom/model.js Normal file

File diff suppressed because it is too large Load Diff

1637
naliia_sao_custom/sao.less Normal file

File diff suppressed because it is too large Load Diff

881
naliia_sao_custom/view.py Normal file
View File

@@ -0,0 +1,881 @@
# 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),
]))

2882
naliia_sao_custom/window.js Normal file

File diff suppressed because it is too large Load Diff

Submodule web_app updated: 7670838215...49c5fffa01