details:   https://code.tryton.org/tryton/commit/6e6176c1eb20
branch:    default
user:      Cédric Krier <[email protected]>
date:      Mon Aug 31 23:21:03 2026 +0200
description:
        Distinct readonly and editable states
diffstat:

 demo/production.py                                                             
                 |   8 +-
 modules/account/account.py                                                     
                 |  25 +-
 modules/account/move.py                                                        
                 |   6 +-
 modules/account_asset/asset.py                                                 
                 |  47 +++--
 modules/account_budget/account.py                                              
                 |   7 +-
 modules/account_invoice/invoice.py                                             
                 |  39 ++--
 modules/account_invoice/tests/scenario_invoice_check_source.rst                
                 |   2 +-
 modules/account_invoice_defer/account.py                                       
                 |   3 +-
 modules/account_product/product.py                                             
                 |   2 +-
 modules/account_statement/statement.py                                         
                 |  17 +-
 modules/account_stock_eu_excise/product.py                                     
                 |   4 +-
 modules/account_stock_eu_excise/sale.py                                        
                 |  11 +-
 modules/account_stock_shipment_cost/account.py                                 
                 |   2 +-
 modules/account_tax_rule_country/purchase.py                                   
                 |   7 +-
 modules/account_tax_rule_country/sale.py                                       
                 |   7 +-
 modules/analytic_budget/analytic_account.py                                    
                 |   2 +-
 modules/carrier_weight/carrier.py                                              
                 |   4 +-
 modules/party/party.py                                                         
                 |   3 +-
 modules/party_relationship/party.py                                            
                 |   3 +-
 modules/product/product.py                                                     
                 |   7 +-
 modules/product_cost_warehouse/product.py                                      
                 |   2 +-
 modules/production/production.py                                               
                 |  24 +-
 modules/production/stock.py                                                    
                 |   6 +-
 modules/production/tests/scenario_production_set_cost.rst                      
                 |   4 +-
 modules/project/work.py                                                        
                 |   2 +-
 modules/purchase/purchase.py                                                   
                 |  37 ++--
 modules/purchase_amendment/purchase.py                                         
                 |   8 +-
 modules/purchase_blanket_agreement/purchase.py                                 
                 |  21 +-
 modules/purchase_price_list/purchase.py                                        
                 |   5 +-
 modules/purchase_request_quotation/purchase.py                                 
                 |   2 +-
 modules/purchase_requisition/purchase.py                                       
                 |   7 +-
 modules/purchase_secondary_unit/purchase.py                                    
                 |   6 +-
 modules/sale/sale.py                                                           
                 |  45 +++--
 modules/sale/tests/scenario_sale_quotation.rst                                 
                 |   5 +-
 modules/sale_amendment/sale.py                                                 
                 |   8 +-
 modules/sale_blanket_agreement/sale.py                                         
                 |  21 +-
 modules/sale_complaint/complaint.py                                            
                 |  12 +-
 modules/sale_extra/sale.py                                                     
                 |   2 +-
 modules/sale_gift_card/sale.py                                                 
                 |  17 +-
 modules/sale_opportunity/opportunity.py                                        
                 |   3 +-
 modules/sale_point/sale.py                                                     
                 |   9 +-
 modules/sale_price_list/sale.py                                                
                 |  11 +-
 modules/sale_promotion/sale.py                                                 
                 |   2 +-
 modules/sale_rental/sale.py                                                    
                 |  16 +-
 modules/sale_secondary_unit/sale.py                                            
                 |   6 +-
 modules/sale_shipment_cost/stock.py                                            
                 |  10 +-
 modules/sale_subscription/subscription.py                                      
                 |  18 +-
 modules/stock/inventory.py                                                     
                 |  12 +-
 modules/stock/move.py                                                          
                 |  43 ++--
 modules/stock/shipment.py                                                      
                 |  81 ++++++---
 modules/stock/tests/scenario_stock_period.rst                                  
                 |   5 +-
 modules/stock/tests/scenario_stock_recompute_average_cost_price.rst            
                 |  18 +-
 modules/stock_forecast/forecast.py                                             
                 |   6 +-
 modules/stock_product_location_place/stock.py                                  
                 |  14 +-
 
modules/stock_quantity_early_planning/tests/scenario_stock_quantity_early_planning_incoming.rst
 |   6 +-
 modules/stock_shipment_cost/stock.py                                           
                 |  26 ++-
 modules/stock_shipment_measurements/stock.py                                   
                 |  30 +++
 trytond/trytond/ir/cron.py                                                     
                 |  10 +-
 trytond/trytond/model/modelstorage.py                                          
                 |   5 +-
 59 files changed, 454 insertions(+), 317 deletions(-)

diffs (2202 lines):

diff -r 3bccf6f3ed9c -r 6e6176c1eb20 demo/production.py
--- a/demo/production.py        Mon Aug 31 13:45:03 2026 +0200
+++ b/demo/production.py        Mon Aug 31 23:21:03 2026 +0200
@@ -111,13 +111,11 @@
                         if 'production_work' in activated:
                             for work in production.works:
                                 for _ in range(0, random.randint(1, 2)):
-                                    cycle = WorkCycle(
-                                        work=work,
-                                        duration=dt.timedelta(
-                                            seconds=random.randint(60, 3600)),
-                                        )
+                                    cycle = WorkCycle(work=work)
                                     cycle.save()
                                     cycle.click('run')
+                                    cycle.duration = dt.timedelta(
+                                        seconds=random.randint(60, 3600))
                                     cycle.click('do')
                         output, = production.outputs
                         output.unit_price = (production.cost
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/account/account.py
--- a/modules/account/account.py        Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/account/account.py        Mon Aug 31 23:21:03 2026 +0200
@@ -629,8 +629,8 @@
             if (not isinstance(field, fields.Field)
                     or isinstance(field, fields.Function)):
                 continue
-            field.states['readonly'] = (
-                Bool(Eval('template', -1)) & ~Eval('template_override', False))
+            field.states['editable'] = (
+                Eval('template', None) & ~Eval('template_override', False))
     return Mixin
 
 
@@ -828,8 +828,7 @@
         ModelSQL, ModelView):
     __name__ = 'account.account'
     _states = {
-        'readonly': (Bool(Eval('template', -1))
-            & ~Eval('template_override', False)),
+        'editable': Eval('template', None) & ~Eval('template_override', False),
         }
     company = fields.Many2One('company.company', 'Company', required=True,
             ondelete="RESTRICT")
