diff --git a/lims_interface/data.py b/lims_interface/data.py index 03193833..f8c395e7 100644 --- a/lims_interface/data.py +++ b/lims_interface/data.py @@ -70,6 +70,10 @@ class Adapter: obj = fields.Many2One(field.related_model.model, field.string) elif field.type in ('binary', 'icon'): obj = fields.Binary(field.string) + elif field.type == 'selection': + selection = [tuple(v.split(':', 1)) + for v in field.selection.splitlines() if v] + obj = fields.Selection(selection, field.string) obj.name = field.name res[field.name] = obj groups = max(groups, field.group or 0) @@ -137,6 +141,10 @@ class GroupedAdapter: obj = fields.Many2One(field.related_model.model, field.string) elif field.type in ('binary', 'icon'): obj = fields.Binary(field.string) + elif field.type == 'selection': + selection = [tuple(v.split(':', 1)) + for v in field.selection.splitlines() if v] + obj = fields.Selection(selection, field.string) obj.name = field.name res[field.name] = obj obj = fields.Integer('ID') @@ -365,13 +373,30 @@ class Data(ModelSQL, ModelView): 'name': field.name, 'string': field.string, 'type': FIELD_TYPE_TRYTON[field.type], - 'relation': (field.related_model.model if - field.related_model else None), - 'states': encoder.encode(states), 'help': field.help, 'domain': field.domain, + 'states': encoder.encode(states), 'sortable': True, } + if field.type == 'many2one': + res[field.name]['relation'] = (field.related_model.model if + field.related_model else None) + if field.type == 'selection': + selection = [tuple(v.split(':', 1)) + for v in field.selection.splitlines() if v] + res[field.name]['selection'] = selection + res[field.name]['selection_change_with'] = [] + res[field.name]['sort'] = False + if field.type == 'reference': + selection = [] + for model in Model.search([]): + selection.append((model.model, model.name)) + res[field.name]['selection'] = selection + if field.type in ['date', 'time', 'datetime', 'timestamp']: + res[field.name]['format'] = PYSONEncoder().encode( + '%H:%M:%S.%f') + if field.type in ['float', 'numeric']: + res[field.name]['digits'] = encoder.encode((16, field.digits)) if field.inputs: inputs = [] for input_ in field.inputs.split(): @@ -387,17 +412,6 @@ class Data(ModelSQL, ModelView): func_name = '%s_%s' % ('on_change_with', field.name) cls.__rpc__.setdefault(func_name, RPC(instantiate=0)) - if field.type == 'reference': - selection = [] - for model in Model.search([]): - selection.append((model.model, model.name)) - res[field.name]['selection'] = selection - if field.type in ['datetime', 'timestamp']: - res[field.name]['format'] = PYSONEncoder().encode( - '%H:%M:%S.%f') - if field.type in ['float', 'numeric']: - res[field.name]['digits'] = encoder.encode((16, field.digits)) - for i in range(0, groups): field_description = None for rep in interface.grouped_repetitions: @@ -882,13 +896,30 @@ class GroupedData(ModelView): 'name': field.name, 'string': field.string, 'type': FIELD_TYPE_TRYTON[field.type], - 'relation': (field.related_model.model if - field.related_model else None), 'readonly': bool(readonly or field.formula or field.readonly), 'help': field.help, 'domain': field.domain, 'states': '{}', } + if field.type == 'many2one': + res[field.name]['relation'] = (field.related_model.model if + field.related_model else None) + if field.type == 'selection': + selection = [tuple(v.split(':', 1)) + for v in field.selection.splitlines() if v] + res[field.name]['selection'] = selection + res[field.name]['selection_change_with'] = [] + res[field.name]['sort'] = False + if field.type == 'reference': + selection = [] + for model in Model.search([]): + selection.append((model.model, model.name)) + res[field.name]['selection'] = selection + if field.type in ['date', 'time', 'datetime', 'timestamp']: + res[field.name]['format'] = PYSONEncoder().encode( + '%H:%M:%S.%f') + if field.type in ['float', 'numeric']: + res[field.name]['digits'] = encoder.encode((16, field.digits)) if field.inputs: res[field.name]['on_change_with'] = field.inputs.split() + [ 'data'] @@ -896,28 +927,17 @@ class GroupedData(ModelView): func_name = '%s_%s' % ('on_change_with', field.name) cls.__rpc__.setdefault(func_name, RPC(instantiate=0)) - if field.type == 'reference': - selection = [] - for model in Model.search([]): - selection.append((model.model, model.name)) - res[field.name]['selection'] = selection - if field.type in ['datetime', 'timestamp']: - res[field.name]['format'] = PYSONEncoder().encode( - '%H:%M:%S.%f') - if field.type in ['float', 'numeric']: - res[field.name]['digits'] = encoder.encode((16, field.digits)) - res['data'] = { 'name': 'data', 'string': 'Data', 'type': 'many2one', + 'readonly': True, + 'help': '', + 'states': '{}', 'relation': 'lims.interface.data', 'relation_field': 'group_%s' % group, 'relation_fields': (Data.fields_get(level=level - 1) if level > 0 else []), - 'readonly': True, - 'help': '', - 'states': '{}', } return res diff --git a/lims_interface/interface.py b/lims_interface/interface.py index 25c39321..b3607bcc 100644 --- a/lims_interface/interface.py +++ b/lims_interface/interface.py @@ -11,6 +11,7 @@ import io import csv import hashlib import tempfile +import json from openpyxl import load_workbook from decimal import Decimal from datetime import datetime, date, time @@ -24,7 +25,7 @@ from trytond.model import (Workflow, ModelView, ModelSQL, fields, from trytond.wizard import (Wizard, StateTransition, StateView, StateAction, Button) from trytond.pool import Pool -from trytond.pyson import PYSONEncoder, Eval, Bool, Not, And, Or +from trytond.pyson import PYSONDecoder, PYSONEncoder, Eval, Bool, Not, And, Or from trytond.transaction import Transaction from trytond.i18n import gettext from trytond.exceptions import UserError @@ -59,6 +60,8 @@ FIELD_TYPES = [ ('binary', 'binary', 'File', 'fields.Binary', 'BLOB', bytes, bytearray), ('reference', 'reference', 'Reference', 'fields.Reference', 'VARCHAR', str, None), + ('selection', 'selection', 'Selection', 'fields.Selection', 'VARCHAR', str, + None), ] FIELD_TYPE_SELECTION = [(x[0], x[2]) for x in FIELD_TYPES] @@ -377,6 +380,7 @@ class Interface(Workflow, ModelSQL, ModelView): related_line_field= column.related_line_field, related_model=column.related_model, + selection=column.selection, formula=(column.expression if column.expression and column.expression.startswith('=') else @@ -409,6 +413,7 @@ class Interface(Workflow, ModelSQL, ModelView): help=expression, domain=column.domain, related_model=column.related_model, + selection=column.selection, formula=(expression if expression and expression.startswith('=') else @@ -436,6 +441,7 @@ class Interface(Workflow, ModelSQL, ModelView): transfer_field=column.transfer_field, related_line_field=column.related_line_field, related_model=column.related_model, + selection=column.selection, formula=(expression if expression and expression.startswith('=') else None), inputs=(get_inputs(expression) @@ -468,6 +474,7 @@ class Interface(Workflow, ModelSQL, ModelView): transfer_field=column.transfer_field, related_line_field=column.related_line_field, related_model=column.related_model, + selection=column.selection, formula=(column.expression if column.expression and column.expression.startswith('=') else @@ -818,6 +825,7 @@ class Column(sequence_ordered(), ModelSQL, ModelView): ('image', 'Image'), ('binary', 'File'), ('reference', 'Reference'), + ('selection', 'Selection'), ], 'Field Type', states=_states, depends=_depends) related_model = fields.Many2One('ir.model', 'Related Model', states={ @@ -826,6 +834,14 @@ class Column(sequence_ordered(), ModelSQL, ModelView): 'readonly': _states['readonly'], }, depends=['type_', 'interface_state']) + selection = fields.Text('Selection', + states={ + 'required': Eval('type_') == 'selection', + 'invisible': Eval('type_') != 'selection', + 'readonly': _states['readonly'], + }, + depends=['type_', 'interface_state'], + help='A couple of key and label separated by ":" per line.') default_value = fields.Char('Default value', states={'readonly': _states['readonly'] | Bool(Eval('expression'))}, depends=['expression', 'interface_state']) @@ -953,6 +969,8 @@ class Column(sequence_ordered(), ModelSQL, ModelView): for column in columns: column.check_alias() column.check_default_value() + column.check_domain() + column.check_selection() def check_alias(self): for symbol in self.alias: @@ -961,39 +979,70 @@ class Column(sequence_ordered(), ModelSQL, ModelView): symbol=symbol, name=self.name)) def check_default_value(self): - if self.default_value: - if self.type_ in [ - 'datetime', 'time', 'timestamp', 'timedelta', - 'icon', 'image', 'binary', 'reference', - ]: + if not self.default_value: + return + if self.type_ in [ + 'datetime', 'time', 'timestamp', 'timedelta', + 'icon', 'image', 'binary', 'reference', + ]: + raise UserError(gettext( + 'lims_interface.invalid_default_value_type', + name=self.name)) + if self.type_ == 'boolean': + try: + int(self.default_value) + except Exception: raise UserError(gettext( - 'lims_interface.invalid_default_value_type', + 'lims_interface.invalid_default_value_boolean', name=self.name)) - if self.type_ == 'boolean': - try: - int(self.default_value) - except Exception: - raise UserError(gettext( - 'lims_interface.invalid_default_value_boolean', - name=self.name)) - elif self.type_ == 'date': - try: - str2date(self.default_value, self.interface.language) - except Exception: - raise UserError(gettext( - 'lims_interface.invalid_default_value_date', - name=self.name)) - elif self.type_ == 'many2one': - get_model_resource( - self.related_model.model, self.default_value, self.name) - else: - ftype = FIELD_TYPE_PYTHON[self.type_] - try: - ftype(self.default_value) - except Exception: - raise UserError(gettext( - 'lims_interface.invalid_default_value', - value=self.default_value, name=self.name)) + elif self.type_ == 'date': + try: + str2date(self.default_value, self.interface.language) + except Exception: + raise UserError(gettext( + 'lims_interface.invalid_default_value_date', + name=self.name)) + elif self.type_ == 'many2one': + get_model_resource( + self.related_model.model, self.default_value, self.name) + else: + ftype = FIELD_TYPE_PYTHON[self.type_] + try: + ftype(self.default_value) + except Exception: + raise UserError(gettext( + 'lims_interface.invalid_default_value', + value=self.default_value, name=self.name)) + + def check_domain(self): + if not self.domain: + return + try: + value = PYSONDecoder().decode(self.domain) + except Exception: + raise UserError(gettext( + 'lims_interface.invalid_domain', + name=self.name)) + if not isinstance(value, list): + raise UserError(gettext( + 'lims_interface.invalid_domain', + name=self.name)) + + def check_selection(self): + if self.type_ != 'selection': + return + try: + dict(json.loads(self.get_selection_json())) + except Exception: + raise UserError(gettext( + 'lims_interface.invalid_selection', + name=self.name)) + + def get_selection_json(self, name=None): + db_selection = self.selection or '' + selection = [[w.strip() for w in v.split(':', 1)] + for v in db_selection.splitlines() if v] + return json.dumps(selection, separators=(',', ':')) def formula_error(self): if not self.expression: @@ -1115,15 +1164,16 @@ class ViewColumn(sequence_ordered(), ModelSQL, ModelView): c.check_analysis_specific() def check_analysis_specific(self): - if self.analysis_specific: - if self.search([ - ('view', '=', self.view.id), - ('analysis_specific', '=', True), - ('id', '!=', self.id), - ]): - raise UserError(gettext( - 'lims_interface.msg_analysis_specific', - view=self.view.name)) + if not self.analysis_specific: + return + if self.search([ + ('view', '=', self.view.id), + ('analysis_specific', '=', True), + ('id', '!=', self.id), + ]): + raise UserError(gettext( + 'lims_interface.msg_analysis_specific', + view=self.view.name)) class CopyInterfaceColumnStart(ModelView): @@ -1186,6 +1236,7 @@ class CopyInterfaceColumn(Wizard): 'type_': origin.type_, 'related_model': (origin.related_model and origin.related_model or None), + 'selection': origin.selection, 'default_value': origin.default_value, 'readonly': origin.readonly, 'transfer_field': origin.transfer_field, diff --git a/lims_interface/locale/es.po b/lims_interface/locale/es.po index ca462b63..33d7f7f1 100644 --- a/lims_interface/locale/es.po +++ b/lims_interface/locale/es.po @@ -158,6 +158,10 @@ msgctxt "field:lims.interface.column,related_model:" msgid "Related Model" msgstr "Modelo relacionado" +msgctxt "field:lims.interface.column,selection:" +msgid "Selection" +msgstr "Selección" + msgctxt "field:lims.interface.column,singleton:" msgid "Is a singleton value" msgstr "Es un valor único" @@ -450,6 +454,10 @@ msgctxt "field:lims.interface.table.field,related_model:" msgid "Related Model" msgstr "Modelo relacionado" +msgctxt "field:lims.interface.table.field,selection:" +msgid "Selection" +msgstr "Selección" + msgctxt "field:lims.interface.table.field,string:" msgid "String" msgstr "Etiqueta" @@ -506,6 +514,10 @@ msgctxt "field:lims.interface.table.grouped_field,related_model:" msgid "Related Model" msgstr "Modelo relacionado" +msgctxt "field:lims.interface.table.grouped_field,selection:" +msgid "Selection" +msgstr "Selección" + msgctxt "field:lims.interface.table.grouped_field,string:" msgid "String" msgstr "Etiqueta" @@ -642,6 +654,10 @@ msgstr "" "En columnas agrupadas el sufijo _XX será reemplazado por la repetición " "correspondiente" +msgctxt "help:lims.interface.column,selection:" +msgid "A couple of key and label separated by \":\" per line." +msgstr "Una pareja de claves y valores separados por \":\" en cada línea." + msgctxt "help:lims.interface.column,singleton:" msgid "Is a fixed value (column:row) in source file" msgstr "Es un campo fijo (columna:fila) en el archivo de origen" @@ -755,6 +771,10 @@ msgctxt "model:ir.message,text:invalid_default_value_type" msgid "The field type in \"%(name)s\" is not valid for default values." msgstr "El Tipo de campo en \"%(name)s\" no permite valores por defecto." +msgctxt "model:ir.message,text:invalid_domain" +msgid "Invalid domain in column \"%(name)s\"." +msgstr "El dominio en la columna \"%(name)s\" es inválido." + msgctxt "model:ir.message,text:invalid_interface_charset" msgid "" "The charset of the file does not match the one defined in the interface." @@ -762,6 +782,10 @@ msgstr "" "El conjunto de caracteres del archivo no coincide con el definido en la " "interfaz." +msgctxt "model:ir.message,text:invalid_selection" +msgid "Invalid selection in column \"%(name)s\"." +msgstr "Las opciones de selección en la columna \"%(name)s\" son inválidas." + msgctxt "model:ir.message,text:msg_analysis_specific" msgid "There cannot be more than one column per analysis in view \"%(view)s\"" msgstr "No puede haber más de una columna por análisis en la vista \"%(view)s\"" @@ -1047,6 +1071,10 @@ msgctxt "selection:lims.interface.column,type_:" msgid "Reference" msgstr "Referencia" +msgctxt "selection:lims.interface.column,type_:" +msgid "Selection" +msgstr "Selección" + msgctxt "selection:lims.interface.column,type_:" msgid "Text (multi-line)" msgstr "Texto (multi-línea)" @@ -1131,6 +1159,10 @@ msgctxt "selection:lims.interface.table.field,type:" msgid "Reference" msgstr "Referencia" +msgctxt "selection:lims.interface.table.field,type:" +msgid "Selection" +msgstr "Selección" + msgctxt "selection:lims.interface.table.field,type:" msgid "Text (multi-line)" msgstr "Texto (multi-línea)" @@ -1195,6 +1227,10 @@ msgctxt "selection:lims.interface.table.grouped_field,type:" msgid "Reference" msgstr "Referencia" +msgctxt "selection:lims.interface.table.grouped_field,type:" +msgid "Selection" +msgstr "Selección" + msgctxt "selection:lims.interface.table.grouped_field,type:" msgid "Text (multi-line)" msgstr "Texto (multi-línea)" diff --git a/lims_interface/message.xml b/lims_interface/message.xml index ef73c7c9..5350b157 100644 --- a/lims_interface/message.xml +++ b/lims_interface/message.xml @@ -22,6 +22,12 @@ The resource set as default value in "%(name)s" is not valid or it does not exist. + + Invalid domain in column "%(name)s". + + + Invalid selection in column "%(name)s". + File "%(file_name)s" already exists as origin. diff --git a/lims_interface/table.py b/lims_interface/table.py index 04721910..b189a8ff 100644 --- a/lims_interface/table.py +++ b/lims_interface/table.py @@ -86,6 +86,7 @@ class TableField(ModelSQL, ModelView): transfer_field = fields.Boolean('Is a transfer field') related_line_field = fields.Many2One('ir.model.field', 'Related Field') related_model = fields.Many2One('ir.model', 'Related Model') + selection = fields.Text('Selection') domain = fields.Char('Domain Value') formula = fields.Char('On Change With Formula') inputs = fields.Char('On Change With Inputs') @@ -117,6 +118,7 @@ class TableGroupedField(ModelSQL, ModelView): 'Field Type', required=False) help = fields.Text('Help') related_model = fields.Many2One('ir.model', 'Related Model') + selection = fields.Text('Selection') domain = fields.Char('Domain Value') formula = fields.Char('On Change With Formula') inputs = fields.Function(fields.Char('On Change With Inputs'), diff --git a/lims_interface/view/interface_column_form.xml b/lims_interface/view/interface_column_form.xml index 09db21d5..22b78451 100644 --- a/lims_interface/view/interface_column_form.xml +++ b/lims_interface/view/interface_column_form.xml @@ -16,6 +16,9 @@