details:   https://code.tryton.org/tryton/commit/9a7e061acd01
branch:    default
user:      Nicolas Évrard <[email protected]>
date:      Thu Jan 08 01:06:16 2026 +0100
description:
        Allow to store empty files in Binary fields

        Closes #14476
diffstat:

 sao/src/model.js                                           |   2 +-
 sao/src/view/form.js                                       |  11 ++++++--
 sao/src/view/tree.js                                       |   3 +-
 tryton/tryton/gui/window/view_form/model/field.py          |   4 +-
 tryton/tryton/gui/window/view_form/view/form_gtk/binary.py |   7 +++--
 tryton/tryton/gui/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(-)

diffs (201 lines):

diff -r f62e20c70717 -r 9a7e061acd01 sao/src/model.js
--- a/sao/src/model.js  Thu Jun 18 15:49:10 2026 +0200
+++ b/sao/src/model.js  Thu Jan 08 01:06:16 2026 +0100
@@ -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 -r f62e20c70717 -r 9a7e061acd01 sao/src/view/form.js
--- a/sao/src/view/form.js      Thu Jun 18 15:49:10 2026 +0200
+++ b/sao/src/view/form.js      Thu Jan 08 01:06:16 2026 +0100
@@ -4768,17 +4768,22 @@
             } 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 -r f62e20c70717 -r 9a7e061acd01 sao/src/view/tree.js
--- a/sao/src/view/tree.js      Thu Jun 18 15:49:10 2026 +0200
+++ b/sao/src/view/tree.js      Thu Jan 08 01:06:16 2026 +0100
@@ -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 -r f62e20c70717 -r 9a7e061acd01 
tryton/tryton/gui/window/view_form/model/field.py
--- a/tryton/tryton/gui/window/view_form/model/field.py Thu Jun 18 15:49:10 
2026 +0200
+++ b/tryton/tryton/gui/window/view_form/model/field.py Thu Jan 08 01:06:16 
2026 +0100
@@ -1140,13 +1140,13 @@
         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 -r f62e20c70717 -r 9a7e061acd01 
tryton/tryton/gui/window/view_form/view/form_gtk/binary.py
--- a/tryton/tryton/gui/window/view_form/view/form_gtk/binary.py        Thu Jun 
18 15:49:10 2026 +0200
+++ b/tryton/tryton/gui/window/view_form/view/form_gtk/binary.py        Thu Jan 
08 01:06:16 2026 +0100
@@ -233,12 +233,13 @@
             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 @@
                 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 -r f62e20c70717 -r 9a7e061acd01 
tryton/tryton/gui/window/view_form/view/list_gtk/widget.py
--- a/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py        Thu Jun 
18 15:49:10 2026 +0200
+++ b/tryton/tryton/gui/window/view_form/view/list_gtk/widget.py        Thu Jan 
08 01:06:16 2026 +0100
@@ -604,7 +604,7 @@
             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 @@
             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 @@
         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 -r f62e20c70717 -r 9a7e061acd01 trytond/trytond/model/fields/binary.py
--- a/trytond/trytond/model/fields/binary.py    Thu Jun 18 15:49:10 2026 +0200
+++ b/trytond/trytond/model/fields/binary.py    Thu Jan 08 01:06:16 2026 +0100
@@ -70,7 +70,6 @@
             '%s.%s' % (model.__name__, name), '')
         if format_ == 'size':
             converter = len
-            default = 0
         result = defaultdict(type(default))
 
         if self.file_id:
@@ -104,7 +103,7 @@
             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 @@
         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 -r f62e20c70717 -r 9a7e061acd01 trytond/trytond/tests/test_field_binary.py
--- a/trytond/trytond/tests/test_field_binary.py        Thu Jun 18 15:49:10 
2026 +0200
+++ b/trytond/trytond/tests/test_field_binary.py        Thu Jan 08 01:06:16 
2026 +0100
@@ -90,10 +90,9 @@
         "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):
@@ -157,6 +156,17 @@
             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"
         Binary = Pool().get('test.binary')

Reply via email to