From ec11c9e83bcf4fad33c254160377966d9fa4971b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20=C3=89vrard?= Date: Thu, 8 Jan 2026 01:06:16 +0100 Subject: [PATCH] Allow to store empty files in Binary fields Closes #14476 --- sao/src/model.js | 2 +- sao/src/view/form.js | 11 ++++++++--- sao/src/view/tree.js | 3 ++- .../tryton/gui/window/view_form/model/field.py | 4 ++-- .../window/view_form/view/form_gtk/binary.py | 7 ++++--- .../window/view_form/view/list_gtk/widget.py | 6 +++--- trytond/trytond/model/fields/binary.py | 10 ++++++---- trytond/trytond/tests/test_field_binary.py | 18 ++++++++++++++---- 8 files changed, 40 insertions(+), 21 deletions(-) diff --git a/sao/src/model.js b/sao/src/model.js index f3300f57ba..8c0e00debc 100644 --- a/sao/src/model.js +++ b/sao/src/model.js @@ -2863,7 +2863,7 @@ return previous != value; }, get_size: function(record) { - var data = record._values[this.name] || 0; + var data = record._values[this.name]; if ((data instanceof Uint8Array) || (typeof(data) == 'string')) { return data.length; diff --git a/sao/src/view/form.js b/sao/src/view/form.js index 3c9bdcb83a..b2617b7afe 100644 --- a/sao/src/view/form.js +++ b/sao/src/view/form.js @@ -4768,17 +4768,22 @@ function eval_pyson(value){ } else { size = field.get(record).length; } - this.size.val(Sao.common.humanize(size, 'B')); + let is_empty = ((size === undefined) || (size === null)); + if (!is_empty) { + this.size.val(Sao.common.humanize(size, 'B')); + } else { + this.size.val(''); + } if (this.text) { this.text.val(this.filename_field.get(record) || ''); - if (size) { + if (!is_empty) { this.but_open.parent().sao_show(); } else { this.but_open.parent().sao_hide(); } } - this.update_buttons(Boolean(size)); + this.update_buttons(!is_empty); }, key_press: function(evt) { var editable = !this.text.prop('readonly'); diff --git a/sao/src/view/tree.js b/sao/src/view/tree.js index bc988df6ce..2c485cbf17 100644 --- a/sao/src/view/tree.js +++ b/sao/src/view/tree.js @@ -3086,7 +3086,8 @@ } else { size = this.field.get(record).length; } - return size? Sao.common.humanize(size, 'B') : ''; + let file_exist = !((size === undefined) || (size === null)); + return file_exist ? Sao.common.humanize(size, 'B') : ''; }, update_text: function(cell, record) { var text = this.get_textual_value(record); diff --git a/tryton/tryton/gui/window/view_form/model/field.py b/tryton/tryton/gui/window/view_form/model/field.py index 01b941f849..6a42accc44 100644 --- a/tryton/tryton/gui/window/view_form/model/field.py +++ b/tryton/tryton/gui/window/view_form/model/field.py @@ -1140,13 +1140,13 @@ class BinaryField(Field): return self.get(record) def set_client(self, record, value, force_change=False): - self._set_file_cache(record, value or b'') + self._set_file_cache(record, value) self.sig_changed(record) record.validate(softvalidation=True) record.set_modified(self.name) def get_size(self, record): - result = record.value.get(self.name) or 0 + result = record.value.get(self.name) if isinstance(result, _FileCache): result = os.stat(result.path).st_size elif isinstance(result, (str, bytes)): diff --git a/tryton/tryton/gui/window/view_form/view/form_gtk/binary.py b/tryton/tryton/gui/window/view_form/view/form_gtk/binary.py index 5a3918e149..5fff8cf167 100644 --- a/tryton/tryton/gui/window/view_form/view/form_gtk/binary.py +++ b/tryton/tryton/gui/window/view_form/view/form_gtk/binary.py @@ -233,12 +233,13 @@ class Binary(BinaryMixin, Widget): size = self.field.get_size(self.record) else: size = len(self.field.get(self.record)) - self.wid_size.set_text(common.humanize(size or 0, 'B')) + self.wid_size.set_text( + common.humanize(size, 'B') if size is not None else '') reset_position(self.wid_size) if self.wid_text: self.wid_text.set_text(self.filename_field.get(self.record) or '') reset_position(self.wid_text) - if size: + if size is not None: icon, tooltip = 'tryton-open', _("Open...") else: icon, tooltip = None, '' @@ -250,7 +251,7 @@ class Binary(BinaryMixin, Widget): pixbuf = None self.wid_text.set_icon_from_pixbuf(pos, pixbuf) self.wid_text.set_icon_tooltip_text(pos, tooltip) - self.update_buttons(bool(size)) + self.update_buttons(size is not None) return True def set_value(self): 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 d132eade03..a236890a75 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 @@ -604,7 +604,7 @@ class Binary(GenericText): size = field.get_size(record) else: size = len(field.get(record)) - return common.humanize(size, 'B') if size else '' + return common.humanize(size, 'B') if size is not None else '' def value_from_text(self, record, text, callback=None): if callback: @@ -694,7 +694,7 @@ class _BinarySave(_BinaryIcon): size = len(field.get(record)) field.state_set(record, states=['invisible']) invisible = field.get_state_attrs(record).get('invisible', False) - cell.set_property('visible', not invisible and size) + cell.set_property('visible', not invisible and size is not None) self._set_visual(cell, record) @@ -763,7 +763,7 @@ class _BinarySelect(_BinaryIcon): invisible = field.get_state_attrs(record).get('invisible', False) readonly = self.attrs.get('readonly', field.get_state_attrs(record).get('readonly', False)) - if readonly or size: + if readonly or size is not None: cell.set_property('visible', False) else: cell.set_property('visible', not invisible) diff --git a/trytond/trytond/model/fields/binary.py b/trytond/trytond/model/fields/binary.py index 342ba219d7..ada3426941 100644 --- a/trytond/trytond/model/fields/binary.py +++ b/trytond/trytond/model/fields/binary.py @@ -70,7 +70,6 @@ class Binary(Field): '%s.%s' % (model.__name__, name), '') if format_ == 'size': converter = len - default = 0 result = defaultdict(type(default)) if self.file_id: @@ -104,7 +103,7 @@ class Binary(Field): if i['id'] in result: continue value = i[name] - if value: + if value is not None: value = converter(value) else: value = default @@ -152,9 +151,12 @@ class Binary(Field): for ids, value in zip(args, args): self.queue_for_removal(Model, name, ids) if self.file_id: + if value is not None: + file_name = filestore.set(value, prefix) + else: + file_name = None columns = [Column(table, self.file_id), Column(table, name)] - values = [ - filestore.set(value, prefix) if value else None, None] + values = [file_name, None] else: columns = [Column(table, name)] values = [self.sql_format(value)] diff --git a/trytond/trytond/tests/test_field_binary.py b/trytond/trytond/tests/test_field_binary.py index 8f9d66c036..107e16de0f 100644 --- a/trytond/trytond/tests/test_field_binary.py +++ b/trytond/trytond/tests/test_field_binary.py @@ -90,10 +90,9 @@ class FieldBinaryTestCase(TestCase): "Test create binary with empty" Binary = Pool().get('test.binary_required') - with self.assertRaises(RequiredValidationError): - binary, = Binary.create([{ - 'binary': cast(b''), - }]) + binary, = Binary.create([{ + 'binary': cast(b''), + }]) @with_transaction() def test_create_required_with_invalid_sql_constraint(self): @@ -156,6 +155,17 @@ class FieldBinaryTestCase(TestCase): with self.assertRaises(ValueError): binary.binary = Literal('foo') + @with_transaction() + def test_read_empty(self): + "Test reading an empty binary field" + Binary = Pool().get('test.binary') + + binary, = Binary.create([{ + 'binary': cast(b''), + }]) + + self.assertEqual(binary.binary, b'') + @with_transaction() def test_read_size(self): "Test read binary size"