@@ -842,14 +841,14 @@
             ('id', '!=', Eval('currency', -1)),
             ],
         states={
-            'readonly': _states['readonly'],
+            'editable': _states['editable'],
             'invisible': ~Eval('deferral', False),
             },
         help="Force all moves for this account to have this second currency.")
     type = fields.Many2One(
         'account.account.type', "Type", ondelete='RESTRICT',
         states={
-            'readonly': _states['readonly'],
+            'editable': _states['editable'],
             },
         domain=[
             ('company', '=', Eval('company', -1)),
@@ -857,10 +856,10 @@
     debit_type = fields.Many2One(
         'account.account.type', "Debit Type", ondelete='RESTRICT',
         states={
-            'readonly': _states['readonly'],
+            'editable': _states['editable'],
             'invisible': (
                 ~Eval('type') | Eval('credit_type')
-                | (_states['readonly']) & ~Eval('debit_type')),
+                | (_states['editable']) & ~Eval('debit_type')),
             },
         domain=[
             ('company', '=', Eval('company', -1)),
@@ -872,10 +871,10 @@
     credit_type = fields.Many2One(
         'account.account.type', "Credit Type", ondelete='RESTRICT',
         states={
-            'readonly': _states['readonly'],
+            'editable': _states['editable'],
             'invisible': (
                 ~Eval('type') | Eval('debit_type')
-                | (_states['readonly']) & ~Eval('credit_type')),
+                | (_states['editable']) & ~Eval('credit_type')),
             },
         domain=[
             ('company', '=', Eval('company', -1)),
@@ -939,7 +938,7 @@
         'account.account', "Replaced By",
         domain=[('company', '=', Eval('company', -1))],
         states={
-            'readonly': _states['readonly'],
+            'editable': _states['editable'],
             'invisible': ~Eval('end_date'),
             })
     template = fields.Many2One('account.account.template', 'Template')
@@ -955,8 +954,8 @@
         super().__setup__()
         for date in [cls.start_date, cls.end_date]:
             date.states = {
-                'readonly': (Bool(Eval('template', -1))
-                    & ~Eval('template_override', False)),
+                'editable': (
+                    Eval('template', -1) & ~Eval('template_override', False)),
                 }
         cls._order.insert(0, ('code', 'ASC'))
         cls._order.insert(1, ('name', 'ASC'))
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/account/move.py
--- a/modules/account/move.py   Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/account/move.py   Mon Aug 31 23:21:03 2026 +0200
@@ -942,8 +942,9 @@
         ondelete='CASCADE',
         states={
             'required': False,
-            'readonly': (((Eval('state') == 'valid') | _states['readonly'])
-                & Bool(Eval('move'))),
+            'readonly': (
+                ((Eval('state') == 'valid') | _states['readonly'])
+                & (Eval('move') & (Eval('id', -1) >= 0))),
             })
     journal = fields.Function(fields.Many2One(
             'account.journal', 'Journal',
@@ -1567,6 +1568,7 @@
             default = {}
         else:
             default = default.copy()
+        default.setdefault('state', 'draft')
         default.setdefault('move', None)
         default.setdefault('reconciliation', None)
         default.setdefault('reconciliations_delegated', [])
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/account_asset/asset.py
--- a/modules/account_asset/asset.py    Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/account_asset/asset.py    Mon Aug 31 23:21:03 2026 +0200
@@ -15,7 +15,7 @@
 from trytond.modules.company import CompanyReport
 from trytond.modules.currency.fields import Monetary
 from trytond.pool import Pool
-from trytond.pyson import Bool, Eval, If
+from trytond.pyson import Eval, If
 from trytond.tools import cached_property
 from trytond.transaction import Transaction, check_access, without_check_access
 from trytond.wizard import (
@@ -57,7 +57,8 @@
     number = fields.Char("Number", readonly=True)
     product = fields.Many2One('product.product', 'Product', required=True,
         states={
-            'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             },
         context={
             'company': Eval('company', None),
@@ -81,7 +82,8 @@
                 ],
             ],
         states={
-            'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0])
             })
     customer_invoice_line = fields.Function(fields.Many2One(
             'account.invoice.line', 'Customer Invoice Line'),
@@ -102,19 +104,21 @@
     quantity = fields.Float(
         "Quantity", digits='unit',
         states={
-            'readonly': (Bool(Eval('supplier_invoice_line', 1))
-                | Eval('lines', [0])
-                | (Eval('state') != 'draft')),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(
+                Eval('supplier_invoice_line', 1)
+                | Eval('lines', [0])),
             })
     unit = fields.Many2One('product.uom', 'Unit',
         states={
-            'readonly': (Bool(Eval('product'))
-                | (Eval('state') != 'draft')),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('product'),
             })
     value = Monetary(
         "Value", currency='currency', digits='currency',
         states={
-            'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             },
         required=True,
         help="The value of the asset when purchased.")
@@ -124,7 +128,8 @@
             ('depreciated_amount', '<=', Eval('value')),
             ],
         states={
-            'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             },
         required=True,
         help="The amount already depreciated at the start date.")
@@ -139,22 +144,26 @@
             ('residual_value', '<=', Eval('depreciating_value')),
             ],
         states={
-            'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             })
     purchase_date = fields.Date('Purchase Date', states={
-            'readonly': (Bool(Eval('supplier_invoice_line', 1))
-                | Eval('lines', [0])
-                | (Eval('state') != 'draft')),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(
+                Eval('supplier_invoice_line', 1)
+                | Eval('lines', [0])),
             },
         required=True)
     start_date = fields.Date('Start Date', states={
-            'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             },
         required=True,
         domain=[('start_date', '<=', Eval('end_date', None))])
     end_date = fields.Date('End Date',
         states={
-            'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             },
         required=True,
         domain=[('end_date', '>=', Eval('start_date', None))])
@@ -162,7 +171,8 @@
             ('linear', 'Linear'),
             ], 'Depreciation Method',
         states={
-            'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             },
         required=True)
     frequency = fields.Selection([
@@ -171,7 +181,8 @@
             ], 'Frequency',
         required=True,
         states={
-            'readonly': (Eval('lines', [0]) | (Eval('state') != 'draft')),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             })
     state = fields.Selection([
             ('draft', 'Draft'),
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/account_budget/account.py
--- a/modules/account_budget/account.py Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/account_budget/account.py Mon Aug 31 23:21:03 2026 +0200
@@ -93,7 +93,8 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': Eval('company') & Eval('lines', [-1]),
+            'readonly': Eval('id', -1) >= 0,
+            'editable': ~Eval('lines', [-1]),
             },
         help="The company that the budget is associated with.")
     lines = None
@@ -305,13 +306,13 @@
     lines = fields.One2Many(
         'account.budget.line', 'budget', "Lines",
         states={
-            'readonly': Eval('id', -1) < 0,
+            'editable': Eval('id', -1) < 0,
             },
         order=[('left', 'ASC'), ('id', 'ASC')])
     root_lines = fields.One2Many(
         'account.budget.line', 'budget', "Lines",
         states={
-            'readonly': Eval('id', -1) < 0,
+            'editable': Eval('id', -1) < 0,
             },
         filter=[
             ('parent', '=', None),
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/account_invoice/invoice.py
--- a/modules/account_invoice/invoice.py        Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/account_invoice/invoice.py        Mon Aug 31 23:21:03 2026 +0200
@@ -75,9 +75,9 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': (
-                _states['readonly']
-                | Eval('party', True)
+            'readonly': _states['readonly'],
+            'editable': ~(
+                Eval('party', True)
                 | Eval('lines', [0])),
             },
         context={
@@ -100,8 +100,9 @@
             ('in', "Supplier"),
             ], "Type", required=True,
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | Eval('context', {}).get('type')
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(
+                Eval('context', {}).get('type')
                 | (Eval('lines', [0]) & Eval('type'))),
             })
     type_name = fields.Function(fields.Char('Type'), 'get_type_name')
@@ -200,9 +201,8 @@
         domain=[('party', '=', Eval('party', -1))])
     currency = fields.Many2One('currency.currency', 'Currency', required=True,
         states={
-            'readonly': (
-                _states['readonly']
-                | (Eval('lines', [0]) & Eval('currency'))),
+            'readonly': _states['readonly'],
+            'editable': ~(Eval('lines', [0]) & Eval('currency')),
             })
     currency_date = fields.Function(fields.Date('Currency Date'),
         'on_change_with_currency_date')
@@ -1692,6 +1692,7 @@
         default.setdefault('supplier_payment_reference')
         default.setdefault('supplier_payment_reference_type')
         default.setdefault('sequence')
+        default.setdefault('sequence_type_cache')
         default.setdefault('move', None)
         default.setdefault('additional_moves', None)
         default.setdefault('cancel_move', None)
@@ -2459,7 +2460,8 @@
             'required': (~Eval('invoice_type') & Eval('party')
                 & Eval('currency') & Eval('company')),
             'invisible': Bool(Eval('context', {}).get('standalone')),
-            'readonly': _states['readonly'] & Bool(Eval('invoice')),
+            'readonly': _states['readonly'],
+            'editable': ~Eval('invoice'),
             })
     invoice_party = fields.Function(
         fields.Many2One(
@@ -2480,7 +2482,9 @@
     invoice_type = fields.Selection(
         'get_invoice_types', "Invoice Type",
         states={
-            'readonly': Eval('context', {}).get('type') | Eval('type'),
+            'editable': (
+                ~Eval('context', {}).get('type')
+                & ~Eval('invoice_type')),
             'required': ~Eval('invoice'),
             })
     party = fields.Many2One(
@@ -3241,7 +3245,8 @@
     invoice = fields.Many2One(
         'account.invoice', "Invoice", ondelete='CASCADE', required=True,
         states={
-            'readonly': _states['readonly'] & Bool(Eval('invoice')),
+            'readonly': _states['readonly'],
+            'editable': ~Eval('invoice'),
             })
     invoice_state = fields.Function(
         fields.Selection('get_invoice_states', "Invoice State"),
@@ -3287,18 +3292,18 @@
             ('company', '=', Eval('_parent_invoice', {}).get('company', 0)),
             ],
         states={
-            'readonly': (
-                ~Eval('manual', False) | ~Bool(Eval('invoice'))
-                | _states['readonly']),
+            'readonly': _states['readonly'],
+            'editable': Eval('manual', False) & Bool(Eval('invoice')),
             },
         depends={'invoice'})
     legal_notice = fields.Text(
         "Legal Notice",
         states={
-            'readonly': (_states['readonly']
-                & ~Id('account', 'group_account_admin').in_(
+            'readonly': _states['readonly'],
+            'editable': (
+                Id('account', 'group_account_admin').in_(
                     Eval('context', {}).get('groups', []))
-                & ~Eval('context', {}).get('administrator', False)),
+                | Eval('context', {}).get('administrator', False)),
             })
 
     del _states
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 
modules/account_invoice/tests/scenario_invoice_check_source.rst
--- a/modules/account_invoice/tests/scenario_invoice_check_source.rst   Mon Aug 
31 13:45:03 2026 +0200
+++ b/modules/account_invoice/tests/scenario_invoice_check_source.rst   Mon Aug 
31 23:21:03 2026 +0200
@@ -58,7 +58,7 @@
     ...         'source_untaxed_amount': Decimal('100.00'),
     ...         'source_tax_amount': Decimal('10.00'),
     ...         'source_total_amount': Decimal('110.00'),
-    ...         }, invoice._context)
+    ...         }, {**invoice._context, '_check_access': False})
 
 Try to validate::
 
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/account_invoice_defer/account.py
--- a/modules/account_invoice_defer/account.py  Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/account_invoice_defer/account.py  Mon Aug 31 23:21:03 2026 +0200
@@ -63,7 +63,8 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': (Eval('state') != 'draft') & Eval('invoice_line'),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('invoice_line'),
             })
     type = fields.Selection([
             ('out', "Customer"),
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/account_product/product.py
--- a/modules/account_product/product.py        Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/account_product/product.py        Mon Aug 31 23:21:03 2026 +0200
@@ -57,7 +57,7 @@
     accounting = fields.Boolean(
         "Accounting",
         states={
-            'readonly': Bool(Eval('childs', [0])) | Bool(Eval('parent')),
+            'editable': Bool(Eval('childs', [0])) | Bool(Eval('parent')),
             },
         help="Check to indicate the category is used for accounting.")
     account_parent = fields.Boolean('Use Parent\'s accounts',
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/account_statement/statement.py
--- a/modules/account_statement/statement.py    Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/account_statement/statement.py    Mon Aug 31 23:21:03 2026 +0200
@@ -79,7 +79,8 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             })
     journal = fields.Many2One(
         'account.statement.journal', "Journal", required=True,
@@ -87,7 +88,8 @@
             ('company', '=', Eval('company', -1)),
             ],
         states={
-            'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             })
     currency = fields.Function(fields.Many2One(
             'currency.currency', "Currency"), 'on_change_with_currency')
@@ -786,10 +788,11 @@
         states=_states,
         context={'with_payment': False})
     origin = fields.Many2One(
-        'account.statement.origin', 'Origin', readonly=True,
+        'account.statement.origin', 'Origin',
         ondelete='RESTRICT',
         states={
             'invisible': ~Bool(Eval('origin')),
+            'readonly': Eval('id', -1) >= 0,
             },
         domain=[
             ('statement', '=', Eval('statement', -1)),
@@ -802,9 +805,8 @@
     def __setup__(cls):
         super().__setup__()
         table = cls.__table__()
-        cls.date.states['readonly'] = (
-            cls.date.states.get('readonly', False)
-            | Bool(Eval('origin', 0)))
+        cls.date.states['readonly'] = cls.date.states.get('readonly', False)
+        cls.date.states['editable'] = ~Bool(Eval('origin', 0))
         cls.account.required = True
         cls.party.states['required'] = (
             cls.party.states.get('required', False)
@@ -1184,7 +1186,8 @@
 
 
 _states = {
-    'readonly': (Eval('statement_state') != 'draft') | Eval('lines', []),
+    'readonly': Eval('statement_state') != 'draft',
+    'editable': ~Eval('lines', []),
     }
 
 
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/account_stock_eu_excise/product.py
--- a/modules/account_stock_eu_excise/product.py        Mon Aug 31 13:45:03 
2026 +0200
+++ b/modules/account_stock_eu_excise/product.py        Mon Aug 31 23:21:03 
2026 +0200
@@ -3,7 +3,7 @@
 
 from trytond.model import DeactivableMixin, ModelSQL, ModelView, Unique, fields
 from trytond.pool import PoolMeta
-from trytond.pyson import Bool, Eval, If
+from trytond.pyson import Eval, If
 from trytond.transaction import Transaction
 
 
@@ -75,7 +75,7 @@
     country = fields.Many2One(
         'country.country', "Country", required=True,
         states={
-            'readonly': Bool(Eval('excise_tax')),
+            'editable': ~Eval('excise_tax'),
             })
 
     @classmethod
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/account_stock_eu_excise/sale.py
--- a/modules/account_stock_eu_excise/sale.py   Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/account_stock_eu_excise/sale.py   Mon Aug 31 23:21:03 2026 +0200
@@ -29,7 +29,8 @@
             ],
         states={
             'invisible': ~Eval('warehouse_eu_excise_number'),
-            'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             })
     eu_excise_types = fields.Function(
         fields.MultiSelection(IDENTIFIER_TYPES, "Excise Types"),
@@ -41,9 +42,11 @@
     @classmethod
     def __setup__(cls):
         super().__setup__()
-        cls.warehouse.states['readonly'] = (
-            cls.warehouse.states.get('readonly', False)
-            | Eval('eu_excise_number'))
+        editable = ~Eval('eu_excise_number')
+        if cls.warehouse.states.get('editable'):
+            cls.warehouse.states['editable'] &= editable
+        else:
+            cls.warehouse.states['editable'] = editable
 
     @fields.depends(methods=['_clear_eu_excise_number'])
     def on_change_party(self):
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 
modules/account_stock_shipment_cost/account.py
--- a/modules/account_stock_shipment_cost/account.py    Mon Aug 31 13:45:03 
2026 +0200
+++ b/modules/account_stock_shipment_cost/account.py    Mon Aug 31 23:21:03 
2026 +0200
@@ -448,9 +448,9 @@
     __name__ = 'account.invoice.line'
     shipment_cost = fields.Many2One(
         'account.shipment_cost', "Shipment Cost",
-        readonly=True,
         states={
             'invisible': ~Eval('shipment_cost'),
+            'editable': False,
             })
 
     @classmethod
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 
modules/account_tax_rule_country/purchase.py
--- a/modules/account_tax_rule_country/purchase.py      Mon Aug 31 13:45:03 
2026 +0200
+++ b/modules/account_tax_rule_country/purchase.py      Mon Aug 31 23:21:03 
2026 +0200
@@ -13,8 +13,11 @@
     def __setup__(cls):
         super().__setup__()
         for field in (cls.invoice_address, cls.warehouse):
-            field.states['readonly'] |= (
-                Eval('lines', [0]) & Eval('invoice_address'))
+            editable = ~(Eval('lines', [0]) & Eval('invoice_address'))
+            if field.states.get('editable'):
+                field.states['editable'] &= editable
+            else:
+                field.states['editable'] = editable
 
 
 class Line(metaclass=PoolMeta):
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/account_tax_rule_country/sale.py
--- a/modules/account_tax_rule_country/sale.py  Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/account_tax_rule_country/sale.py  Mon Aug 31 23:21:03 2026 +0200
@@ -13,8 +13,11 @@
     def __setup__(cls):
         super().__setup__()
         for field in (cls.shipment_party, cls.shipment_address, cls.warehouse):
-            field.states['readonly'] |= (
-                Eval('lines', [0]) & Eval('shipment_address'))
+            editable = ~(Eval('lines', [0]) & Eval('shipment_address'))
+            if field.states.get('editable'):
+                field.states['editable'] &= editable
+            else:
+                field.states['editable'] = editable
 
 
 class Line(metaclass=PoolMeta):
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/analytic_budget/analytic_account.py
--- a/modules/analytic_budget/analytic_account.py       Mon Aug 31 13:45:03 
2026 +0200
+++ b/modules/analytic_budget/analytic_account.py       Mon Aug 31 23:21:03 
2026 +0200
@@ -53,7 +53,7 @@
             ('type', '=', 'root'),
             ],
         states={
-            'readonly': Eval('root') & Eval('lines', [-1]),
+            'editable': ~(Eval('root') & Eval('lines', [-1])),
             })
     lines = fields.One2Many(
         'analytic_account.budget.line', 'budget', "Lines",
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/carrier_weight/carrier.py
--- a/modules/carrier_weight/carrier.py Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/carrier_weight/carrier.py Mon Aug 31 23:21:03 2026 +0200
@@ -17,14 +17,14 @@
         states={
             'invisible': Eval('carrier_cost_method') != 'weight',
             'required': Eval('carrier_cost_method') == 'weight',
-            'readonly': Bool(Eval('weight_price_list', [])),
+            'editable': Bool(Eval('weight_price_list', [])),
             },
         help="The Unit of Measure of weight criteria for the price list.")
     weight_currency = fields.Many2One('currency.currency', 'Currency',
         states={
             'invisible': Eval('carrier_cost_method') != 'weight',
             'required': Eval('carrier_cost_method') == 'weight',
-            'readonly': Bool(Eval('weight_price_list', [])),
+            'editable': Bool(Eval('weight_price_list', [])),
             },
         help="The currency of the price.")
     weight_price_list = fields.One2Many('carrier.weight_price_list', 'carrier',
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/party/party.py
--- a/modules/party/party.py    Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/party/party.py    Mon Aug 31 23:21:03 2026 +0200
@@ -47,7 +47,8 @@
     code = fields.Char(
         "Code", required=True,
         states={
-            'readonly': Eval('code_readonly', True),
+            'readonly': Eval('id', -1) >= 0,
+            'editable': ~Eval('code_readonly', True),
             },
         help="The unique identifier of the party.")
     code_readonly = fields.Function(fields.Boolean('Code Readonly'),
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/party_relationship/party.py
--- a/modules/party_relationship/party.py       Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/party_relationship/party.py       Mon Aug 31 23:21:03 2026 +0200
@@ -196,7 +196,8 @@
         Relation = pool.get('party.relation')
         vlist = cls._before_create(vlist)
         relations = Relation.create(vlist)
-        return cls.browse(cls._after_create([r.id * 2 for r in relations]))
+        return cls.browse(cls._after_create(
+                [r.id * 2 for r in relations], vlist))
 
     @classmethod
     def write(cls, *args):
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/product/product.py
--- a/modules/product/product.py        Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/product/product.py        Mon Aug 31 23:21:03 2026 +0200
@@ -153,7 +153,7 @@
     list_price = fields.MultiValue(fields.Numeric(
             "List Price", digits=price_digits,
             states={
-                'readonly': ~Eval('context', {}).get('company'),
+                'editable': ~Eval('context', {}).get('company'),
                 },
             help="The standard price the product is sold at."))
     list_prices = fields.One2Many(
@@ -515,7 +515,7 @@
     list_price = fields.MultiValue(fields.Numeric(
             "List Price", digits=price_digits,
             states={
-                'readonly': ~Eval('context', {}).get('company'),
+                'editable': ~Eval('context', {}).get('company'),
                 },
             help="The standard price the variant is sold at.\n"
             "Leave empty to use the list price of the product."))
@@ -528,7 +528,7 @@
     cost_price = fields.MultiValue(fields.Numeric(
             "Cost Price", digits=price_digits,
             states={
-                'readonly': ~Eval('context', {}).get('company'),
+                'editable': ~Eval('context', {}).get('company'),
                 },
             help="The amount it costs to purchase or make the variant, "
             "or carry out the service."))
@@ -633,6 +633,7 @@
                     setattr(cls, 'order_%s' % attr, order_method)
                 if isinstance(tfield, fields.One2Many):
                     getattr(cls, attr).setter = '_set_template_function'
+                    getattr(cls, attr)._field.readonly = False
 
     @classmethod
     def _set_template_function(cls, products, name, value):
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/product_cost_warehouse/product.py
--- a/modules/product_cost_warehouse/product.py Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/product_cost_warehouse/product.py Mon Aug 31 23:21:03 2026 +0200
@@ -40,7 +40,7 @@
         readonly = (
             Eval('context', {}).get('cost_price_warehouse', False)
             & ~Eval('context', {}).get('warehouse'))
-        if cls.cost_price.states['readonly']:
+        if cls.cost_price.states.get('readonly'):
             cls.cost_price.states['readonly'] |= readonly
         else:
             cls.cost_price.states['readonly'] = readonly
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/production/production.py
--- a/modules/production/production.py  Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/production/production.py  Mon Aug 31 23:21:03 2026 +0200
@@ -47,16 +47,20 @@
             ('type', '=', 'warehouse'),
             ],
         states={
-            'readonly': (~Eval('state').in_(['request', 'draft'])
-                | Eval('inputs', [-1]) | Eval('outputs', [-1])),
+            'readonly': ~Eval('state').in_(['request', 'draft']),
+            'editable': ~(
+                Eval('inputs', [-1])
+                | Eval('outputs', [-1])),
             })
     location = fields.Many2One('stock.location', 'Location', required=True,
         domain=[
             ('type', '=', 'production'),
             ],
         states={
-            'readonly': (~Eval('state').in_(['request', 'draft'])
-                | Eval('inputs', [-1]) | Eval('outputs', [-1])),
+            'readonly': ~Eval('state').in_(['request', 'draft']),
+            'editable': ~(
+                Eval('inputs', [-1])
+                | Eval('outputs', [-1])),
             })
     type = fields.Selection([
             ('assembly', "Assembly"),
@@ -87,8 +91,8 @@
                 ),
             ],
         states={
-            'readonly': (~Eval('state').in_(['request', 'draft'])
-                | ~Eval('warehouse', 0) | ~Eval('location', 0)),
+            'readonly': ~Eval('state').in_(['request', 'draft']),
+            'editable': Eval('warehouse', None) & Eval('location', None),
             'invisible': ~Eval('product'),
             })
     uom_category = fields.Function(fields.Many2One(
@@ -123,8 +127,8 @@
             ('company', '=', Eval('company', -1)),
             ],
         states={
-            'readonly': (~Eval('state').in_(['request', 'draft', 'waiting'])
-                | ~Eval('warehouse') | ~Eval('location')),
+            'readonly': ~Eval('state').in_(['request', 'draft', 'waiting']),
+            'editable': Eval('warehouse', None) & Eval('location', None),
             })
     outputs = fields.One2Many(
         'stock.move', 'production_output', "Output Materials",
@@ -138,8 +142,8 @@
             ('company', '=', Eval('company', -1)),
             ],
         states={
-            'readonly': (Eval('state').in_(['done', 'cancelled'])
-                | ~Eval('warehouse') | ~Eval('location')),
+            'readonly': Eval('state').in_(['done', 'cancelled']),
+            'editable': Eval('warehouse', None) & Eval('location', None),
             })
 
     assigned_by = employee_field("Assigned By")
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/production/stock.py
--- a/modules/production/stock.py       Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/production/stock.py       Mon Aug 31 23:21:03 2026 +0200
@@ -46,7 +46,7 @@
 class Move(metaclass=PoolMeta):
     __name__ = 'stock.move'
     production_input = fields.Many2One(
-        'production', "Production Input", readonly=True, ondelete='CASCADE',
+        'production', "Production Input", ondelete='CASCADE',
         domain=[
             ('company', '=', Eval('company', -1)),
             If(Eval('production_output', None),
@@ -55,9 +55,10 @@
             ],
         states={
             'invisible': ~Eval('production_input'),
+            'readonly': Eval('id', -1) >= 0,
             })
     production_output = fields.Many2One(
-        'production', "Production Output", readonly=True, ondelete='CASCADE',
+        'production', "Production Output", ondelete='CASCADE',
         domain=[
             ('company', '=', Eval('company', -1)),
             If(Eval('production_input', None),
@@ -66,6 +67,7 @@
             ],
         states={
             'invisible': ~Eval('production_output'),
+            'readonly': Eval('id', -1) >= 0,
             })
     production = fields.Function(fields.Many2One(
             'production', "Production",
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 
modules/production/tests/scenario_production_set_cost.rst
--- a/modules/production/tests/scenario_production_set_cost.rst Mon Aug 31 
13:45:03 2026 +0200
+++ b/modules/production/tests/scenario_production_set_cost.rst Mon Aug 31 
23:21:03 2026 +0200
@@ -82,7 +82,9 @@
 
     >>> Move = Model.get('stock.move')
     >>> input, = production.inputs
-    >>> Move.write([input], {'cost_price': Decimal(6)}, config.context)
+    >>> Move.write(
+    ...     [input], {'cost_price': Decimal(6)},
+    ...     {**config.context, '_check_access': False})
     >>> input.reload()
     >>> bool(input.production_cost_price_updated)
     True
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/project/work.py
--- a/modules/project/work.py   Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/project/work.py   Mon Aug 31 23:21:03 2026 +0200
@@ -161,7 +161,7 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': (
+            'editable': ~(
                 Eval('origin', None)
                 | Eval('parent', None)
                 | Eval('children', [])
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/purchase/purchase.py
--- a/modules/purchase/purchase.py      Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/purchase/purchase.py      Mon Aug 31 23:21:03 2026 +0200
@@ -85,9 +85,9 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | Eval('lines', [0])
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(
+                Eval('lines', [0])
                 | Eval('party', True)
                 | Eval('invoice_party', True)),
             })
@@ -112,8 +112,8 @@
             })
     party = fields.Many2One('party.party', 'Party', required=True,
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | (Eval('lines', [0]) & Eval('party'))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(Eval('lines', [0]) & Eval('party')),
             },
         context={
             'company': Eval('company', -1),
@@ -132,8 +132,8 @@
         depends={'company'})
     invoice_party = fields.Many2One('party.party', "Invoice Party",
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | Eval('lines', [0])),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             },
         context={
             'company': Eval('company', -1),
@@ -157,16 +157,14 @@
         domain=[('type', '=', 'warehouse')], states=_states)
     currency = fields.Many2One('currency.currency', 'Currency', required=True,
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | (Eval('lines', [0]) & Eval('currency'))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(Eval('lines', [0]) & Eval('currency')),
             })
     lines = fields.One2Many(
         'purchase.line', 'purchase', "Lines",
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | ~Eval('company')
-                | ~Eval('currency')),
+            'readonly': Eval('state') != 'draft',
+            'editable': Eval('company', None) & Eval('currency', None),
             })
     line_lines = fields.One2Many(
         'purchase.line', 'purchase', "Line - Lines", readonly=True,
@@ -1177,8 +1175,8 @@
     purchase = fields.Many2One(
         'purchase.purchase', "Purchase", ondelete='CASCADE', required=True,
         states={
-            'readonly': ((Eval('purchase_state') != 'draft')
-                & Bool(Eval('purchase'))),
+            'readonly': Eval('purchase_state') != 'draft',
+            'editable': ~Eval('purchase'),
             })
     type = fields.Selection([
         ('line', 'Line'),
@@ -1403,9 +1401,9 @@
     delivery_date = fields.Function(fields.Date('Delivery Date',
             states={
                 'invisible': Eval('type') != 'line',
-                'readonly': (Eval('purchase_state').in_(
-                        ['processing', 'done', 'cancelled'])
-                    | ~Eval('delivery_date_edit', False)),
+                'readonly': Eval('purchase_state').in_(
+                        ['processing', 'done', 'cancelled']),
+                'editable': Eval('delivery_date_edit', False),
                 }),
         'on_change_with_delivery_date', setter='set_delivery_date')
     delivery_date_edit = fields.Boolean(
@@ -1425,7 +1423,7 @@
             },
         help="Check to edit the delivery date.")
     delivery_date_store = fields.Date(
-        "Delivery Date", readonly=True,
+        "Delivery Date",
         domain=[
             If(Eval('type') != 'line',
                 ('delivery_date_store', '=', None),
@@ -1433,6 +1431,7 @@
             ],
         states={
             'invisible': Eval('type') != 'line',
+            'editable': False
             })
     purchase_state = fields.Function(
         fields.Selection('get_purchase_states', 'Purchase State'),
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/purchase_amendment/purchase.py
--- a/modules/purchase_amendment/purchase.py    Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/purchase_amendment/purchase.py    Mon Aug 31 23:21:03 2026 +0200
@@ -42,7 +42,8 @@
             ('state', 'in', ['processing', 'done']),
             ],
         states={
-            'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             })
     date = fields.Date(
         "Date", required=True,
@@ -248,9 +249,8 @@
             ('default_uom_category', '=', Eval('product_uom_category', -1)),
             ],
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | ~Eval('product_uom_category', None)),
+            'readonly': Eval('state') != 'draft',
+            'editable': Bool(Eval('product_uom_category', None)),
             'invisible': Eval('action') != 'line',
             })
     product_supplier = fields.Many2One(
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 
modules/purchase_blanket_agreement/purchase.py
--- a/modules/purchase_blanket_agreement/purchase.py    Mon Aug 31 13:45:03 
2026 +0200
+++ b/modules/purchase_blanket_agreement/purchase.py    Mon Aug 31 23:21:03 
2026 +0200
@@ -115,9 +115,9 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | Eval('lines', [0])
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(
+                Eval('lines', [0])
                 | Eval('supplier', True)),
             })
     number = fields.Char("Number", readonly=True)
