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: