diff --git a/sao/src/common.js b/sao/src/common.js index 727b4a62fa..e27005e691 100644 --- a/sao/src/common.js +++ b/sao/src/common.js @@ -788,7 +788,7 @@ 'method': 'model.' + this.attributes.relation + '.search_read', 'params': [domain, 0, null, null, fields, context] - }, record.model.session); + }, record.model.session, true, false); prm.done(result => { var selection = []; for (const x of result) { diff --git a/sao/src/model.js b/sao/src/model.js index b58474bc3c..29a5134017 100644 --- a/sao/src/model.js +++ b/sao/src/model.js @@ -24,12 +24,14 @@ } return added; }, - execute: function(method, params, context={}, async=true) { + execute: function( + method, params, context={}, async=true, + process_exception=true) { var args = { 'method': 'model.' + this.name + '.' + method, 'params': params.concat(context) }; - return Sao.rpc(args, this.session, async); + return Sao.rpc(args, this.session, async, process_exception); }, copy: function(records, context) { if (jQuery.isEmptyObject(records)) { @@ -609,7 +611,7 @@ is_loaded: function(name) { return ((this.id < 0) || (name in this._loaded)); }, - load: function(name, async=true) { + load: function(name, async=true, process_exception=true) { var fname; var prm; if (this.destroyed || this.is_loaded(name)) { @@ -748,7 +750,7 @@ } var result = this.model.execute('read', [ Object.keys(id2record).map( e => parseInt(e, 10)), - fnames_to_fetch], context, async); + fnames_to_fetch], context, async, process_exception); const succeed = (values, exception=false) => { var id2value = {}; for (const e of values) { diff --git a/sao/src/rpc.js b/sao/src/rpc.js index b6749cd018..d15aba61bf 100644 --- a/sao/src/rpc.js +++ b/sao/src/rpc.js @@ -3,7 +3,7 @@ (function() { 'use strict'; - Sao.rpc = function(args, session=null, async=true) { + Sao.rpc = function(args, session=null, async=true, process_exception=true) { var dfd = jQuery.Deferred(), result; if (!session) { @@ -32,7 +32,10 @@ Sao.common.warning.run('', Sao.i18n.gettext('Unable to reach the server.')) .always(dfd.reject); - } else if (data.error) { + } else if (data.error && !process_exception) { + console.debug(`RPC error calling ${args}: ${data.error[0]}: ${data.error[1]}.`); + dfd.reject(); + } else if (data.error && process_exception) { var name, msg, description; if (data.error[0] == 'UserWarning') { name = data.error[1][0]; diff --git a/sao/src/view/tree.js b/sao/src/view/tree.js index 20019ce423..a910dd89b8 100644 --- a/sao/src/view/tree.js +++ b/sao/src/view/tree.js @@ -1389,7 +1389,7 @@ if (field_name && !record.is_loaded(field_name)) { // Prefetch the first field to prevent promises in // Cell.render - record.load(field_name).done(redraw); + record.load(field_name, true, false).done(redraw); return; } else { row.redraw(selected, expanded); @@ -1686,7 +1686,8 @@ } }; if (!this.record.is_loaded(this.children_field)) { - this.record.load(this.children_field).done(update_expander); + this.record.load(this.children_field, true, false) + .done(update_expander); } else { update_expander(); } @@ -1761,7 +1762,8 @@ this.rows = []; }, expand_children: function(selected, expanded) { - return this.record.load(this.children_field).done(() => { + return this.record.load( + this.children_field, true, false).done(() => { if (this.rows.length === 0) { var children = this.record.field_get_client( this.children_field); @@ -1957,7 +1959,8 @@ if (widget) { var callback = display_callback(widget); if (!this.record.is_loaded(column.attributes.name)) { - this.record.load(column.attributes.name).done(callback); + this.record.load(column.attributes.name, true, false) + .done(callback); } else { callback(); } @@ -2304,7 +2307,7 @@ } }; if (!record.is_loaded(this.attributes.name)) { - record.load(this.attributes.name).done(render); + record.load(this.attributes.name, true, false).done(render); } else { render(); } @@ -2353,7 +2356,7 @@ } }; if (!record.is_loaded(this.attributes.name)) { - record.load(this.attributes.name).done(render); + record.load(this.attributes.name, true, false).done(render); } else { render(); } @@ -2405,10 +2408,21 @@ cell.show(); } }; + const render_error = () => { + var text = Sao.i18n.gettext('#ERROR'); + cell.text(text).attr('title', text); + }; + if (!record.is_loaded(this.attributes.name)) { - record.load(this.attributes.name).done(render); + record.load(this.attributes.name, true, false) + .done(render) + .fail(render_error); } else { - render(); + if (record.exception) { + render_error(); + } else { + render(); + } } return cell; }, @@ -2832,10 +2846,19 @@ set_src(null); } }; + const render_error = () => { + cell.attr('src', null); + }; if (!record.is_loaded(this.attributes.name)) { - record.load(this.attributes.name).done(render); + record.load(this.attributes.name, true, false) + .done(render) + .fail(render_error); } else { - render(); + if (render.exception) { + render_error(); + } else { + render(); + } } return cell; } diff --git a/tryton/tryton/common/selection.py b/tryton/tryton/common/selection.py index b3ee916ff3..a7e420165b 100644 --- a/tryton/tryton/common/selection.py +++ b/tryton/tryton/common/selection.py @@ -73,7 +73,7 @@ class SelectionMixin(object): try: result = RPCExecute('model', self.attrs['relation'], 'search_read', domain, 0, None, None, fields, - context=context) + context=context, process_exception=False) except RPCException: result = False if isinstance(result, list): diff --git a/tryton/tryton/gui/window/view_form/model/record.py b/tryton/tryton/gui/window/view_form/model/record.py index a92cd0bcae..bf27dbb077 100644 --- a/tryton/tryton/gui/window/view_form/model/record.py +++ b/tryton/tryton/gui/window/view_form/model/record.py @@ -44,6 +44,11 @@ class Record: self.destroyed = False def __getitem__(self, name): + self.load(name) + if name != '*': + return self.group.fields[name] + + def load(self, name, process_exception=True): if not self.destroyed and self.id >= 0 and name not in self._loaded: id2record = { self.id: self, @@ -130,7 +135,8 @@ class Record: exception = False try: values = RPCExecute('model', self.model_name, 'read', - list(id2record.keys()), fnames, context=ctx) + list(id2record.keys()), fnames, context=ctx, + process_exception=process_exception) except RPCException: values = [{'id': x} for x in id2record] default_values = dict((f, None) for f in fnames) @@ -146,8 +152,6 @@ class Record: for key in record.modified_fields: value.pop(key, None) record.set(value, modified=False) - if name != '*': - return self.group.fields[name] def __repr__(self): return '' % (self.id, self.model_name, id(self)) diff --git a/tryton/tryton/gui/window/view_form/view/list_gtk/editabletree.py b/tryton/tryton/gui/window/view_form/view/list_gtk/editabletree.py index 0d72f9f8f3..e21aa05e99 100644 --- a/tryton/tryton/gui/window/view_form/view/list_gtk/editabletree.py +++ b/tryton/tryton/gui/window/view_form/view/list_gtk/editabletree.py @@ -1,6 +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 gettext +import logging from itertools import chain, cycle, islice from gi.repository import Gdk, GLib, Gtk @@ -9,6 +10,7 @@ from tryton.common import MODELACCESS from tryton.common.datetime_ import Date, Time _ = gettext.gettext +logger = logging.getLogger(__name__) def focusable_cells(column, editable=True): @@ -51,7 +53,14 @@ class TreeView(Gtk.TreeView): if not column.name: continue widget = self.view.get_column_widget(column) - field = record[column.name] + try: + record.load(column.name, process_exception=False) + except Exception: + logger.error( + f"Error loading '{column.name}' for {record}", + exc_info=True) + return (None, None) + field = record.group.fields[column.name] field.state_set(record, states=('readonly', 'invisible')) invisible = field.get_state_attrs(record).get('invisible', False) if not column.get_visible(): @@ -83,7 +92,8 @@ class EditableTreeView(TreeView): def on_quit_cell( self, current_record, column, renderer, value, callback=None): - field = current_record[column.name] + current_record.load(column.name, process_exception=False) + field = current_record.group.fields[column.name] widget = self.view.get_column_widget(column) # The value has not changed and is valid ... do nothing. @@ -271,7 +281,7 @@ class EditableTreeView(TreeView): create=(event.keyval == Gdk.KEY_F3), value=value, callback=callback) else: - field = record[column.name] + field = record.group.fields[column.name] if isinstance(entry, Gtk.Entry): entry.set_max_length(int(field.attrs.get('size', 0))) record.modified_fields.setdefault(column.name) diff --git a/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py b/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py index 443b5d0cfd..e6373875c6 100644 --- a/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py +++ b/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py @@ -2,6 +2,7 @@ # this repository contains the full copyright notices and license terms. import datetime import gettext +import logging import os import webbrowser from functools import partial, wraps @@ -31,6 +32,7 @@ from tryton.gui.window.win_form import WinForm from tryton.gui.window.win_search import WinSearch _ = gettext.gettext +logger = logging.getLogger(__name__) COLORS = {n: v for n, v in zip( ['muted', 'success', 'warning', 'danger'], @@ -50,6 +52,23 @@ def send_keys(renderer, editable, position, treeview): editable.connect('changed', changed) +def catch_errors(error_value=_('#ERROR')): + def decorator(func): + @wraps(func) + def wrapper(self, record): + if record.exception: + return error_value + try: + return func(self, record) + except Exception: + logger.error( + f"Error calling {func.__name__} for {self} with {record}", + exc_info=True) + return error_value + return wrapper + return decorator + + def realized(func): @wraps(func) @@ -143,7 +162,7 @@ class Cell(object): if not store: store = self.view.treeview.get_model() record = store.get_value(iter_, 0) - field = record[self.attrs['name']] + field = record.group.fields[self.attrs['name']] return record, field def _set_visual(self, cell, record): @@ -330,9 +349,9 @@ class GenericText(Cell): callback=None): raise NotImplementedError + @catch_errors() def get_textual_value(self, record): - if not record: - return '' + record.load(self.attrs['name'], process_exception=False) return record[self.attrs['name']].get_client(record) def value_from_text(self, record, text, callback=None): @@ -396,9 +415,9 @@ class Int(GenericText): return [self.renderer_suffix] return [] + @catch_errors() def get_textual_value(self, record): - if not record: - return '' + record.load(self.attrs['name'], process_exception=False) return record[self.attrs['name']].get_client( record, factor=self.factor, grouping=self.grouping) @@ -464,9 +483,9 @@ class Date(GenericText): else: return '%x' + @catch_errors() def get_textual_value(self, record): - if not record: - return '' + record.load(self.attrs['name'], process_exception=False) value = record[self.attrs['name']].get_client(record) if value: return value.strftime(self.renderer.props.format) @@ -487,9 +506,9 @@ class Time(Date): else: return '%X' + @catch_errors() def get_textual_value(self, record): - if not record: - return '' + record.load(self.attrs['name'], process_exception=False) value = record[self.attrs['name']].get_client(record) if value is not None: if isinstance(value, datetime.datetime): @@ -549,7 +568,9 @@ class Binary(GenericText): def suffixes(self): return [self.renderer_save, self.renderer_select] + @catch_errors() def get_textual_value(self, record): + record.load(self.attrs['name'], process_exception=False) field = record[self.attrs['name']] if hasattr(field, 'get_size'): size = field.get_size(record) @@ -604,6 +625,11 @@ class _BinaryIcon(Cell): def view(self): return self.binary.view + @catch_errors(False) + def _fetch_data(self, record): + record.fetch(self.attrs['name'], process_exception=False) + return True + class _BinarySave(_BinaryIcon): icon_name = 'tryton-save' @@ -631,6 +657,9 @@ class _BinarySave(_BinaryIcon): @CellCache.cache def setter(self, column, cell, store, iter_, user_data=None): record, field = self._get_record_field_from_iter(iter_, store) + if not self._fetch_data(record): + cell.set_property('visible', False) + return if hasattr(field, 'get_size'): size = field.get_size(record) else: @@ -671,6 +700,9 @@ class _BinarySelect(_BinaryIcon): @CellCache.cache def setter(self, column, cell, store, iter_, user_data=None): record, field = self._get_record_field_from_iter(iter_, store) + if not self._fetch_data(record): + cell.set_property('visible', False) + return if hasattr(field, 'get_size'): size = field.get_size(record) else: @@ -710,6 +742,9 @@ class _BinaryOpen(_BinarySave): def setter(self, column, cell, store, iter_, user_data=None): super().setter(column, cell, store, iter_) record, field = self._get_record_field_from_iter(iter_, store) + if not self._fetch_data(record): + cell.set_property('visible', False) + return filename_field = record.group.fields.get(self.attrs.get('filename')) filename = filename_field.get(record) if not filename: @@ -731,22 +766,33 @@ class Image(GenericText): @CellCache.cache def setter(self, column, cell, store, iter_, user_data=None): record, field = self._get_record_field_from_iter(iter_, store) - value = field.get_client(record) - if isinstance(value, int): - if value > CONFIG['image.max_size']: - value = None - else: - value = field.get_data(record) + value = self._get_data(record) + if value is None: + cell.set_property('pixbuf', None) + return pixbuf = data2pixbuf(value) if pixbuf: pixbuf = common.resize_pixbuf(pixbuf, self.width, self.height) cell.set_property('pixbuf', pixbuf) self._set_visual(cell, record) + @catch_errors(None) + def _get_data(self, record): + record.load(self.attrs['name'], process_exception=False) + field = record[self.attrs['name']] + value = field.get_client(record) + if isinstance(value, int): + if value > CONFIG['image.max_size']: + value = None + else: + value = field.get_data(record) + return value + + @catch_errors() def get_textual_value(self, record): - if not record: - return '' - return str(record[self.attrs['name']].get_size(record)) + record.load(self.attrs['name'], process_exception=False) + field = record[self.attrs['name']] + return str(field.get_size(record)) class M2O(GenericText): @@ -974,9 +1020,11 @@ class O2O(M2O): class O2M(GenericText): align = 0.5 + @catch_errors() def get_textual_value(self, record): - return '( ' + str(len(record[self.attrs['name']] - .get_eval(record))) + ' )' + record.load(self.attrs['name'], process_exception=False) + field = record[self.attrs['name']] + return '( ' + str(len(field.get_eval(record))) + ' )' def value_from_text(self, record, text, callback=None): if callback: @@ -1056,13 +1104,16 @@ class Selection(GenericText, SelectionMixin, PopdownMixin): def get_value(self, record, field): return field.get(record) + @catch_errors() def get_textual_value(self, record): related = self.attrs['name'] + ':string' + record.load(related, process_exception=False) if not self.view.editable and related in record.value: return record.value[related] + record.load(self.attrs['name'], process_exception=False) field = record[self.attrs['name']] - self.update_selection(record, field) + self.update_selection(record, field, process_exception=False) value = self.get_value(record, field) text = dict(self.selection).get(value, '') if value and not text: @@ -1138,13 +1189,16 @@ class MultiSelection(GenericText, SelectionMixin): if callback: callback() + @catch_errors() def get_textual_value(self, record): related = self.attrs['name'] + ':string' + record.load(related, process_exception=False) if not self.view.editable and related in record.value: return ";".join(record.value[related]) + record.load(self.attrs['name'], process_exception=False) field = record[self.attrs['name']] - self.update_selection(record, field) + self.update_selection(record, field, process_exception=False) selection = dict(self.selection) values = [] for value in field.get_eval(record): @@ -1191,6 +1245,7 @@ class Reference(M2O): _, value = value.split(',') return int(value) + @catch_errors() def get_textual_value(self, record): value = super().get_textual_value(record) if value: @@ -1232,8 +1287,11 @@ class Dict(GenericText): super().setter(column, cell, store, iter_, user_data=None) cell.props.editable = False + @catch_errors() def get_textual_value(self, record): - return '(%s)' % len(record[self.attrs['name']].get_client(record)) + record.load(self.attrs['name'], process_exception=False) + field = record[self.attrs['name']] + return '(%s)' % len(field.get_client(record)) class ProgressBar(Cell): @@ -1275,8 +1333,11 @@ class ProgressBar(Cell): callback=None): raise NotImplementedError + @catch_errors() def get_textual_value(self, record): - return record[self.attrs['name']].get_client(record, factor=100) or '' + record.load(self.attrs['name'], process_exception=False) + field = record[self.attrs['name']] + return field.get_client(record, factor=100) or '' def value_from_text(self, record, text, callback=None): field = record[self.attrs['name']]