@@ -130,9 +130,8 @@
     supplier = fields.Many2One(
         'party.party', "Supplier", required=True,
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | (Eval('lines', [0]) & Eval('supplier'))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(Eval('lines', [0]) & Eval('supplier', None)),
             },
         context={
             'company': Eval('company', -1),
@@ -163,16 +162,14 @@
     currency = fields.Many2One(
         'currency.currency', "Currency", required=True,
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | (Eval('lines', [0]) & Eval('currency', 0))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(Eval('lines', [0]) & Eval('currency', 0)),
             })
     lines = fields.One2Many(
         'purchase.blanket_agreement.line', 'blanket_agreement', "Lines",
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | ~Eval('supplier'))
+            'readonly': Eval('state') != 'draft',
+            'editable': Bool(Eval('supplier', None)),
             })
 
     amount = fields.Function(Monetary(
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/purchase_price_list/purchase.py
--- a/modules/purchase_price_list/purchase.py   Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/purchase_price_list/purchase.py   Mon Aug 31 23:21:03 2026 +0200
@@ -10,10 +10,7 @@
     @classmethod
     def __setup__(cls):
         super().__setup__()
-        cls.party.states['readonly'] = (cls.party.states['readonly']
-            | Eval('lines', [0]))
-        cls.lines.states['readonly'] = (cls.lines.states['readonly']
-            | ~Eval('party'))
+        cls.lines.states['editable'] &= Eval('party', None)
 
 
 class Line(metaclass=PoolMeta):
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 
modules/purchase_request_quotation/purchase.py
--- a/modules/purchase_request_quotation/purchase.py    Mon Aug 31 13:45:03 
2026 +0200
+++ b/modules/purchase_request_quotation/purchase.py    Mon Aug 31 23:21:03 
2026 +0200
@@ -111,7 +111,7 @@
     supplier = fields.Many2One(
         'party.party', "Supplier", required=True,
         states={
-            'readonly': Eval('lines', [0]) & Eval('supplier'),
+            'editable': Eval('lines', [0]) & Eval('supplier'),
             },
         context={
             'company': Eval('company', -1),
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/purchase_requisition/purchase.py
--- a/modules/purchase_requisition/purchase.py  Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/purchase_requisition/purchase.py  Mon Aug 31 23:21:03 2026 +0200
@@ -78,7 +78,8 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             })
     number = fields.Char('Number', readonly=True)
     description = fields.Char('Description', states=_states)
@@ -99,8 +100,8 @@
     currency = fields.Many2One(
         'currency.currency', 'Currency',
         states={
-            'readonly': (_states['readonly']
-                | (Eval('lines', [0]) & Eval('currency'))),
+            'readonly': _states['readonly'],
+            'editable': ~(Eval('lines', [0]) & Eval('currency')),
             })
     total_amount = fields.Function(
         Monetary("Total", currency='currency', digits='currency'),
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/purchase_secondary_unit/purchase.py
--- a/modules/purchase_secondary_unit/purchase.py       Mon Aug 31 13:45:03 
2026 +0200
+++ b/modules/purchase_secondary_unit/purchase.py       Mon Aug 31 23:21:03 
2026 +0200
@@ -43,14 +43,16 @@
     secondary_uom_factor = fields.Float(
         "Secondary UoM Factor",
         states={
-            'readonly': True,
+            'readonly': Eval('purchase_state') != 'draft',
+            'editable': False,
             'required': (Eval('type') == 'line') & Eval('secondary_unit'),
             },
         help="The factor for the secondary Unit of Measure.")
     secondary_uom_rate = fields.Float(
         "Secondary UoM Rate",
         states={
-            'readonly': True,
+            'readonly': Eval('purchase_state') != 'draft',
+            'editable': False,
             'required': (Eval('type') == 'line') & Eval('secondary_unit'),
             },
         help="The rate for the secondary Unit of Measure.")
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale/sale.py
--- a/modules/sale/sale.py      Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale/sale.py      Mon Aug 31 23:21:03 2026 +0200
@@ -81,9 +81,9 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | Eval('lines', [0])
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(
+                Eval('lines', [0])
                 | Eval('party', True)
                 | Eval('invoice_party', True)
                 | Eval('shipment_party', True)),
@@ -97,9 +97,8 @@
     quotation_date = fields.Date(
         "Quotation Date",
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | (Eval('number') & Eval('lines'))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(Eval('number') & Eval('lines', [0])),
             'invisible': ~Eval('state').in_(['draft', 'quotation']),
             },
         help="When the quotation was edited.")
@@ -140,8 +139,8 @@
     party = fields.Many2One(
         'party.party', "Party", required=True,
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | (Eval('lines', [0]) & Eval('party'))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(Eval('lines', [0]) & Eval('party')),
             },
         context={
             'company': Eval('company', -1),
@@ -160,8 +159,8 @@
         depends={'company'})
     invoice_party = fields.Many2One('party.party', "Invoice Party",
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | Eval('lines', [0])),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             },
         context={
             'company': Eval('company', -1),
@@ -213,16 +212,14 @@
             })
     currency = fields.Many2One('currency.currency', 'Currency', required=True,
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | (Eval('lines', [0]) & Eval('currency', 0))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(Eval('lines', [0]) & Eval('currency', 0)),
             })
     lines = fields.One2Many(
         'sale.line', 'sale', "Lines",
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | ~Eval('company')
-                | ~Eval('currency')),
+            'readonly': Eval('state') != 'draft',
+            'editable': Eval('company', None) & Eval('currency', None),
             })
     line_lines = fields.One2Many(
         'sale.line', 'sale', "Line - Lines", readonly=True,
@@ -557,6 +554,10 @@
         return 'none'
 
     @classmethod
+    def default_to_invoice(cls):
+        return False
+
+    @classmethod
     def default_shipment_method(cls, **pattern):
         Config = Pool().get('sale.configuration')
         config = Config(1)
@@ -566,6 +567,10 @@
     def default_shipment_state():
         return 'none'
 
+    @classmethod
+    def default_to_ship(cls):
+        return False
+
     @fields.depends(
         'company', 'party', 'invoice_party', 'shipment_party', 'warehouse',
         'payment_term', 'lines')
@@ -916,10 +921,10 @@
         default.setdefault('number', None)
         default.setdefault('reference')
         default.setdefault('invoice_state', 'none')
-        default.setdefault('to_invoice')
+        default.setdefault('to_invoice', cls.default_to_invoice())
         default.setdefault('invoices_ignored', None)
         default.setdefault('shipment_state', 'none')
-        default.setdefault('to_ship')
+        default.setdefault('to_ship', cls.default_to_ship())
         default.setdefault('quotation_date')
         default.setdefault('sale_date', None)
         default.setdefault('quoted_by')
@@ -1389,8 +1394,8 @@
     sale = fields.Many2One(
         'sale.sale', "Sale", ondelete='CASCADE', required=True,
         states={
-            'readonly': ((Eval('sale_state') != 'draft')
-                & Bool(Eval('sale'))),
+            'readonly': Eval('sale_state') != 'draft',
+            'editable': ~Eval('sale'),
             })
     type = fields.Selection([
         ('line', 'Line'),
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 
modules/sale/tests/scenario_sale_quotation.rst
--- a/modules/sale/tests/scenario_sale_quotation.rst    Mon Aug 31 13:45:03 
2026 +0200
+++ b/modules/sale/tests/scenario_sale_quotation.rst    Mon Aug 31 23:21:03 
2026 +0200
@@ -46,8 +46,9 @@
 
 Expire quotation::
 
-    >>> sale.quotation_date = today - dt.timedelta(weeks=2)
-    >>> sale.save()
+    >>> Sale.write([sale.id], {
+    ...        'quotation_date': today - dt.timedelta(weeks=2)
+    ...        }, {**config._context, '_check_access': False})
 
     >>> cron, = Cron.find(
     ...     [('method', '=', 'sale.sale|cancel_expired_quotation')], limit=1)
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale_amendment/sale.py
--- a/modules/sale_amendment/sale.py    Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale_amendment/sale.py    Mon Aug 31 23:21:03 2026 +0200
@@ -42,7 +42,8 @@
             ('state', 'in', ['processing', 'done']),
             ],
         states={
-            'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             })
     date = fields.Date(
         "Date", required=True,
@@ -286,9 +287,8 @@
             ('default_uom_category', '=', Eval('product_uom_category', -1)),
             ],
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | ~Eval('product_uom_category', None)),
+            'readonly': Eval('state') != 'draft',
+            'editable': Bool(Eval('product_uom_category', None)),
             'invisible': Eval('action') != 'line',
             })
     quantity = fields.Float(
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale_blanket_agreement/sale.py
--- a/modules/sale_blanket_agreement/sale.py    Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale_blanket_agreement/sale.py    Mon Aug 31 23:21:03 2026 +0200
@@ -115,9 +115,9 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | Eval('lines', [0])
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(
+                Eval('lines', [0])
                 | Eval('customer', True)),
             })
     number = fields.Char("Number", readonly=True)
