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 @@
+
+
+