From 197a988ef1ecfe79bee4316ecb88051bf0736d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 5 Oct 2026 23:21:28 +0200 Subject: [PATCH] Ensure readonly fields are copied with the default value Since 6c611c520744, the readonly of field is enforced so the copied value of readonly field must be the default value to skip the check. --- modules/account/account.py | 2 +- modules/account/move.py | 2 +- modules/account_asset/asset.py | 6 +++--- modules/account_payment_stripe/payment.py | 16 ++-------------- modules/party/party.py | 6 +++++- modules/sale_advance_payment/sale.py | 2 +- modules/sale_project_task/sale.py | 6 +++++- trytond/trytond/model/modelsingleton.py | 11 ++++++++--- trytond/trytond/tests/test_tryton.py | 8 ++++++++ 9 files changed, 34 insertions(+), 25 deletions(-) diff --git a/modules/account/account.py b/modules/account/account.py index 89dbbd3d3f..71c1dc4795 100644 --- a/modules/account/account.py +++ b/modules/account/account.py @@ -1274,7 +1274,7 @@ class Account( else: default = default.copy() default.setdefault('template', None) - default.setdefault('deferrals', []) + default.setdefault('deferrals') new_accounts = super().copy(accounts, default=default) cls._rebuild_tree('parent', None, 0) return new_accounts diff --git a/modules/account/move.py b/modules/account/move.py index 62b6d3e2f0..5e4e579a81 100644 --- a/modules/account/move.py +++ b/modules/account/move.py @@ -1571,7 +1571,7 @@ class Line(DescriptionOriginMixin, MoveLineMixin, ModelSQL, ModelView): default.setdefault('state', 'draft') default.setdefault('move', None) default.setdefault('reconciliation', None) - default.setdefault('reconciliations_delegated', []) + default.setdefault('reconciliations_delegated') return super().copy(lines, default=default) @classmethod diff --git a/modules/account_asset/asset.py b/modules/account_asset/asset.py index 04bbaffd55..d01732c734 100644 --- a/modules/account_asset/asset.py +++ b/modules/account_asset/asset.py @@ -728,12 +728,12 @@ class Asset(Workflow, ModelSQL, ModelView): default = {} else: default = default.copy() - default.setdefault('lines', []) - default.setdefault('update_moves', []) + default.setdefault('lines') + default.setdefault('update_moves') default.setdefault('number', None) default.setdefault('supplier_invoice_line', None) default.setdefault('move') - default.setdefault('revisions', []) + default.setdefault('revisions') return super().copy(assets, default=default) @classmethod diff --git a/modules/account_payment_stripe/payment.py b/modules/account_payment_stripe/payment.py index 4a95c0c98e..e2e12a1411 100644 --- a/modules/account_payment_stripe/payment.py +++ b/modules/account_payment_stripe/payment.py @@ -282,10 +282,6 @@ class Payment(StripeCustomerMethodMixin, CheckoutMixin, metaclass=PoolMeta): def default_stripe_capturable(cls): return False - @classmethod - def default_stripe_idempotency_key(cls): - return uuid.uuid4().hex - @fields.depends('process_method', 'stripe_token', 'stripe_payment_intent_id', 'stripe_customer_source', 'stripe_customer_source_selection', @@ -349,9 +345,7 @@ class Payment(StripeCustomerMethodMixin, CheckoutMixin, metaclass=PoolMeta): if mode == 'create': if 'stripe_idempotency_key' not in values: # Ensure to get a different key for each record - # default methods are called only once - values['stripe_idempotency_key'] = ( - cls.default_stripe_idempotency_key()) + values['stripe_idempotency_key'] = uuid.uuid4().hex return values @classmethod @@ -843,10 +837,6 @@ class Refund(Workflow, ModelSQL, ModelView): def get_stripe_amount(self, name): return int(self.amount * 10 ** self.currency.digits) - @classmethod - def default_stripe_idempotency_key(cls): - return uuid.uuid4().hex - @fields.depends('payment', '_parent_payment.currency') def on_change_with_currency(self, name=None): return self.payment.currency if self.payment else None @@ -865,9 +855,7 @@ class Refund(Workflow, ModelSQL, ModelView): if mode == 'create': if 'stripe_idempotency_key' not in values: # Ensure to get a different key for each record - # default methods are called only once - values['stripe_idempotency_key'] = ( - cls.default_stripe_idempotency_key()) + values['stripe_idempotency_key'] = uuid.uuid4().hex return values @classmethod diff --git a/modules/party/party.py b/modules/party/party.py index b2ac705813..52a70c3ba4 100644 --- a/modules/party/party.py +++ b/modules/party/party.py @@ -968,6 +968,10 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView): pass return code + @classmethod + def default_eu_vat_valid(cls): + return False + def pre_validate(self): super().pre_validate() self.check_code() @@ -1076,7 +1080,7 @@ class Identifier(sequence_ordered(), DeactivableMixin, ModelSQL, ModelView): @classmethod def copy(cls, identifiers, default=None): default = default.copy() if default is not None else {} - default.setdefault('eu_vat_valid') + default.setdefault('eu_vat_valid', cls.default_eu_vat_valid()) default.setdefault('eu_vat_validated_at') return super().copy(identifiers, default=default) diff --git a/modules/sale_advance_payment/sale.py b/modules/sale_advance_payment/sale.py index 2dfce7f45a..e0a4446faa 100644 --- a/modules/sale_advance_payment/sale.py +++ b/modules/sale_advance_payment/sale.py @@ -223,7 +223,7 @@ class AdvancePaymentLine(ModelSQL, ModelView): default = {} else: default = default.copy() - default.setdefault('invoice_lines', []) + default.setdefault('invoice_lines') return super().copy(lines, default) def create_invoice(self): diff --git a/modules/sale_project_task/sale.py b/modules/sale_project_task/sale.py index f0374838b0..174050b133 100644 --- a/modules/sale_project_task/sale.py +++ b/modules/sale_project_task/sale.py @@ -128,6 +128,10 @@ class Sale(metaclass=PoolMeta): return Configuration(1).get_multivalue( 'sale_task_creation_method', **pattern) + @classmethod + def default_tasks_to_create(cls): + return False + @classmethod def check_method(cls, sales, field_names=None): super().check_method(sales, field_names=field_names) @@ -148,7 +152,7 @@ class Sale(metaclass=PoolMeta): def copy(cls, sales, default=None): default = default.copy() if default is not None else {} default.setdefault('tasks_state') - default.setdefault('tasks_to_create') + default.setdefault('tasks_to_create', cls.default_tasks_to_create()) return super().copy(sales, default=default) @classmethod diff --git a/trytond/trytond/model/modelsingleton.py b/trytond/trytond/model/modelsingleton.py index b88ca7e1b6..a0eef5bf89 100644 --- a/trytond/trytond/model/modelsingleton.py +++ b/trytond/trytond/model/modelsingleton.py @@ -114,12 +114,17 @@ class ModelSingleton(ModelStorage): return res @classmethod - def default_get(cls, fields_names, with_rec_name=True): + def default_get( + cls, fields_names=None, with_rec_name=True, with_default=True): + if fields_names is None: + fields_names = cls._fields.keys() + if '_timestamp' in fields_names: fields_names = list(fields_names) fields_names.remove('_timestamp') - default = super().default_get(fields_names, - with_rec_name=with_rec_name) + default = super().default_get( + fields_names, + with_rec_name=with_rec_name, with_default=with_default) singleton = cls.get_singleton() if singleton: if with_rec_name: diff --git a/trytond/trytond/tests/test_tryton.py b/trytond/trytond/tests/test_tryton.py index 5004009c33..65789c26e2 100644 --- a/trytond/trytond/tests/test_tryton.py +++ b/trytond/trytond/tests/test_tryton.py @@ -927,6 +927,7 @@ class ModuleTestCase(DBTestCase): continue if not issubclass(model, ModelStorage): continue + default_values = model.default_get(with_rec_name=False) with self.subTest(model=mname): model.copy([]) if copy.call_args: @@ -939,6 +940,13 @@ class ModuleTestCase(DBTestCase): fields = { k.split('.', 1)[0] for k in default.keys()} self.assertLessEqual(fields, klass._fields.keys()) + for field_name in fields: + field = getattr(klass, field_name) + if field.readonly: + with self.subTest(field=field_name): + self.assertEqual( + default[field_name], + default_values.get(field_name)) copy.reset_mock() @with_transaction()