@@ -130,9 +130,8 @@
     customer = fields.Many2One(
         'party.party', "Customer", required=True,
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | (Eval('lines', [0]) & Eval('customer'))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(Eval('lines', [0]) & Eval('customer', None)),
             },
         context={
             'company': Eval('company', -1),
@@ -163,16 +162,14 @@
     currency = fields.Many2One(
         'currency.currency', "Currency", required=True,
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | (Eval('lines', [0]) & Eval('currency', 0))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(Eval('lines', [0]) & Eval('currency', 0)),
             })
     lines = fields.One2Many(
         'sale.blanket_agreement.line', 'blanket_agreement', "Lines",
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | ~Eval('customer'))
+            'readonly': Eval('state') != 'draft',
+            'editable': Bool(Eval('customer', None)),
             })
 
     amount = fields.Function(Monetary(
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale_complaint/complaint.py
--- a/modules/sale_complaint/complaint.py       Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale_complaint/complaint.py       Mon Aug 31 23:21:03 2026 +0200
@@ -56,7 +56,8 @@
     company = fields.Many2One(
         'company.company', 'Company', required=True,
         states={
-            'readonly': _states['readonly'] | Eval('origin'),
+            'readonly': _states['readonly'],
+            'editable': ~Eval('origin'),
             })
     type = fields.Many2One('sale.complaint.type', 'Type', required=True,
         states=_states)
@@ -96,8 +97,8 @@
                 ],
             },
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | Bool(Eval('actions', [0]))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('actions', [0]),
             'required': Bool(Eval('origin_model')),
             },
         depends={'origin_model'})
@@ -108,8 +109,9 @@
     description = fields.Text('Description', states=_states)
     actions = fields.One2Many('sale.complaint.action', 'complaint', 'Actions',
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | (If(~Eval('origin_id', 0), 0, Eval('origin_id', 0)) <= 0)),
+            'readonly': Eval('state') != 'draft',
+            'editable': (
+                If(~Eval('origin_id', 0), -1, Eval('origin_id', -1)) < 0),
             },
         depends={'origin_model'})
     submitted_by = employee_field(
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale_extra/sale.py
--- a/modules/sale_extra/sale.py        Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale_extra/sale.py        Mon Aug 31 23:21:03 2026 +0200
@@ -71,7 +71,7 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': Eval('id', 0) > 0,
+            'readonly': Eval('id', -1) >= 0,
             })
     start_date = fields.Date('Start Date',
         domain=['OR',
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale_gift_card/sale.py
--- a/modules/sale_gift_card/sale.py    Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale_gift_card/sale.py    Mon Aug 31 23:21:03 2026 +0200
@@ -11,7 +11,7 @@
 from trytond.modules.company.model import CompanyValueMixin
 from trytond.modules.currency.fields import Monetary
 from trytond.pool import Pool, PoolMeta
-from trytond.pyson import Bool, Eval, Id
+from trytond.pyson import Eval, Id
 from trytond.report import Report, get_email
 from trytond.sendmail import send_message_transactional
 from trytond.tools.email_ import (
@@ -58,7 +58,9 @@
     _rec_name = 'number'
 
     _states = {
-        'readonly': Bool(Eval('origin')) | Bool(Eval('spent_on')),
+        'readonly': (
+            (Eval('origin', None) | Eval('spent_on', None))
+            & (Eval('id', -1) >= 0)),
         }
 
     number = fields.Char(
@@ -80,9 +82,16 @@
     currency = fields.Many2One(
         'currency.currency', "Currency", required=True, states=_states)
 
-    origin = fields.Reference("Origin", selection='get_origin', readonly=True)
+    origin = fields.Reference(
+        "Origin", selection='get_origin',
+        states={
+            'editable': Eval('id', -1) >= 0,
+        })
     spent_on = fields.Reference(
-        "Spent On", selection='get_spent_on', readonly=True)
+        "Spent On", selection='get_spent_on',
+        states={
+            'editable': Eval('id', -1) >= 0,
+        })
 
     del _states
 
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale_opportunity/opportunity.py
--- a/modules/sale_opportunity/opportunity.py   Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale_opportunity/opportunity.py   Mon Aug 31 23:21:03 2026 +0200
@@ -65,7 +65,8 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': _states_stop['readonly'] | Eval('party', True),
+            'readonly': _states_stop['readonly'],
+            'editable': ~Eval('party', True),
             },
         domain=[
             ('id', If(In('company', Eval('context', {})), '=', '!='),
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale_point/sale.py
--- a/modules/sale_point/sale.py        Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale_point/sale.py        Mon Aug 31 23:21:03 2026 +0200
@@ -111,9 +111,8 @@
     point = fields.Many2One(
         'sale.point', "Point", required=True, ondelete='RESTRICT',
         states={
-            'readonly': ((Eval('id', 0) > 0)
-                | Bool(Eval('lines', [0]))
-                | _states['readonly']),
+            'readonly': (Eval('id', -1) >= 0) | _states['readonly'],
+            'editable': ~Eval('lines', [0]),
             },
         domain=[
             ('company', '=', Eval('company', -1)),
@@ -482,12 +481,12 @@
     unit_list_price = fields.Numeric(
         "Unit List Price", digits=price_digits, required=True,
         states={
-            'readonly': True,  # Allow client to sent value
+            'editable': False,
             })
     unit_gross_price = fields.Numeric(
         "Unit Gross Price", digits=price_digits, required=True,
         states={
-            'readonly': True,  # Allow client to sent value
+            'editable': False,
             })
     unit_price = fields.Function(fields.Numeric(
             "Unit Price", digits=price_digits,
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale_price_list/sale.py
--- a/modules/sale_price_list/sale.py   Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale_price_list/sale.py   Mon Aug 31 23:21:03 2026 +0200
@@ -2,7 +2,7 @@
 # this repository contains the full copyright notices and license terms.
 from trytond.model import fields
 from trytond.pool import Pool, PoolMeta
-from trytond.pyson import Bool, Equal, Eval, Not, Or
+from trytond.pyson import Eval
 
 
 class Sale(metaclass=PoolMeta):
@@ -11,17 +11,14 @@
         help="Use to compute the unit price of lines.",
         domain=[('company', '=', Eval('company', -1))],
         states={
-            'readonly': Or(Not(Equal(Eval('state'), 'draft')),
-                Bool(Eval('lines', [0]))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             })
 
     @classmethod
     def __setup__(cls):
         super().__setup__()
-        cls.party.states['readonly'] = (cls.party.states['readonly']
-            | Eval('lines', [0]))
-        cls.lines.states['readonly'] = (cls.lines.states['readonly']
-            | ~Eval('party') | ~Eval('company'))
+        cls.lines.states['editable'] &= Eval('party', None)
 
     @fields.depends('company')
     def on_change_party(self):
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale_promotion/sale.py
--- a/modules/sale_promotion/sale.py    Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale_promotion/sale.py    Mon Aug 31 23:21:03 2026 +0200
@@ -184,7 +184,7 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': Eval('id', 0) > 0,
+            'readonly': Eval('id', -1) >= 0,
             })
     start_date = fields.Date('Start Date',
         domain=['OR',
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale_rental/sale.py
--- a/modules/sale_rental/sale.py       Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale_rental/sale.py       Mon Aug 31 23:21:03 2026 +0200
@@ -130,7 +130,8 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': (Eval('state') != 'draft') | Eval('party', True),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('party', True),
             },
         help="Make the sale rental belong to the company.")
 
@@ -149,9 +150,8 @@
     party = fields.Many2One(
         'party.party', "Party", required=True,
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | (Eval('lines', [0]) & Eval('party'))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(Eval('lines', [0]) & Eval('party')),
             },
         context={
             'company': Eval('company', -1),
@@ -169,8 +169,8 @@
         depends={'company', 'party'})
     invoice_party = fields.Many2One('party.party', "Invoice Party",
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | Eval('lines', [0])),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             },
         context={
             'company': Eval('company', -1),
@@ -207,8 +207,8 @@
     currency = fields.Many2One(
         'currency.currency', "Currency", required=True,
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | (Eval('lines', [0]) & Eval('currency', 0))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(Eval('lines', [0]) & Eval('currency', None)),
             })
 
     start = fields.Function(
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale_secondary_unit/sale.py
--- a/modules/sale_secondary_unit/sale.py       Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale_secondary_unit/sale.py       Mon Aug 31 23:21:03 2026 +0200
@@ -43,14 +43,16 @@
     secondary_uom_factor = fields.Float(
         "Secondary UoM Factor",
         states={
-            'readonly': True,
+            'readonly': Eval('sale_state') != 'draft',
+            'editable': False,
             'required': (Eval('type') == 'line') & Eval('secondary_unit'),
             },
         help="The factor for the secondary Unit of Measure.")
     secondary_uom_rate = fields.Float(
         "Secondary UoM Rate",
         states={
-            'readonly': True,
+            'readonly': Eval('sale_state') != 'draft',
+            'editable': False,
             'required': (Eval('type') == 'line') & Eval('secondary_unit'),
             },
         help="The rate for the secondary Unit of Measure.")
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale_shipment_cost/stock.py
--- a/modules/sale_shipment_cost/stock.py       Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale_shipment_cost/stock.py       Mon Aug 31 23:21:03 2026 +0200
@@ -17,9 +17,8 @@
             'currency.currency', "Cost Sale Currency",
             states={
                 'invisible': Eval('cost_sale_method') != 'shipment',
-                'readonly': (
-                    Eval('shipment_cost_sale_readonly', True)
-                    | ~Eval('cost_edit', False)),
+                'readonly': Eval('shipment_cost_sale_readonly', True),
+                'editable': Eval('cost_edit', False),
                 }),
         'on_change_with_cost_sale_currency_used', setter='set_cost')
     cost_sale_currency = fields.Many2One(
@@ -33,9 +32,8 @@
             "Cost Sale", digits=price_digits,
             states={
                 'invisible': Eval('cost_sale_method') != 'shipment',
-                'readonly': (
-                    Eval('shipment_cost_sale_readonly', True)
-                    | ~Eval('cost_edit', False)),
+                'readonly': Eval('shipment_cost_sale_readonly', True),
+                'editable': Eval('cost_edit', False),
                 }),
         'on_change_with_cost_sale_used', setter='set_cost')
     cost_sale = fields.Numeric(
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/sale_subscription/subscription.py
--- a/modules/sale_subscription/subscription.py Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/sale_subscription/subscription.py Mon Aug 31 23:21:03 2026 +0200
@@ -32,9 +32,9 @@
     company = fields.Many2One(
         'company.company', "Company", required=True,
         states={
-            'readonly': (
-                (Eval('state') != 'draft')
-                | Eval('lines', [0])
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(
+                Eval('lines', [0])
                 | Eval('party', True)
                 | Eval('invoice_party', True)),
             },
@@ -54,8 +54,8 @@
     party = fields.Many2One(
         'party.party', "Party", required=True,
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | (Eval('lines', [0]) & Eval('party'))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(Eval('lines', [0]) & Eval('party', None)),
             },
         context={
             'company': Eval('company', -1),
@@ -73,8 +73,8 @@
         depends={'company', 'party'})
     invoice_party = fields.Many2One('party.party', "Invoice Party",
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | Eval('lines', [0])),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             },
         context={
             'company': Eval('company', -1),
@@ -102,8 +102,8 @@
     currency = fields.Many2One(
         'currency.currency', "Currency", required=True,
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | (Eval('lines', [0]) & Eval('currency', 0))),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(Eval('lines', [0]) & Eval('currency', 0)),
             })
 
     start_date = fields.Date(
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/stock/inventory.py
--- a/modules/stock/inventory.py        Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/stock/inventory.py        Mon Aug 31 23:21:03 2026 +0200
@@ -34,12 +34,15 @@
         help="The main identifier for the inventory.")
     location = fields.Many2One(
         'stock.location', 'Location', required=True,
-        domain=[('type', '=', 'storage')], states={
-            'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
+        domain=[('type', '=', 'storage')],
+        states={
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             },
         help="The location inventoried.")
     date = fields.Date('Date', required=True, states={
-            'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             },
         help="The date of the stock count.")
     lines = fields.One2Many(
@@ -56,7 +59,8 @@
         help="How lines without a quantity are handled.")
     company = fields.Many2One('company.company', 'Company', required=True,
         states={
-            'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             },
         help="The company the inventory is associated with.")
     state = fields.Selection([
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/stock/move.py
--- a/modules/stock/move.py     Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/stock/move.py     Mon Aug 31 23:21:03 2026 +0200
@@ -18,7 +18,7 @@
 from trytond.model.exceptions import AccessError
 from trytond.modules.product import price_digits, round_price
 from trytond.pool import Pool
-from trytond.pyson import Bool, Equal, Eval, Id, If, TimeDelta
+from trytond.pyson import Bool, Eval, Id, If, TimeDelta
 from trytond.tools import cached_property
 from trytond.transaction import Transaction, without_check_access
 
@@ -214,8 +214,8 @@
     unit = fields.Many2One(
         'product.uom', "Unit", required=True,
         states={
-            'readonly': (Eval('state').in_(['cancelled', 'assigned', 'done'])
-                | Eval('unit_price')),
+            'readonly': Eval('state').in_(['cancelled', 'assigned', 'done']),
+            'editable': ~Eval('unit_price'),
             },
         domain=[
             If(~Eval('state').in_(['done', 'cancelled']),
@@ -239,10 +239,10 @@
         'stock.location', "From Location",
         required=True,
         states={
-            'readonly': (
-                STATES['readonly']
-                | (Eval('shipment')
-                    & Equal(Eval('_parent_shipment.id', None), None))),
+            'readonly': STATES['readonly'],
+            'editable': ~(
+                Eval('shipment')
+                | Eval('_parent_shipment', None)),
             },
         domain=[
             LOCATION_DOMAIN,
@@ -264,10 +264,10 @@
         'stock.location', "To Location",
         required=True,
         states={
-            'readonly': (
-                STATES['readonly']
-                | (Eval('shipment')
-                    & Equal(Eval('_parent_shipment.id', None), None))),
+            'readonly': STATES['readonly'],
+            'editable': ~(
+                Eval('shipment')
+                | Eval('_parent_shipment', None)),
             },
         domain=[
             LOCATION_DOMAIN,
@@ -286,9 +286,10 @@
             "To Location"),
         'on_change_with_to_location_name')
     shipment = fields.Reference(
-        "Shipment", selection='get_shipment', readonly=True,
+        "Shipment", selection='get_shipment',
         states={
             'invisible': ~Eval('shipment'),
+            'readonly': Eval('id', -1) >= 0,
             },
         help="Used to group several stock moves together.")
     origin = fields.Reference(
@@ -305,20 +306,20 @@
     planned_date = fields.Date(
         "Planned Date",
         states={
-            'readonly': (
-                Eval('state').in_(['cancelled', 'assigned', 'done'])
-                | Eval('shipment')
-                | Eval('_parent_shipment'))
+            'readonly': Eval('state').in_(['cancelled', 'assigned', 'done']),
+            'editable': ~(
+                Eval('shipment', None)
+                | Eval('_parent_shipment', None)),
             },
         help="When the stock is expected to be moved.")
     effective_date = fields.Date(
         "Effective Date",
         states={
             'required': Eval('state') == 'done',
-            'readonly': (
-                Eval('state').in_(['cancelled', 'done'])
-                | Eval('shipment')
-                | Eval('_parent_shipment')),
+            'readonly': Eval('state').in_(['cancelled', 'done']),
+            'editable': ~(
+                Eval('shipment', None)
+                | Eval('_parent_shipment', None)),
             },
         help="When the stock was actually moved.")
     delay = fields.Function(
@@ -1024,6 +1025,8 @@
     @classmethod
     def copy(cls, moves, default=None):
         default = default.copy() if default is not None else {}
+        default.setdefault('internal_quantity', 0)
+        default.setdefault('cost_price')
         default.setdefault('outcome_moves', None)
         return super().copy(moves, default=default)
 
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/stock/shipment.py
--- a/modules/stock/shipment.py Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/stock/shipment.py Mon Aug 31 23:21:03 2026 +0200
@@ -359,9 +359,10 @@
         help="The company the shipment is associated with.")
     supplier = fields.Many2One('party.party', 'Supplier',
         states={
-            'readonly': (((Eval('state') != 'draft')
-                    | Eval('incoming_moves', [0]))
-                & Eval('supplier')),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(
+                Eval('incoming_moves', [0])
+                & Eval('supplier', None)),
             }, required=True,
         context={
             'company': Eval('company', -1),
@@ -381,8 +382,10 @@
     warehouse = fields.Many2One('stock.location', "Warehouse",
         required=True, domain=[('type', '=', 'warehouse')],
         states={
-            'readonly': (Eval('state').in_(['cancelled', 'done'])
-                | Eval('incoming_moves', [0]) | Eval('inventory_moves', [0])),
+            'readonly': Eval('state').in_(['cancelled', 'done']),
+            'editable': ~(
+                Eval('incoming_moves', [0])
+                | Eval('inventory_moves', [0])),
             },
         help="Where the stock is received.")
     warehouse_input = fields.Many2One(
@@ -822,10 +825,12 @@
         help="The company the shipment is associated with.")
     supplier = fields.Many2One('party.party', 'Supplier',
         states={
-            'readonly': (((Eval('state') != 'draft')
-                    | Eval('moves', [0]))
-                    & Eval('supplier', 0)),
-            }, required=True,
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(
+                Eval('moves', [0])
+                & Eval('supplier', None)),
+            },
+        required=True,
         context={
             'company': Eval('company', -1),
             'party_contact_mechanism_usage': 'delivery',
@@ -847,14 +852,20 @@
             ],
         help="Where the stock is sent to.")
     from_location = fields.Many2One('stock.location', "From Location",
-        required=True, states={
-            'readonly': (Eval('state') != 'draft') | Eval('moves', [0]),
-            }, domain=[('type', 'in', ['storage', 'view'])],
+        required=True,
+        states={
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('moves', [0]),
+            },
+        domain=[('type', 'in', ['storage', 'view'])],
         help="Where the stock is moved from.")
     to_location = fields.Many2One('stock.location', "To Location",
-        required=True, states={
-            'readonly': (Eval('state') != 'draft') | Eval('moves', [0]),
-            }, domain=[('type', '=', 'supplier')],
+        required=True,
+        states={
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('moves', [0]),
+            },
+        domain=[('type', '=', 'supplier')],
         help="Where the stock is moved to.")
     warehouse = fields.Function(
         fields.Many2One('stock.location', "Warehouse"),
@@ -1162,8 +1173,8 @@
         help="The company the shipment is associated with.")
     customer = fields.Many2One('party.party', 'Customer', required=True,
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | Eval('outgoing_moves', [0])),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('outgoing_moves', [0]),
             },
         context={
             'company': Eval('company', -1),
@@ -1190,9 +1201,12 @@
         help="Where the stock is sent to.")
     warehouse = fields.Many2One('stock.location', "Warehouse", required=True,
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | Eval('outgoing_moves', [0]) | Eval('inventory_moves', [0])),
-            }, domain=[('type', '=', 'warehouse')],
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(
+                Eval('outgoing_moves', [0])
+                | Eval('inventory_moves', [0])),
+            },
+        domain=[('type', '=', 'warehouse')],
         help="Where the stock is sent from.")
     warehouse_storage = fields.Many2One(
         'stock.location', "Warehouse Storage", required=True,
@@ -1876,8 +1890,8 @@
         help="The company the shipment is associated with.")
     customer = fields.Many2One('party.party', 'Customer', required=True,
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | Eval('incoming_moves', [0])),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('incoming_moves', [0]),
             },
         context={
             'company': Eval('company', -1),
@@ -1896,9 +1910,12 @@
         help="The address the customer can be contacted at.")
     warehouse = fields.Many2One('stock.location', "Warehouse", required=True,
         states={
-            'readonly': ((Eval('state') != 'draft')
-                | Eval('incoming_moves', [0]) | Eval('inventory_moves', [0])),
-            }, domain=[('type', '=', 'warehouse')],
+            'readonly': Eval('state') != 'draft',
+            'editable': ~(
+                Eval('incoming_moves', [0])
+                | Eval('inventory_moves', [0])),
+            },
+        domain=[('type', '=', 'warehouse')],
         help="Where the stock is returned.")
     warehouse_storage = fields.Many2One(
         'stock.location', "Warehouse Storage", required=True,
@@ -2337,18 +2354,20 @@
         help="The company the shipment is associated with.")
     from_location = fields.Many2One('stock.location', "From Location",
         required=True, states={
-            'readonly': (~Eval('state').in_(['request', 'draft'])
-                | Eval('moves', [0])),
+            'readonly': ~Eval('state').in_(['request', 'draft']),
+            'editable': ~Eval('moves', [0]),
             },
         domain=[
             ('type', 'in', ['view', 'storage', 'lost_found']),
             ],
         help="Where the stock is moved from.")
     to_location = fields.Many2One('stock.location', "To Location",
-        required=True, states={
-            'readonly': (~Eval('state').in_(['request', 'draft'])
-                    | Eval('moves', [0])),
-            }, domain=[
+        required=True,
+        states={
+            'readonly': ~Eval('state').in_(['request', 'draft']),
+            'editable': ~Eval('moves', [0]),
+            },
+        domain=[
             ('type', 'in', ['view', 'storage', 'lost_found']),
             ],
         help="Where the stock is moved to.")
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 
modules/stock/tests/scenario_stock_period.rst
--- a/modules/stock/tests/scenario_stock_period.rst     Mon Aug 31 13:45:03 
2026 +0200
+++ b/modules/stock/tests/scenario_stock_period.rst     Mon Aug 31 23:21:03 
2026 +0200
@@ -107,7 +107,10 @@
 
 Create an assigned move::
 
-    >>> Move.write([move], {'state': 'assigned'}, config._context)
+    >>> Move.write(
+    ...     [move], {'state': 'assigned'},
+    ...     {**config._context, '_check_access': False})
+    >>> move.reload()
     >>> move.state
     'assigned'
 
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 
modules/stock/tests/scenario_stock_recompute_average_cost_price.rst
--- a/modules/stock/tests/scenario_stock_recompute_average_cost_price.rst       
Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/stock/tests/scenario_stock_recompute_average_cost_price.rst       
Mon Aug 31 23:21:03 2026 +0200
@@ -10,7 +10,7 @@
 
     >>> from proteus import Model, Wizard
     >>> from trytond.modules.company.tests.tools import create_company, 
get_company
-    >>> from trytond.modules.stock.move import Move
+    >>> from trytond.modules.stock.move import Move as StockMove
     >>> from trytond.tests.tools import activate_modules, assertEqual, 
assertFalse
 
     >>> today = dt.date.today()
@@ -18,7 +18,7 @@
 Patch on_change_with_assignation_required::
 
     >>> _ = patch.object(
-    ...     Move, 'on_change_with_assignation_required',
+    ...     StockMove, 'on_change_with_assignation_required',
     ...     return_value=False).start()
 
 Activate modules::
@@ -29,6 +29,8 @@
 
     >>> company = get_company()
 
+    >>> Move = Model.get('stock.move')
+
 Create product::
 
     >>> ProductUom = Model.get('product.uom')
@@ -129,8 +131,10 @@
     ...         ])
     >>> bool(move.unit_price_updated)
     False
-    >>> move.unit_price = Decimal('130')
-    >>> move.save()
+    >>> Move.write(
+    ...     [move], {'unit_price': Decimal('130')},
+    ...     {**config._context, '_check_access': False})
+    >>> move.reload()
     >>> bool(move.unit_price_updated)
     True
 
@@ -151,8 +155,10 @@
 
 Launch cron task::
 
-    >>> move.unit_price = Decimal('120')
-    >>> move.save()
+    >>> Move.write(
+    ...     [move], {'unit_price': Decimal('120')},
+    ...     {**config._context, '_check_access': False})
+    >>> move.reload()
 
     >>> Cron = Model.get('ir.cron')
     >>> Company = Model.get('company.company')
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/stock_forecast/forecast.py
--- a/modules/stock_forecast/forecast.py        Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/stock_forecast/forecast.py        Mon Aug 31 23:21:03 2026 +0200
@@ -32,7 +32,8 @@
     warehouse = fields.Many2One(
         'stock.location', 'Location', required=True,
         domain=[('type', '=', 'warehouse')], states={
-            'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             })
     destination = fields.Many2One(
         'stock.location', 'Destination', required=True,
@@ -49,7 +50,8 @@
         'stock.forecast.line', 'forecast', 'Lines', states=_states)
     company = fields.Many2One(
         'company.company', 'Company', required=True, states={
-            'readonly': (Eval('state') != 'draft') | Eval('lines', [0]),
+            'readonly': Eval('state') != 'draft',
+            'editable': ~Eval('lines', [0]),
             })
     state = fields.Selection([
             ('draft', "Draft"),
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 
modules/stock_product_location_place/stock.py
--- a/modules/stock_product_location_place/stock.py     Mon Aug 31 13:45:03 
2026 +0200
+++ b/modules/stock_product_location_place/stock.py     Mon Aug 31 23:21:03 
2026 +0200
@@ -70,7 +70,7 @@
     __name__ = 'stock.move'
 
     from_place = fields.Many2One(
-        'stock.product.location.place', "From Place", readonly=True,
+        'stock.product.location.place', "From Place",
         domain=[
             If(~Eval('state').in_(['done', 'cancelled']),
                 ['OR',
@@ -79,9 +79,12 @@
                     ],
                 ('location', '=', Eval('from_location', -1)),
                 ),
-            ])
+            ],
+        states={
+            'editable': False,
+        })
     to_place = fields.Many2One(
-        'stock.product.location.place', "To Place", readonly=True,
+        'stock.product.location.place', "To Place",
         domain=[
             If(~Eval('state').in_(['done', 'cancelled']),
                 ['OR',
@@ -90,7 +93,10 @@
                     ],
                 ('location', '=', Eval('to_location', -1)),
                 ),
-            ])
+            ],
+        states={
+            'editable': False,
+        })
 
     @classmethod
     def __setup__(cls):
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 
modules/stock_quantity_early_planning/tests/scenario_stock_quantity_early_planning_incoming.rst
--- 
a/modules/stock_quantity_early_planning/tests/scenario_stock_quantity_early_planning_incoming.rst
   Mon Aug 31 13:45:03 2026 +0200
+++ 
b/modules/stock_quantity_early_planning/tests/scenario_stock_quantity_early_planning_incoming.rst
   Mon Aug 31 23:21:03 2026 +0200
@@ -93,7 +93,8 @@
     >>> move.quantity = 10
     >>> shipment_int1.save()
     >>> ShipmentInternal.write(
-    ...     [shipment_int1.id], {'state': 'request'}, shipment_int1._context)
+    ...     [shipment_int1.id], {'state': 'request'},
+    ...     {**shipment_int1._context, '_check_access': False})
 
     >>> shipment_int2 = ShipmentInternal()
     >>> shipment_int2.planned_date = week2
@@ -106,7 +107,8 @@
     >>> move.quantity = 5
     >>> shipment_int2.save()
     >>> ShipmentInternal.write(
-    ...     [shipment_int2.id], {'state': 'request'}, shipment_int2._context)
+    ...     [shipment_int2.id], {'state': 'request'},
+    ...     {**shipment_int2._context, '_check_access': False})
 
 Plan to ship in 3 weeks::
 
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 modules/stock_shipment_cost/stock.py
--- a/modules/stock_shipment_cost/stock.py      Mon Aug 31 13:45:03 2026 +0200
+++ b/modules/stock_shipment_cost/stock.py      Mon Aug 31 23:21:03 2026 +0200
@@ -26,6 +26,16 @@
         super().__setup__()
         cls._allow_modify_closed_period.add('shipment_out_cost_price')
 
+    @classmethod
+    def default_shipment_out_cost_price(cls):
+        return None
+
+    @classmethod
+    def copy(cls, moves, default=None):
+        default = default.copy() if default is not None else {}
+        default.setdefault('shipment_out_cost_price')
+        return super().copy(moves, default=default)
+
 
 class ShipmentCostMixin:
     __slots__ = ()
@@ -43,9 +53,8 @@
     cost_currency_used = fields.Function(fields.Many2One(
             'currency.currency', "Cost Currency",
             states={
-                'readonly': (
-                    Eval('shipment_cost_readonly', True)
-                    | ~Eval('cost_edit', False)),
+                'readonly': Eval('shipment_cost_readonly', True),
+                'editable': Eval('cost_edit', False),
                 }),
         'on_change_with_cost_currency_used', setter='set_cost')
     cost_currency = fields.Many2One(
@@ -58,12 +67,15 @@
     cost_used = fields.Function(fields.Numeric(
             "Cost", digits=price_digits,
             states={
-                'readonly': (
-                    Eval('shipment_cost_readonly', True)
-                    | ~Eval('cost_edit', False)),
+                'readonly': Eval('shipment_cost_readonly', True),
+                'editable': Eval('cost_edit', False),
                 }),
         'on_change_with_cost_used', setter='set_cost')
-    cost = fields.Numeric("Cost", digits=price_digits, readonly=True)
+    cost = fields.Numeric(
+        "Cost", digits=price_digits,
+        states={
+            'editable': False,
+        })
     cost_edit = fields.Boolean(
         "Edit Cost",
         states={
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 
modules/stock_shipment_measurements/stock.py
--- a/modules/stock_shipment_measurements/stock.py      Mon Aug 31 13:45:03 
2026 +0200
+++ b/modules/stock_shipment_measurements/stock.py      Mon Aug 31 23:21:03 
2026 +0200
@@ -83,6 +83,14 @@
         "Internal Volume", readonly=True,
         help="The volume of the moved product in liter.")
 
+    @classmethod
+    def default_internal_weight(cls):
+        return None
+
+    @classmethod
+    def default_internal_volume(cls):
+        return None
+
     @fields.depends(
         'quantity', 'unit', 'product',
         methods=['on_change_with_internal_quantity'])
@@ -148,6 +156,13 @@
         return values
 
     @classmethod
+    def copy(cls, moves, default=None):
+        default = default.copy() if default is not None else {}
+        default.setdefault('internal_weight', cls.default_internal_weight())
+        default.setdefault('internal_volume', cls.default_internal_volume())
+        return super().copy(moves, default=default)
+
+    @classmethod
     @ModelView.button
     @Workflow.transition('cancelled')
     def cancel(cls, moves):
@@ -230,6 +245,14 @@
             cls.set_measurements()
 
     @classmethod
+    def default_internal_weight(cls):
+        return None
+
+    @classmethod
+    def default_internal_volume(cls):
+        return None
+
+    @classmethod
     def _measurements_states(cls):
         return []
 
@@ -433,6 +456,13 @@
     def _measurements_location_condition(cls, table, move, location):
         raise NotImplementedError
 
+    @classmethod
+    def copy(cls, records, default=None):
+        default = default.copy() if default is not None else {}
+        default.setdefault('internal_weight', cls.default_internal_weight())
+        default.setdefault('internal_volume', cls.default_internal_volume())
+        return super().copy(records, default=default)
+
 
 def set_measurements(func):
     @wraps(func)
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 trytond/trytond/ir/cron.py
--- a/trytond/trytond/ir/cron.py        Mon Aug 31 13:45:03 2026 +0200
+++ b/trytond/trytond/ir/cron.py        Mon Aug 31 23:21:03 2026 +0200
@@ -36,7 +36,7 @@
 class Cron(DeactivableMixin, ModelSQL, ModelView):
     __name__ = "ir.cron"
     _states = {
-        'readonly': Eval('running', False),
+        'editable': Eval('running', False),
         }
     interval_number = fields.Integer(
         "Interval Number", required=True, states=_states)
@@ -53,7 +53,7 @@
             [('minute', '>=', 0), ('minute', '<=', 59)],
             ],
         states={
-            'readonly': _states['readonly'],
+            'editable': _states['editable'],
             'invisible': Eval('interval_type').in_(['minutes']),
             },
         depends=['interval_type'])
@@ -63,14 +63,14 @@
             [('hour', '>=', 0), ('hour', '<=', 23)],
             ],
         states={
-            'readonly': _states['readonly'],
+            'editable': _states['editable'],
             'invisible': Eval('interval_type').in_(['minutes', 'hours']),
             },
         depends=['interval_type'])
     weekday = fields.Many2One(
         'ir.calendar.day', "Day of Week",
         states={
-            'readonly': _states['readonly'],
+            'editable': _states['editable'],
             'invisible': Eval('interval_type').in_(
                 ['minutes', 'hours', 'days']),
             },
@@ -81,7 +81,7 @@
             ('day', '>=', 0),
             ],
         states={
-            'readonly': _states['readonly'],
+            'editable': _states['editable'],
             'invisible': Eval('interval_type').in_(
                 ['minutes', 'hours', 'days', 'weeks']),
             },
diff -r 3bccf6f3ed9c -r 6e6176c1eb20 trytond/trytond/model/modelstorage.py
--- a/trytond/trytond/model/modelstorage.py     Mon Aug 31 13:45:03 2026 +0200
+++ b/trytond/trytond/model/modelstorage.py     Mon Aug 31 23:21:03 2026 +0200
@@ -564,7 +564,10 @@
 
         def test_readonly(records, field_name, override):
             field = cls._fields[field_name]
-            if isinstance(field, (fields.One2Many, fields.Many2Many)):
+            if (isinstance(field, (fields.One2Many, fields.Many2Many))
+                or (isinstance(field, fields.Function)
+                    and isinstance(
+                        field._field, (fields.One2Many, fields.Many2Many)))):
                 # Must be enforced on the target
                 return
             if field.readonly:

Reply via email to